After having found the source of the GDB anti-debug trick, I started modifying GDB to work around the problem and fix the number of sections on the fly (it’s simple to calculate the real number of sections). I was coding on a long train trip and everything was going great… My hack worked and GDB fixed and loaded the file without a problem. Next step was to run the program but when I tried I had this surprise:

gdb$ r

Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x00005040
0x8fe146d8 in __dyld__ZN16ImageLoaderMachO13parseLoadCmdsEv ()
EAX: 00005008  EBX: 8FE143C1  ECX: 44001048  EDX: 00000000  o d I t s z a p c
ESI: 00001054  EDI: 00000001  EBP: BFFFE0C8  ESP: BFFFE060  EIP: 8FE146D8
CS: 0017  DS: 001F  ES: 001F  FS: 0000  GS: 0037  SS: 001F
0x8fe146d8:     0f b6 50 38                   movzx  edx,BYTE PTR [eax+0x38]
0x8fe146dc:     80 fa 09                      cmp    dl,0x9
0x8fe146df:     75 e2                         jne    0x8fe146c3
0x8fe146e1:     8b 55 08                      mov    edx,DWORD PTR [ebp+0x8]
0x8fe146e4:     81 4a 74 00 80 00 00          or     DWORD PTR [edx+0x74],0x8000
0x8fe146eb:     eb e0                         jmp    0x8fe146cd
0x8fe146ed:     8b 55 08                      mov    edx,DWORD PTR [ebp+0x8]
0x8fe146f0:     81 4a 74 00 08 00 00          or     DWORD PTR [edx+0x74],0x800

Holy crap! I must have goofed somewhere in my code. This ocurred due to the fact that I had never tried to run the program while I was debugging the problem and modifying GDB so it was natural to think it was my problem (I didn’t even bothered to look to the function name that gave the error! duh 😉). Gave a quick review into the code and it seemed ok since it was a simple modification. Tried to run the program outside GDB and voila, the real problem shown itself.

dyld crash
dyld crash report

After some tests, the conclusion was that the problem ocurred for most values except 0xFFFFFFFF (else the trick wouldn’t work). I downloaded dyld source code but I couldn’t find where the crash was located. Train arrived to destination, weekend time and in the next week I had to finish some documentation since I’m leaving my current job and joining a MBA program. Today I was bored and decided to get back to the problem. After disassembling dyld, setting a breakpoint at ImageLoaderMachO::parseLoadCmds and tracing, I finally found the interesting piece of code:

8fe146a1  8b5630            movl        0x30(%esi),%edx <- code for case LC_SEGMENT_COMMAND
8fe146a4  8d4638            leal        0x38(%esi),%eax
8fe146a7  89d1              movl        %edx,%ecx
8fe146a9  c1e106            shll        $0x06,%ecx
8fe146ac  8d1491            leal        (%ecx,%edx,4),%edx
8fe146af  8d0c10            leal        (%eax,%edx),%ecx
8fe146b2  39c8              cmpl        %ecx,%eax <- for condition sect < sectionsEnd
8fe146b4  0f8336ffffff      jael        0x8fe145f0
8fe146ba  0fb65038          movzbl      0x38(%eax),%edx
8fe146be  80fa09            cmpb        $0x09,%dl <- if ( type == S_MOD_INIT_FUNC_POINTERS )
8fe146c1  741e              je          0x8fe146e1
8fe146c3  80fa0a            cmpb        $0x0a,%dl <- else if ( type == S_MOD_TERM_FUNC_POINTERS )
8fe146c6  7434              je          0x8fe146fc
8fe146c8  80fa0f            cmpb        $0x0f,%dl <- else if ( type == S_DTRACE_DOF )
8fe146cb  7457              je          0x8fe14724
8fe146cd  83c044            addl        $0x44,%eax <- ++sect
8fe146d0  39c1              cmpl        %eax,%ecx <- for condition sect < sectionsEnd
8fe146d2  0f8618ffffff      jbel        0x8fe145f0
8fe146d8  0fb65038          movzbl      0x38(%eax),%edx <- CRASH HERE

Where the original source code is:

 const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
 const bool isDataSeg = (strcmp(seg->segname, "__DATA") == 0);
 const struct macho_section* const sectionsStart=(struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
 const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
 for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
  const uint8_t type = sect->flags & SECTION_TYPE;
  if ( type == S_MOD_INIT_FUNC_POINTERS )
    fHasInitializers = true;
  else if ( type == S_MOD_TERM_FUNC_POINTERS )
    fHasTerminators = true;
  else if ( type == S_DTRACE_DOF )
    fHasDOFSections = true;
  else if ( isDataSeg && (strcmp(sect->sectname, "__image_notify") == 0) )
    fHasImageNotifySection = true;

ecx holds the value of sectionsEnd and the number of sections described in the header affects its value, because sectionsEnd = §ionsStart[seg->nsects]. dyld suffers from a similar problem to GDB, because it trusts the header information without any further check. One small thing was left to explain… Why 0xFFFFFFFF works? By setting a breakpoint at 0x8fe146b2 it’s very easy to see why! If nsects is 0x00FFFFFF then ecx=44001048, if nsects is 0x0FFFFFFF then ecx=40001048, if nsects is 0xFFFFFFFF then ecx=00001048. So 0xFFFFFFFF overflows sectionsEnd and that’s why the tricks works for that value!

Unless Apple fixes this dyld bug (maybe I should report it) there’s no much sense in fixing GDB except to produce a warning about a possible usage of that anti-debug trick. The bug itself is pretty easy to fix in GDB, dyld and other programs (HT Editor, IDA, etc) because the real number of sections can be calcuted using the cmdsize field from the load command. It’s not possible to play tricks with this field (I did a few tests and it doesn’t work) so it must be a reliable value.

I think I can finally close this bug hunt! It was fun. Only thing left is to release the patches I did for GDB. I must finish the fixes to other read commands and then I will publish them.

That’s all for now folks!