Debugging Stripped Binaries in GDB


Debugging executables is all well and good when they are compiled with GCC's -g flag to retain debugging information, but hackers have to deal with stripped binaries. By removing symbolic information unessential for correct execution, stripping not only saves disk space and potentially improves performance, but—in the context of security—serves as one level of obfuscation against prying eyes.

We examine the effects of stripping using GCC's -s flag on the following toy program that prints out a UID.

➜ cat getuid.c
#include <stdio.h>
#include <unistd.h>

int main() {
	printf("UID: %d\n", geteuid());
}
➜  gcc getuid.c -o getuid
➜  ./getuid 
UID: 1000
		

After compiling the program without any stripping, we can open the executable with GDB and readily place a breakpoint at a symbol. Shoutout to the extremely handy GDB enhancer, GEF.

➜ gdb getuid
Reading symbols from getuid...
gef➤  b main
Breakpoint 1 at 0x114d
		

But when we compile and strip the binary, we no longer have this luxury.

➜ gcc -s getuid.c -o getuid
➜ gdb getuid
Reading symbols from getuid...
Debuginfod has been disabled.
(No debugging symbols found in getuid)
gef➤  b main
Function "main" not defined.
		

Not to worry! We can still find our way in this seemingly hopeless scenario by finding the program's entry point offset, then its start address, and finally our destination.

Within GDB, we find the entry point with the command info files.

gef➤  info files
Symbols from "/home/raj/getuid".
Local exec file:
		`/home/raj/getuid', file type elf64-x86-64.
		Entry point: 0x1050
		0x0000000000000318 - 0x0000000000000334 is .interp
		0x0000000000000338 - 0x0000000000000378 is .note.gnu.property
		0x0000000000000378 - 0x000000000000039c is .note.gnu.build-id
		0x000000000000039c - 0x00000000000003bc is .note.ABI-tag
		0x00000000000003c0 - 0x00000000000003dc is .gnu.hash
		0x00000000000003e0 - 0x00000000000004a0 is .dynsym
		0x00000000000004a0 - 0x0000000000000537 is .dynstr
		0x0000000000000538 - 0x0000000000000548 is .gnu.version
		0x0000000000000548 - 0x0000000000000578 is .gnu.version_r
		0x0000000000000578 - 0x0000000000000638 is .rela.dyn
		0x0000000000000638 - 0x0000000000000668 is .rela.plt
		0x0000000000001000 - 0x000000000000101b is .init
		0x0000000000001020 - 0x0000000000001050 is .plt
		0x0000000000001050 - 0x000000000000116f is .text
		0x0000000000001170 - 0x000000000000117d is .fini
		0x0000000000002000 - 0x000000000000200d is .rodata
		0x0000000000002010 - 0x0000000000002034 is .eh_frame_hdr
		0x0000000000002038 - 0x00000000000020b4 is .eh_frame
		0x0000000000003dd0 - 0x0000000000003dd8 is .init_array
		0x0000000000003dd8 - 0x0000000000003de0 is .fini_array
		0x0000000000003de0 - 0x0000000000003fc0 is .dynamic
		0x0000000000003fc0 - 0x0000000000003fe8 is .got
		0x0000000000003fe8 - 0x0000000000004010 is .got.plt
		0x0000000000004010 - 0x0000000000004020 is .data
		0x0000000000004020 - 0x0000000000004028 is .bss			
		

Then, we find the start address by doing set stop-on-solib-events 1, running the program, and doing info proc map.

gef➤  set stop-on-solib-events 1
gef➤  r                                                                                   
Starting program: /home/raj/getuid 
Stopped due to shared library event (no libraries added or removed)
gef➤  info proc map
process 59703
Mapped address spaces:

          Start Addr           End Addr       Size     Offset  Perms  objfile
      0x555555554000     0x555555555000     0x1000        0x0  r--p   /home/raj/getuid
      0x555555555000     0x555555556000     0x1000     0x1000  r-xp   /home/raj/getuid
      0x555555556000     0x555555557000     0x1000     0x2000  r--p   /home/raj/getuid
      0x555555557000     0x555555559000     0x2000     0x2000  rw-p   /home/raj/getuid
      0x7ffff7fc4000     0x7ffff7fc8000     0x4000        0x0  r--p   [vvar]
      0x7ffff7fc8000     0x7ffff7fca000     0x2000        0x0  r-xp   [vdso]
      0x7ffff7fca000     0x7ffff7fcb000     0x1000        0x0  r--p   /usr/lib/ld-linux-x86-64.so.2
      0x7ffff7fcb000     0x7ffff7ff1000    0x26000     0x1000  r-xp   /usr/lib/ld-linux-x86-64.so.2
      0x7ffff7ff1000     0x7ffff7ffb000     0xa000    0x27000  r--p   /usr/lib/ld-linux-x86-64.so.2
      0x7ffff7ffb000     0x7ffff7fff000     0x4000    0x31000  rw-p   /usr/lib/ld-linux-x86-64.so.2
      0x7ffffffdd000     0x7ffffffff000    0x22000        0x0  rw-p   [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0  --xp   [vsyscall]
		

So, we find the two crucial pieces of information that we need: the entry point offset 0x1050 and the start address 0x555555554000. We set a breakpoint at their sum, and then continue twice to break at the entry point.

gef➤  b *(0x555555554000 + 0x1050)
Breakpoint 1 at 0x555555555050
gef➤  c                                                                                   
Continuing.                                                                               
[Thread debugging using libthread_db enabled]  
Using host libthread_db library "/usr/lib/libthread_db.so.1".                             
Stopped due to shared library event:                                                      
  Inferior loaded /usr/lib/libc.so.6
gef➤  c
Continuing.
  
Breakpoint 1, 0x0000555555555050 in ?? ()
		

We are now at the entry point, but not yet at main(). So, we keep pushing and examine the next few instructions.

gef➤  x /15i $rip
=>  0x555555555050:      endbr64 
	0x555555555054:      xor    ebp,ebp
	0x555555555056:      mov    r9,rdx
	0x555555555059:      pop    rsi
	0x55555555505a:      mov    rdx,rsp
	0x55555555505d:      and    rsp,0xfffffffffffffff0
	0x555555555061:      push   rax
	0x555555555062:      push   rsp
	0x555555555063:      xor    r8d,r8d
	0x555555555066:      xor    ecx,ecx
	0x555555555068:      lea    rdi,[rip+0xda]        # 0x555555555149
	0x55555555506f:      call   QWORD PTR [rip+0x2f4b]        # 0x555555557fc0
	0x555555555075:      hlt    
	0x555555555076:      cs nop WORD PTR [rax+rax*1+0x0]
	0x555555555080:      lea    rdi,[rip+0x2f99]        # 0x555555558020
	

At instruction 0x55555555506f we identify the call to __libc_start_main, which is located at the address loaded into register rdi in the previous instruction. So, we break at 0x5555555551f9 and continue execution.

gef➤  b *0x555555555149                                                               
Breakpoint 2 at 0x555555555149                                                            
gef➤  c                                                                                   
Continuing.                                                                               
						
Breakpoint 2, 0x0000555555555149 in ?? ()
	

Examining the next few instructions, we see the familiar function prologue and epilogue, as well as markers for the functions in our program.

gef➤  x/15i $rip
=>  0x555555555149:      push   rbp
	0x55555555514a:      mov    rbp,rsp
	0x55555555514d:      call   0x555555555040 <geteuid@plt>
	0x555555555152:      mov    esi,eax
	0x555555555154:      lea    rax,[rip+0xea9]        # 0x555555556004
	0x55555555515b:      mov    rdi,rax
	0x55555555515e:      mov    eax,0x0
	0x555555555163:      call   0x555555555030 <printf@plt>
	0x555555555168:      mov    eax,0x0
	0x55555555516d:      pop    rbp
	0x55555555516e:      ret    
	0x55555555516f:      add    bl,dh
	0x555555555171:      nop    edx
	0x555555555174:      sub    rsp,0x8
	0x555555555178:      add    rsp,0x8
	

Terrific! We finally made it to our main() function, and can now begin actually reversing (although there isn't much to reverse in our toy UID example).