Indirect Control Flow (ICF)

What is Indirect Control Flow (ICF)?

In the context of binary analysis, indirect control flow occurs when a branching instruction (such as jmp, call, etc.) has a variable destination (memory, register). A common term for this is indirection.

Taxonomy of Compilation-Induced ICF

Indirection from compilers are usually introduced from function pointers or jump tables. Other sources of indirection are usually some variations of this (e.g. vtable).

Function Pointers

In the program ls, we see function pointer calls, such as (*error_print_progname)(); and h->chunkfun.extra. The latter comes from obstack.c, where it needs to decide how it should allocate a chunk of memory depending on whether use_extra_arg is true.

call_chunkfun (struct obstack *h, size_t size)
{
  if (h->use_extra_arg)
    return h->chunkfun.extra (h->extra_arg, size);
  else
    return h->chunkfun.plain (size);
}

Dynamic Linking via Global Offset Table (GOT)

In Linux ELF binaries, you will typically see a GOT introduced by the linker.

In the program ls, functions like free and strcmp are used from the standard C library at runtime. For example, consider jmp *0x20c4c(%rip). The address of the pointer refers to <_GLOBAL_OFFSET_TABLE_+0x10> and is called from the PLT section.

GOT is used in conjunction with the PLT in the following way:
1. The program makes a shared library call.
2. The program jumps to the PLT entry for the function.
3. This PLT entry redirects to the dynamic linker via the GOT, as the address is not yet resolved.
4. The dynamic linker updates the GOT with the resolved address of the library call.

Jump Table

Jump tables are typically generated from switch statements that exceed a certain length. In the program sort, a jump table is used in lieu of consecutive sequence of cmp for its option parsing.

A more simple example would be something like the following:

void process_case(int value) {
    switch (value) {
        case 0: printf("Case 0\n"); break;
        case 1: printf("Case 1\n"); break;
        case 2: printf("Case 2\n"); break;
        case 3: printf("Case 3\n"); break;
        case 4: printf("Case 4\n"); break;
        default: printf("Default case\n"); break;
    }
}

In this case, having 6 cases is enough for x86-64 GCC 14.2 to conclude a jmp rax is necessary. x86-64 clang 19.1.0 on the other hand is okay with just 4 cases, including the default case.

Reproduce

You can reproduce the discovery of some of the results using this Github gist.

x