【WP】Rarctf2021-Pwn题
archer
输入的v1对应%p,直接输16进制
动调发现是add rax, 0x500000
需要让v1指向codegetshell0x404068-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 | from pwn import* |
Not That Simple
这题之前没见过
程序本身不难,开了沙盒
flag为同目录下文件的文件名,不是常规的orw了
需要找能输出目录下文件名的函数来写shellcode
mark同学找到了sys_getdents函数1
2int 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
14struct 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 | from pwn import* |
The Guessing Game
爆破canary和一字节返回地址+partial overwrite返回one gadget
Ch4rc0al同学写了快很多的二分法爆破并最终打通了
exp
1 | from pwn import * |
boring-flag-runner
题目涉及到最小的图灵完备语言:brainfuck
还没搞明白。。
Unintended
问题出现在patch函数中
当对堆块内容进行修改时,可输入字节的计算使用的是strlen,而在创建的时候malloc的大小和可输入的大小是相等的,出现的off by one的可能性。当输满的时候,strlen测量的长度会包括下一个堆块的size,造成单字节溢出。
存储的结构如下1
2
3
4
5
6
7
8
9size[0] des-size
size[1] idx
challenges+idx *chunk_ptr
chunk category(0x10)
chunk+16 name(0x10)
chunk+32 *des_ptr
chunk+40 point
打法
- 泄露基址。由于只能溢出一个字节,环境为2.27,无法一次放出unsorted bin。这里先尽量多申请chunk,然后利用修改size为接下来两个chunk的总和,造成
chunk overlap。接下来利用chunk的堆叠修改一个chunk的size为0x561,放进unsorted bin,再申请回来,就能拿到基址了(且地址最后一字节不变)。 - 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 | from pwn import* |
RaRmony
发现flag藏在channels中的secret-admin-chat中
直接读会显示没有权限
查看判断条件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if ( *(current_user + 4) <= *(a1 + 8) )
{
lseek(*(a1 + 4), 0LL, 0);
v3 = fdopen(*(a1 + 4), "r");
printf("\x1B[4m%s\n\x1B[0m", (a1 + 12));
( __isoc99_fscanf(v3, "%d:%[^\n]s", &v2, v4) > 0 )
{
print_user(users[v2]);
printf("\x1B[0;37m: %s\n", v4);
}
}
{
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 | from pwn import* |
因为复现的时候没有环境了,拿到本地的flag文件

Object Oriented Pwning
C++
给了源码
重复买牛卖牛可以得到更多的钱
在SetName方法中会发生堆溢出
覆盖掉下一个chunk中的type为flag
然后买个translator,进入对应对象的Translate方法中执行
1 | void Animal::Translate() { |
即可打印出flag
注意chunk开头是一个指向虚函数表的指针,即指向一个指针数组,数组中是该类的成员函数。由于每次操作后都会调用方法Age对年龄进行更新,所以覆盖的时候把虚指针原封不动写回就好。
exp
1 | from pwn import* |
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=0b10000000x*=2即x<<2
不难发现当输入\xff时能二次输入的字节数最多
+4处存着下一个chunk的指针
bss段上只会存0x20大小的chunk指针
且多次申请可以让指针放到下面的garbage中
delete_emoji
1 | v2 = find_free_slot(garbage, 400LL); |
delete操作会在garbage找到最靠前的空闲位置依次放进0x20和0xb0的指针,并置0,但不会free
read_emoji
1 | printf("Title: %s\nEmoji: ", *(entries[v2] + 4LL)); |
读出chunk中指针指向的内容 和 输入的一字节与二次输入的内容
collect_garbage
1 | for ( i = 0; ; ++i ) |
free所有位于garbage中的chunk
打法
- 泄露地址。照例需要先泄露基址,先拿个堆地址,二次输入接上指针就能读出来,方便后续修改堆指针指向。0xb0大小的chunk在填满tcache后变会被放到unsorted bin中,通过二次输入时修改chunk中的指针指向unsorted bin即可读出基址。
- 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 | from pwn import* |
The Mound
这题赛后复现学习的
题意总览
本地在0xDEAD0000000和0xBEEF0000000mmap了两块内存空间用来实现自己实现的简易堆管理,其中对空闲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大小的chunk1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
190xbeef0000000: 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
190xbeef0000000: 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的id1
2
3
40xdead0000000: 0x00000beef0000000 0x2acd85582b6411a8
0xdead0000010: 0x4d480b96179027ed 0x0000000000000000
0xdead0000020: 0x0000000000000000 0x0000000000000000
0xdead0000030: 0x0000000000000000 0x0000000000000000
0xdead0008008处存*vertify0xdead0008010处存top chunk地址1
2
3xdead0007ff8: 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表开始分配
另外有后门函数win1
2
3
4
5
6
7ssize_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 | openat(/home/ayoung/pwn/rarctf/mound/pwn/)(本地) |

然后再用sendfile读出flag(又学到一个新知识。其实这里用read,write也行。)1
2
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);1
2openat(/home/ayoung/pwn/rarctf/mound/pwn/071f74c56073a10c3109e8d10d51bda9.txt)
sendfile(1, 3, 0, 100)
其中in_fd为3,对应openat新打开的fd
exp
1 | from pwn import* |




