0

So let's say the ELF binary is stripped - meaning no symbol table - and the _start function doesn't push the address of main before calling __libc_start_main.

This happened in a binary when compiled for both 32-bit and 64-bit architectures, not sure why sometimes _start doesn't push the absolute value and instead pushes a register value (if anyone knows please let me know).

Is there any way in this situation that I can generate the call-graph (without names) of the binary starting from main? Is there any way I can find the address of main? because right now it pushes a register value on the stack and the absolute address of main doesn't appear in any instruction!

I've added the a sample of the startup routine for a 32 bit version of a simple C program, compiled with gcc with -m32 (the 64 bit version is kinda the same in this section, no absolute address).

I'm not sure why some of my 32 bit programs push the absolute address of main before calling __libc_main_start and some don`t. Please let me know if you know the answer.

00001070 <_start>:
    1070:       31 ed                   xor    ebp,ebp
    1072:       5e                      pop    esi
    1073:       89 e1                   mov    ecx,esp
    1075:       83 e4 f0                and    esp,0xfffffff0
    1078:       50                      push   eax
    1079:       54                      push   esp
    107a:       52                      push   edx
    107b:       e8 22 00 00 00          call   10a2 <_start+0x32>
    1080:       81 c3 80 2f 00 00       add    ebx,0x2f80
    1086:       8d 83 50 d4 ff ff       lea    eax,[ebx-0x2bb0]
    108c:       50                      push   eax
    108d:       8d 83 f0 d3 ff ff       lea    eax,[ebx-0x2c10]
    1093:       50                      push   eax
    1094:       51                      push   ecx
    1095:       56                      push   esi
    1096:       ff b3 f8 ff ff ff       push   DWORD PTR [ebx-0x8]
    109c:       e8 9f ff ff ff          call   1040 <__libc_start_main@plt>
    10a1:       f4                      hlt
    10a2:       8b 1c 24                mov    ebx,DWORD PTR [esp]
    10a5:       c3                      ret
    10a6:       66 90                   xchg   ax,ax
    10a8:       66 90                   xchg   ax,ax
    10aa:       66 90                   xchg   ax,ax
    10ac:       66 90                   xchg   ax,ax
    10ae:       66 90                   xchg   ax,ax
  • Please share either the binaries themselves or disassembly of the relevant code so that we know what you are looking at. – julian Apr 26 at 9:44
  • @julian added a picture for a 32 bit version of a simple C program, compiled with gcc with -m32 ( the 64 bit version is kinda the same in this section, no absolute address ) – OneAndOnly Apr 26 at 11:32
  • 1
    Please add the code as text rather than as a screenshot – julian Apr 26 at 11:35
  • @julian alright done. – OneAndOnly Apr 26 at 11:37
1

The binary appears to be a position-independent executable (PIE).

This means absolute addresses cannot be pushed onto the stack: the purpose of position independent code is for code to be loaded to a random location in virtual memory upon process creation via address space layout randomization (ASLR) (implemented by the kernel), so no absolute addresses can be known prior to runtime. Instead, program counter-relative addressing is used, in which relocations are calculated in reference to the CPU instruction pointer.1

Note:

  1. the absolute address of main can be found at runtime
  2. the absolute address of main will be different every time the program is executed
  3. A call graph can be generated statically if the binary is unstripped, and a CFG can be generated whether a binary is stripped or not.

I was able to produce similar code when I compiled a C program with GCC using the -pie and fPIE options. Full command:

gcc -m32 -pie -fPIE [source_file] -o [output_file_name]


The offsets of the instructions in the provided assembly snippet are a big clue to what is going on here.

In a vanilla x86 ELF32 binary, the offset of the program initialization code (the first instruction of which is pointed to by the value in the e_entry field in the ELF header, aka the program entry point) will not be far from the canonical entry point of 0x8048000. Here is a sample, which we can use as a baseline for comparison:

080482f0 <_start>:
 80482f0:       31 ed                   xor    ebp,ebp
 80482f2:       5e                      pop    esi
 80482f3:       89 e1                   mov    ecx,esp
 80482f5:       83 e4 f0                and    esp,0xfffffff0
 80482f8:       50                      push   eax
 80482f9:       54                      push   esp
 80482fa:       52                      push   edx
 80482fb:       68 70 84 04 08          push   0x8048470
 8048300:       68 00 84 04 08          push   0x8048400
 8048305:       51                      push   ecx
 8048306:       56                      push   esi
 8048307:       68 ed 83 04 08          push   0x80483ed
 804830c:       e8 cf ff ff ff          call   80482e0 <__libc_start_main@plt>
 8048311:       f4                      hlt    
 8048312:       66 90                   xchg   ax,ax
 8048314:       66 90                   xchg   ax,ax

Now let us compare these instruction offsets with the offsets in the position-independent code:

00000530 <_start>:
 530:   31 ed                   xor    ebp,ebp
 532:   5e                      pop    esi
 533:   89 e1                   mov    ecx,esp
 535:   83 e4 f0                and    esp,0xfffffff0
 538:   50                      push   eax
 539:   54                      push   esp
 53a:   52                      push   edx
 53b:   e8 22 00 00 00          call   562 <_start+0x32>       <------------
 540:   81 c3 c0 1a 00 00       add    ebx,0x1ac0
 546:   8d 83 80 e7 ff ff       lea    eax,[ebx-0x1880]        <------------
 54c:   50                      push   eax
 54d:   8d 83 10 e7 ff ff       lea    eax,[ebx-0x18f0]        <------------
 553:   50                      push   eax
 554:   51                      push   ecx
 555:   56                      push   esi
 556:   ff b3 f4 ff ff ff       push   DWORD PTR [ebx-0xc]     <------------
 55c:   e8 bf ff ff ff          call   520 <__libc_start_main@plt>
 561:   f4                      hlt    
 562:   8b 1c 24                mov    ebx,DWORD PTR [esp]     <------------
 565:   c3                      ret    
 566:   66 90                   xchg   ax,ax
 568:   66 90                   xchg   ax,ax

Here we notice that the offsets here are completely different from those in the non-PIE ELF binary. Additionally, the arrows highlight similarities with the code snippet in the question that are particularly relevant in this context. Just like the code sample provided in the question, the code here is entirely devoid of absolute addresses.


References:

  1. What is the -fPIE option for position-independent executables in gcc and ld?
  • So how can i generate a call graph statically if i cant find the address of main and code is PIE? also why is compiled in PIE considering i didnt specify any option wtih GCC? i thought we had to say -fPIE for it to become PIE? – OneAndOnly Apr 26 at 14:00
  • @OneAndOnly I was able to generate a callgraph using Cutter just fine. PIC has no bearing on the ability to generate call graphs. GCC's behavior is deterministic - the same source will compile the same way each time the same flags/options are used. The simplest explanation for the behavior you are seeing would be user error, but I cannot explain to you why you are seeing what you are seeing unless you provide much more precise information about what you are doing. – julian Apr 26 at 14:06
  • but Im not looking for a tool, I'm writing my own tool and usually i find the main function address and start a recursive call generation from there, but now that i cant find it idk, and i cant just do it from the start of .text section since a lot of calls are indirect and i cant do it statically(don't know where to jump to when its indirect like call [eax]) – OneAndOnly Apr 26 at 14:14
  • @OneAndOnly this issue is entirely separate from the question above. My advice is to do some meaningful research before attempting to design your own implementation. PIC is not the problem (in fact, why would it be?); the problem is that the binary is stripped. Are you aware of the fact that in stripped binaries, finding function boundaries is an undecidable problem? Tools such as radare/Cutter and IDA use basic blocks, not function calls, to map program logic. – julian Apr 26 at 14:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.