In this gist I show how to disassemble and modify a Linux executable binary to change the body of a function. This will allow you to control how a binary behaves, even when you don't have access to the source code and you can't recompile it.
In my case, I was asked to try and bypass the protection mechanism implemented. The protection mechanism implemented was meant to only allow a binary to be run in presence of a valid license.
So basically my activity involved:
- Finding the function which performs the protection check
- Disassembling the binary
- Patching the binary to change the function to always tell "Hey, the check is OK!"
Below I would like to briefly share these activities. Note that the one discussed is a real example of a real application. I redacted names and other elements that could reveal sensitive details. For the sake of example, I will call the application vulnbin
.
Disclaimer: Bypassing binary protection mechanisms may be an illegal activity. I was explicitly authorized. I am not encouraging you to perform anything that is against the law. Ask for permission.
The binary was an ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, not stripped
. As it was not stripped, all the symbols were inside it. Hence, i started crawling the binary using GNU strings
with the aim of identifying a function name which could somehow tell me it was performing the protection check. I checked for common words such as protection, check, security, security checks and after a few minutes I was able to identify a function which was very likely the one used for the check.
Now that I had a candidate function, I needed to disassemble it to understand, to some extent, what it was doing. To do the disassembly, I used gdb
- but I could also have used objdump
.
$ gdb --write -q vulnbin
Reading symbols from vulnbin...done.
(gdb) disassemble/r _<REDACTED>
Dump of assembler code for function <REDACTED>:
0x00000000004405fc <+0>: 55 push %rbp
0x00000000004405fd <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000440600 <+4>: 48 83 ec 10 sub $0x10,%rsp
0x0000000000440604 <+8>: 48 89 7d f8 mov %rdi,-0x8(%rbp)
0x0000000000440608 <+12>: 48 8b 45 f8 mov -0x8(%rbp),%rax
0x000000000044060c <+16>: 0f b6 40 07 movzbl 0x7(%rax),%eax
0x0000000000440610 <+20>: 84 c0 test %al,%al
0x0000000000440612 <+22>: 74 07 je 0x44061b <REDACTED+31>
0x0000000000440614 <+24>: b8 00 00 00 00 mov $0x0,%eax
0x0000000000440619 <+29>: eb 14 jmp 0x44062f <REDACTED+51>
0x000000000044061b <+31>: 48 8b 45 f8 mov -0x8(%rbp),%rax
0x000000000044061f <+35>: 48 89 c7 mov %rax,%rdi
0x0000000000440622 <+38>: e8 d3 fe ff ff callq 0x4404fa
0x0000000000440627 <+43>: 48 8b 45 f8 mov -0x8(%rbp),%rax
0x000000000044062b <+47>: 0f b6 40 01 movzbl 0x1(%rax),%eax
0x000000000044062f <+51>: c9 leaveq
0x0000000000440630 <+52>: c3 retq
End of assembler dump.
So the disassembled function consists of 0x0000000000440630 - 0x00000000004405fc + 1 = 52 + 1 = 53 bytes starting at offset 0x405fc
of vulnbin
.
By looking at instructions such as mov $0x0,%eax
, executed before the leaveq
, I guessed the function was just returning a boolean which was true ($0x1
) upon success or ($0x0
) upon failure. Indeed, the (boolean) result of the function is just placed in register %eax
right before the return.
The point was now one and only one: changing <REDACTED>
to behave like "Always return true!". I didn't care about all the other operations performed by the function. I'm not even that good at reading assembly. I just needed to replace all this stuff with a single return true
action.
How to do this? Well, I wrote a small c++ program with a dummy return true
function. The relevant part is this one:
class TestClass {
private:
public:
bool easy() {
return true;
}
};
A quick g++
compilation produced an a.out
that I could disassemble. This time I used objdump
- just to use another tool, but I could have disassembled also gdb
as done above. The disassembled bool easy()
is
0000000000400a9c <_ZN9TestClass4easyEv>:
400a9c: 55 push %rbp
400a9d: 48 89 e5 mov %rsp,%rbp
400aa0: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400aa4: b8 01 00 00 00 mov $0x1,%eax
400aa9: 5d pop %rbp
400aaa: c3 retq
400aab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Here the function is composed of 0x400aab - 0x400a9c + 1 = 15 + 4 = 19 bytes, starting at offset a9c
of a.out
. Let's dump these 19 bytes worth of opcodes out to a repl.bin
file. I used dd
for this. As dd
doesn't like hex values in skip
, I merely converted 0xa9c its decimal representation which is 2716:
$ dd if=a.out of=repl.bin bs=1 count=19 skip=2716
19+0 records in
19+0 records out
19 bytes copied, 9.3988e-05 s, 202 kB/s
I will use these dumped opcodes later to replace the initial opcodes of REDACTED
function in vulnbin
. But before doing the replacement, as REDACTED
opcodes are 53 bytes, I needed to pad repl.bin
with 53 - 19 = 34 NOP - no operation - that is, operations that are actually doing nothing. The NOP in the current architecture is 0x90
so I did a simple echo as follow:
$ for i in {1..34}; do echo -e -n "\x90" >> repl.bin; done
$ ls -l repl.bin
-rw-rw-r-- 1 simone simone 53 May 1 16:56 repl.bin
Here we are. Now we have a 53-bytes repl.bin
with opcodes which is ready to replace REDACTED
opcodes. To do the replacement is used dd
and wrote 53 bytes directly to vulnbin
, starting at offset 0x405fc = 263676:
$ dd if=repl.bin of=./vulnbin bs=1 count=53 seek=263676 conv=notrunc
53+0 records in
53+0 records out
53 bytes copied, 8.858e-05 s, 598 kB/s
Allright. Now the binary is patched. Let's see how it looks like before and after the patching
$ gdb --write -q vulnbin
Reading symbols from vulnbin...done.
(gdb) disassemble/r _<REDACTED>
Dump of assembler code for function <REDACTED>:
0x00000000004405fc <+0>: 55 push %rbp
0x00000000004405fd <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000440600 <+4>: 48 83 ec 10 sub $0x10,%rsp
0x0000000000440604 <+8>: 48 89 7d f8 mov %rdi,-0x8(%rbp)
0x0000000000440608 <+12>: 48 8b 45 f8 mov -0x8(%rbp),%rax
0x000000000044060c <+16>: 0f b6 40 07 movzbl 0x7(%rax),%eax
0x0000000000440610 <+20>: 84 c0 test %al,%al
0x0000000000440612 <+22>: 74 07 je 0x44061b <REDACTED+31>
0x0000000000440614 <+24>: b8 00 00 00 00 mov $0x0,%eax
0x0000000000440619 <+29>: eb 14 jmp 0x44062f <REDACTED+51>
0x000000000044061b <+31>: 48 8b 45 f8 mov -0x8(%rbp),%rax
0x000000000044061f <+35>: 48 89 c7 mov %rax,%rdi
0x0000000000440622 <+38>: e8 d3 fe ff ff callq 0x4404fa
0x0000000000440627 <+43>: 48 8b 45 f8 mov -0x8(%rbp),%rax
0x000000000044062b <+47>: 0f b6 40 01 movzbl 0x1(%rax),%eax
0x000000000044062f <+51>: c9 leaveq
0x0000000000440630 <+52>: c3 retq
End of assembler dump.
$ gdb --write -q vulnbin
Reading symbols from vulnbin...done.
(gdb) disassemble/r _<REDACTED>
Dump of assembler code for function <REDACTED>:
0x00000000004405fc <+0>: 55 push %rbp
0x00000000004405fd <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000440600 <+4>: 48 89 7d f8 mov %rdi,-0x8(%rbp)
0x0000000000440604 <+8>: b8 01 00 00 00 mov $0x1,%eax
0x0000000000440609 <+13>: 5d pop %rbp
0x000000000044060a <+14>: c3 retq
0x000000000044060b <+15>: 0f 1f 44 00 90 nopl -0x70(%rax,%rax,1)
0x0000000000440610 <+20>: 90 nop
0x0000000000440611 <+21>: 90 nop
0x0000000000440612 <+22>: 90 nop
0x0000000000440613 <+23>: 90 nop
0x0000000000440614 <+24>: 90 nop
0x0000000000440615 <+25>: 90 nop
0x0000000000440616 <+26>: 90 nop
0x0000000000440617 <+27>: 90 nop
0x0000000000440618 <+28>: 90 nop
0x0000000000440619 <+29>: 90 nop
0x000000000044061a <+30>: 90 nop
0x000000000044061b <+31>: 90 nop
0x000000000044061c <+32>: 90 nop
0x000000000044061d <+33>: 90 nop
0x000000000044061e <+34>: 90 nop
0x000000000044061f <+35>: 90 nop
0x0000000000440620 <+36>: 90 nop
0x0000000000440621 <+37>: 90 nop
0x0000000000440622 <+38>: 90 nop
0x0000000000440623 <+39>: 90 nop
0x0000000000440624 <+40>: 90 nop
0x0000000000440625 <+41>: 90 nop
0x0000000000440626 <+42>: 90 nop
0x0000000000440627 <+43>: 90 nop
0x0000000000440628 <+44>: 90 nop
0x0000000000440629 <+45>: 90 nop
0x000000000044062a <+46>: 90 nop
0x000000000044062b <+47>: 90 nop
0x000000000044062c <+48>: 90 nop
0x000000000044062d <+49>: 90 nop
0x000000000044062e <+50>: 90 nop
0x000000000044062f <+51>: 90 nop
0x0000000000440630 <+52>: 90 nop
As it can be seen, the patched function now contains the first 19 bytes of opcodes and then a series of padding nop
s.
Here we are. Binary is ready. At this point I just executed vulnbin
to see if the patching has worked as expected. Guess what? The binary was running beautifully without any license. Pwned.
Thank you so much for this very nice explanation