otx: Objective-C disassembly


Let’s say that you have a closed-source Objective-C program on your Mac that needs a minor modification. Maybe the company is delayed in sending you the license file that you purchased, or the company went out of business and there’s no way that the code will ever get fixed.

I’m not very sophisticated at this sort of thing, but I’ve twiddled a few bits here and there in the past. Usually simple stuff like replacing a set of instructions with no-ops or altering a constant in a move or arithmetic instruction. However, it was always in regular C programs, so the standard GNU binutils like nm and objdump, together with xxd, have been sufficient to get an idea of what was going on in the program. Unfortunately, these tools aren’t sufficient by themselves for an Objective-C program.

Here is an excerpt from the normal objdump -d from the binary that lives inside of GitX:

4018:       89 74 24 04             mov    %esi,0x4(%esp)
401c:       89 04 24                mov    %eax,(%esp)
401f:       e8 88 21 02 00          call   261ac <.objc_class_name_PBNSURLPathUserDefaultsTransfomer+0x3b0c>
4024:       83 c4 20                add    $0x20,%esp
4027:       31 c0                   xor    %eax,%eax
4029:       5b                      pop    %ebx
402a:       5e                      pop    %esi
402b:       c9                      leave
402c:       c3                      ret
402d:       55                      push   %ebp
402e:       89 e5                   mov    %esp,%ebp
4030:       83 ec 38                sub    $0x38,%esp
4033:       89 5d f4                mov    %ebx,0xfffffff4(%ebp)
4036:       89 75 f8                mov    %esi,0xfffffff8(%ebp)
4039:       8b 75 10                mov    0x10(%ebp),%esi
403c:       89 7d fc                mov    %edi,0xfffffffc(%ebp)
403f:       8b 3d 30 1b 02 00       mov    0x21b30,%edi
4045:       c7 44 24 10 00 00 00    movl   $0x0,0x10(%esp)

Here is the same range of bytes interpreted by otx (otx -e < /Applications/GitX.app/Contents/MacOS/GitX):

+100  00004018  89742404                movl        %esi,0x04(%esp)
+104  0000401c  890424                  movl        %eax,(%esp)
+107  0000401f  e888210200              calll       0x000261ac                    _objc_assign_strongCast
+112  00004024  83c420                  addl        $0x20,%esp
+115  00004027  31c0                    xorl        %eax,%eax
+117  00004029  5b                      popl        %ebx
+118  0000402a  5e                      popl        %esi
+119  0000402b  c9                      leave
+120  0000402c  c3                      ret

+(BOOL)[PBGitRepository isBareRepository:]
+0  0000402d  55                      pushl       %ebp
+1  0000402e  89e5                    movl        %esp,%ebp
+3  00004030  83ec38                  subl        $0x38,%esp
+6  00004033  895df4                  movl        %ebx,0xf4(%ebp)
+9  00004036  8975f8                  movl        %esi,0xf8(%ebp)
+12  00004039  8b7510                  movl        0x10(%ebp),%esi
+15  0000403c  897dfc                  movl        %edi,0xfc(%ebp)
+18  0000403f  8b3d301b0200            movl        0x00021b30,%edi               PBEasyPipe
+24  00004045  c744241000000000        movl        $0x00000000,0x10(%esp)

As you can see, it makes a big difference in terms of readability. otx decodes the function names and boundaries from the text and decodes the function calls whenever possible. The dynamic dispatch makes it tricky to read a raw disassembly otherwise.

So you just take the byte offsets of the instructions you want to replace and write the appropriate opcodes in place of them with your hex editor. You can write up a little bit of assembly and assemble it with as and then disassemble that if you don’t know the opcodes for the replacement instructions off the top of your head. On x86/x86_64, 0x90 is the opcode for nop, which is the most common instruction that I use as filler. The filler is pretty important, since you can’t easily change the byte offsets of any of the other instructions or else your jump locations and possibly your variable references will be wrong.