Anatomy of a GDB anti-debug trick

Well, it seems this is the GDB post season! The past days have been dedicated to mess around with GDB source code and today I have what I think it’s a nice story to tell.

After hacking off my old wish of having the disassembly raw bytes to be printed (like Ollydbg, Softice, IDA, otx, etc…) I was interested in trying to fix one anti-debug trick. This presentation by nemo shows an anti-debug trick that works against GDB and others. The original description is: If you set the “number of sections” field in a SEGMENT_COMMAND to 0xffffffff many of the popular debuggers will crash. This bug exists in GDB, IDA Pro and the HTE hex editor.

Armed with my great reversing skills and my lame C skills I started searching for the problem… Oh man have I told you that GDB code is a nightmare? Probably I did! It’s a freaking nightmare…

This is what happens when I modified the number of sections at __TEXT segment (any segment does the trick):

$ gdb ./segment_command_number_of_sections_antidebug
GNU gdb 6.3.50-20050815 (Apple version gdb-768) (Thu Aug 13 13:17:30 UTC 2009)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-apple-darwin"... "./segment_command_number_of_sections_antidebug": not in executable
format: File format not recognized

gdb$ r
No executable file specified.
Use the "file" or "exec-file" command.

GDB can’t work with this modified executable file so the anti-debug trick is doing its job very well. Bfd is GDB component responsible for parsing the file headers and other stuff. It has its own directory at GDB source and you can find there many files related to the different formats it can parse. The mach-o.c should be naturally our main target and the error message the starting point. After a few attempts at inserting debug messages and many compilations, I finally managed to trace where the error was happening. As usual, this is the flow:

(…) bfd_mach_o_scan_read_command -> bfd_mach_o_scan_read_segment_32 -> bfd_mach_o_scan_read_segment -> bfd_mach_o_scan_read_section -> bfd_mach_o_scan_read_section_32

The important piece of code at bfd_mach_o_sca_read_command is:

 if (bfd_mach_o_scan_read_segment_32 (abfd, command) != 0)
  return -1;

Returning -1 signals an error and sets prints the error message that will be displayed by bfd_set_error (bfd_error_file_not_recognized); @ format.c (still at bfd directory). There are two instances of this function call - I used a few simple printf to find which one was used (simple tricks always work best). The next important piece of code is located at bfd_mach_o_scan_read_segment. Printfs were used once again to confirm the correct piece of code.

for (i = 0; i < seg->nsects; i++)
 bfd_vma segoff;
 if (wide)
  segoff = command->offset + 64 + 8 + (i * 80);
  segoff = command->offset + 48 + 8 + (i * 68);

 if (bfd_mach_o_scan_read_section(abfd, &seg->sections[i], segoff, wide) != 0)
  return -1;

That was the return responsible for the error message. I modified it to 0 and voila, GDB worked without a problem. Time to go deeper into bfd_mach_o_scan_read_section_32. This function had two return -1 instances, so printf to the rescue and the first one is the guilty piece of code.

static int
bfd_mach_o_scan_read_section_32 (bfd *abfd,
bfd_mach_o_section *section,
bfd_vma offset)
 unsigned char buf[68];

 bfd_seek (abfd, offset, SEEK_SET);
 if (bfd_bread ((PTR) buf, 68, abfd) != 68)
  return -1;


If bfd_read can’t retrieve 68 bytes, then it’s an error… Once again, I used printfs (this is getting annoying hehe) to check what offset and what sizes were read and returned. That made clear that failure was due to less than expected retrieved bytes. Let me get back to bfd_mach_o_scan_read_segment to resume the problem.

for (i = 0; i < seg->nsects; i++)
 bfd_vma segoff;
 if (wide)
  segoff = command->offset + 64 + 8 + (i * 80);
  segoff = command->offset + 48 + 8 + (i * 68);

 if (bfd_mach_o_scan_read_section(abfd, &seg->sections[i], segoff, wide) != 0)
  return -1;

seg->nsects holds the number of sections from the header. So if the header says it has 1000 sections, this routine will try to read 1000 sections. I’m pretty sure you can now spot the problem! In reality the executable doesn’t have 1000 sections so the routine will keep reading things outside the header. If the program size is less than the size that will be read by the routine, then an error will be raised (by that small piece of code that expects 68 bytes). In reality the anti-debug trick doesn’t require the number of sections to be 0xFFFFFFFF but just large enough to be bigger than the program size or not be evenly divisible by 68 bytes. Of course 0xFFFFFFFF is the best bet but it’s not the only value that works.

I’m not sure if nemo knew this or just fuzzed the header (most probably he knew since he rules) but I had a lot of fun tracking this bug/problem!

The problem here is that GDB blindly trusts the information from the Mach-O header. This is bad design from a security point of view. You shouldn’t trust external input! GDB should parse the whole file and verify if the information is true and consistent with the header, else it opens the door for this kind of tricks. An easy workaround to avoid the error is to check if the size given from the number of sections is compatible with the executable size. It’s a lame workaround but it saves you from editing the binary and fixing the header. Yes, I’m a bit lazy sometimes but I do believe that computers exist to do the work for me 😉.

About the raw bytes disassembly display, have a look at this example:

Breakpoint 1, 0x000023f0 in ?? ()
EAX: 000023F0  EBX: 00001000  ECX: 00000001  EDX: 00000000  o d I t S z A P c
ESI: 00000000  EDI: 00000000  EBP: 00000000  ESP: BFFFF8D4  EIP: 000023F0
CS: 0017  DS: 001F  ES: 001F  FS: 0000  GS: 0037  SS: 001F
0x23f0:     6a 00                         push   0x0
0x23f2:     89 e5                         mov    ebp,esp
0x23f4:     83 e4 f0                      and    esp,0xfffffff0
0x23f7:     83 ec 10                      sub    esp,0x10
0x23fa:     8b 5d 04                      mov    ebx,DWORD PTR [ebp+0x4]
0x23fd:     89 5c 24 00                   mov    DWORD PTR [esp+0x0],ebx
0x2401:     8d 4d 08                      lea    ecx,[ebp+0x8]
0x2404:     89 4c 24 04                   mov    DWORD PTR [esp+0x4],ecx

I will post the patches and whole source package soon.

As usual, have fun!