Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simonemainardi/e63c03d6cefe65d0e2541a67e82955f9 to your computer and use it in GitHub Desktop.
Save simonemainardi/e63c03d6cefe65d0e2541a67e82955f9 to your computer and use it in GitHub Desktop.
Disassemble and Modify an Binary To Change a Function

Disassemble and Modify an Binary To Change a Function

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.

Finding the Function Which Performs the Protection Check

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.

Disassembling the Binary

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.

Patching the Binary

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.

@kolayne
Copy link

kolayne commented Aug 6, 2021

Thank you so much for this very nice explanation

@sugruedes
Copy link

Great explanation. Allows me to make the license check more complex! lol

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment