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:
case BFD_MACH_O_LC_SEGMENT:
if (bfd_mach_o_scan_read_segment_32 (abfd, command) != 0)
return -1;
break;
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);
else
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);
else
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 ?? ()
--------------------------------------------------------------------------[regs]
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
--------------------------------------------------------------------------[code]
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!
fG!