Binary patching ... in Mac OS X
Time and time again, we came across binary-only executables doing things against our likings. Like asking for registration number to use it, or open it. With no viable replacement solution or no source code, it turns out that we are left with only one option and that to patch the binary to override some of it's functionality to make it work as per our likings.
In other words, most of the bad programs these days comes in binary-only version. With no source code available it gets very frustrating when we are forced to use programs, which are so obviously wrong, but which can not be fixed and rebuilt. But there is a very simple way available where you need to flip few bits of the program to solve the issue on hand.
Remember the last time you had to patch a binary program to make it work as per your liking? Don't worry, I'll show you how to patch a binary on Mac OS X.
Lets first write our own test program. Here it is...[remember, this is just a sample program ;-)]
#include
int verify(char* cUser, char* cPass)
{
if(cUser != NULL && cPass != NULL)
{
if(strcmp(cPass, "Test") == 0)
return 1;
else
return 0;
}
}
int main(int argc, char* argv[])
{
char cUser[8];
char cPass[8];
printf("User:");
scanf("%s", cUser);
printf("\nPass:");
scanf("%s", cPass);
if(verify(cUser, cPass))
{
printf("\nWelcome %s\n", cUser);
}
else
{
printf("\nTry again!\n");
}
return 0;
}
Lets build it using gcc.
nightrover:test administrator$ gcc -o test test.c
Run the test program...Lets see how does it behave when different passwords are supplied.
nightrover:test administrator$ ./test
User:Username
Pass:password <-- wrong password, should give "Try again!" message
Try again! <-- as expected!
nightrover:test administrator$
Now lets try correct password (since we have source code, we know the correct password is "Test"). As mentioned in the beginning, great deal of these binaries do not come with source codes.
nightrover:test administrator$ ./test
User:Username
Pass:Test <-- wrong password, should give "Try again" message
Welcome Username <-- as expected! correct user/pass authorized you!
nightrover:test administrator$
But now what if we don't have source of this binary and we don't have correct password to login? As mentioned in the beginning, a great deal of these binaries come without source code.
We need to scan the binary (test.o) through the microscope - in our case a disassembler program can do this for us. We are going to use the "gdb". We need to learn a bit about the executable "gcc" has made beforehand. "otool" can be used to find address and offset for our text segment from the binary.
nightrover:test administrator$ otool -l test
Load command 0
cmd LC_SEGMENT
[...]
Section
sectname __text
segname __TEXT
addr 0x00001e60
size 0x00000172
offset 3680
align 2^2 (4)
reloff 0
nreloc 0
flags 0x80000400
reserved1 0
reserved2 0
To find out the in memory address of the instruction where the condition is checked, we will have to run our binary inside "gdb". We choose to break at "verify" function - because it is the "verify" function which we are going to disassemble.
nightrover:test administrator$ gdb --arch i386 test
GNU gdb 6.3.50-20050815 (Apple version gdb-768) (Tue Oct 2 04:07:49 UTC 2007)
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"...Reading symbols for shared libraries ... done
(gdb) break verify
Breakpoint 1 at 0x1ec8
(gdb) break main
Breakpoint 2 at 0x1f3d
Note: setting a breakpoint in the verify() function will halt the program's execution and let us step through the code one line at a time. A breakpoint can be set either by specifying the name of the function or a line number.
(gdb) run
Note: now if we run the program, it will stop at the begining of verify() and wait for further instructions.
Starting program: /Users/administrator/test/test
Reading symbols for shared libraries ++. done
Breakpoint 2, 0x00001f3d in main ()
(gdb) bt
#0 0x00001f3d in main ()
(gdb) next
Single stepping until exit from function main,
which has no line number information.
User:Pallav
Pass:Text
Breakpoint 1, 0x00001ec8 in verify ()
(gdb) bt
#0 0x00001ec8 in verify ()
#1 0x00001f9e in main ()
(gdb) f 0
#0 0x00001ec8 in verify ()
(gdb) disass
Dump of assembler code for function verify:
0x00001ec2 : push %ebp
0x00001ec3 : mov %esp,%ebp
0x00001ec5 : push %edi
0x00001ec6 : push %esi
0x00001ec7 : push %ebx
0x00001ec8 : sub $0x2c,%esp
0x00001ecb : call 0x1ed0
0x00001ed0 : pop %ebx
0x00001ed1 : cmpl $0x0,0x8(%ebp)
0x00001ed5 : je 0x1f2c
0x00001ed7 : cmpl $0x0,0xc(%ebp)
0x00001edb : je 0x1f2c
0x00001edd : mov 0xc(%ebp),%eax
0x00001ee0 : lea 0x102(%ebx),%edx
0x00001ee6 : mov %eax,-0x24(%ebp)
0x00001ee9 : mov %edx,-0x28(%ebp)
0x00001eec : movl $0x5,-0x2c(%ebp)
0x00001ef3 : cld
0x00001ef4 : mov -0x24(%ebp),%esi
0x00001ef7 : mov -0x28(%ebp),%edi
0x00001efa : mov -0x2c(%ebp),%ecx
0x00001efd : repz cmpsb %es:(%edi),%ds:(%esi)
0x00001eff : mov $0x0,%eax
0x00001f04 : je 0x1f10
0x00001f06 : movzbl -0x1(%esi),%eax
0x00001f0a : movzbl -0x1(%edi),%ecx
0x00001f0e : sub %ecx,%eax
0x00001f10 : test %eax,%eax
0x00001f12 : jne 0x1f23
0x00001f14 : movl $0x1,-0x1c(%ebp)
0x00001f1b : mov -0x1c(%ebp),%eax
0x00001f1e : mov %eax,-0x20(%ebp)
0x00001f21 : jmp 0x1f2e
0x00001f23 : movl $0x0,-0x1c(%ebp)
0x00001f2a :jmp 0x1f1b
0x00001f2c :jmp 0x1f2e
0x00001f2e :mov -0x20(%ebp),%eax
0x00001f31 :add $0x2c,%esp
0x00001f34 :pop %ebx
0x00001f35 :pop %esi
0x00001f36 :pop %edi
0x00001f37 :leave
0x00001f38 :ret
End of assembler dump.
(gdb)
We need to figure out the offset for our jne conditional expression in the binary.
offset_in_file = address_in_memory - address_in_text_section + offset_in_text_section
offset_in_file = 0x00001F12 - 0x00001E60 + 3680
offset_in_file = 0x00000F12
We will use the "xxd" to get a hex dump of our binary.
nightrover:test administrator$ xxd ./test > test.hex
[...]
0000f00: 0000 0000 740a 0fb6 46ff 0fb6 4fff 29c8 ....t...F...O.).
0000f10: 85c0 750f c745 e401 0000 008b 45e4 8945 ..u..E......E..E
0000f20: e0eb 0bc7 45e4 0000 0000 ebef eb00 8b45 ....E..........E
[...]
If we look at the disassembled code, we know we have a very easy condition to bypass. There are so many different ways we could fix it: changing the return value of the verify function, forcing the zero flag to be clear at the conditional jump, overwriting the jump with NOPs, etc. The main challenge is to make it with the least amount of bytes changed. After considering different alternatives, I opted for a quite a simple method of flipping the conditional jump instruction. To do so, we need to change the instruction at offset 0x0000f12 from 75 (JNE) to 74 (JE). This single byte change to flip the conditional instruction lead us to...
[...]
0000f00: 0000 0000 740a 0fb6 46ff 0fb6 4fff 29c8 ....t...F...O.).
0000f10: 85c0 740f c745 e401 0000 008b 45e4 8945 ..u..E......E..E
0000f20: e0eb 0bc7 45e4 0000 0000 ebef eb00 8b45 ....E..........E
[...]
With the fix in hand, we need to recreate the binary from the altered xxd file. We will also have to set the executable bit set to let OS know that the file is executable.
nightrover:test administrator$ xxd -r test.hex > testpatched
nightrover:test administrator$ chmod +x testpatched
nightrover:test administrator$ ./testpatched
Now, lets run the patched binary...
nightrover:test administrator$ ./testpatched
User:nightrover
Pass:password <-- wrong password, should give "Try again" message
Welcome nightrover <-- even wrong password allowed you to login! Voila!
nightrover:test administrator$
It was very quick and painless solution. It may not be as good as having the real source of the application to be fixed, but in the real world it solved the problem. Our goal was to solve the problem, wasn't it?