wtfshell1 复现
注意:此挑战是开源的,不要浪费时间对二进制文件进行逆向工程
这个挑战有两个 flag:
- 1.第一个 flag 隐藏在虚拟文件系统(又名内存)内的“flag1”文件中,甚至无法被虚拟根目录读取,您的目标是 pwn 库并实现 RAA
- 2.第二个 flag 位于虚拟文件系统之外,这意味着您必须实现任意代码执行(实际上是 ORW,由于seccomp)才能获得 flag
1 | GNU C Library (Ubuntu GLIBC 2.36-0ubuntu3) stable release version 2.36.\n |
1 | wtfshell: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=14b74f98df26b1325153b74f6d77440e0b761024, for GNU/Linux 3.2.0, stripped |
- 64位,dynamically,全开
1 | ➜ share seccomp-tools dump ./wtfshell |
read
的 FD 必须为“0”
漏洞分析
1 | √ rtfm |
- 程序提供了一个菜单,提供了这些功能
比赛时根本找不到洞,只是发现有些不严谨的地方:
1 | int read_pw(char *dest) { |
- 这里的
read
最多只能读取 0x40 字节的数据 - 如果把这 0x40 字节的数据读满,就会导致字符串最后的 “\x00” 被覆盖,从而导致
ushadow[PWMAX]
和uname
连起来:
1 | struct user { |
- 由于程序不会输出
ushadow[PWMAX]
,我也就找不到泄露的办法了,直到比赛最后也没有进展了
侧信道攻击
关于侧信道攻击,我之前在复现 SCTF 的 Gadget 时用过一次,用于爆破存储于内存中的 “flag”
网上有位师傅采用了侧信道攻击的方法来泄露存放于 user->uname
的堆地址,我仔细思考了一下,发现本题目确实有侧信道攻击的条件:
1 | int chk_pw(const char *pw) { |
- 程序有一个功能是检查
user->ushadow[PWMAX]
是否和输出值匹配,这里read
读入的字符数并不是固定的 “0x40”,而是strlen(pw)
- 而之前我们已经把
user->ushadow[PWMAX]
末尾的 “\x00” 给去除掉了,因此这个read
将会读取超过 “0x40” 大小的数据
1 | if (!chk_pw(gusers[i]->ushadow)) { |
- 如果字符不匹配就会立刻返回,并且输出
asap: pw1 ≠ pw2\n
尝试使用侧信道攻击来泄露 user->uname
:
1 | def leakname(username,password): |
- 注意以下细节:
- 爆破时把“\n”排除掉(程序对“\n”有特殊操作)
- 当字符匹配成功时,需要再输入一个“\x00”来结束当前的
chk_pw
函数
Strtok Off-by-null
strtok
函数会不断往字符串后面遍历,直到遇到 “\x00” 截断,同时 strtok
会将查找到的隔断符 delim
设置为 “\x00”
1 | char *strtok(char s[], const char *delim); |
delim
可以有多个值,单独的这些值都可以完成分割(如果出现连续的多个delim
中所记录的值,则只把第一个值置为 “\x00”,然后跳过最后一个值返回指针)strtok
第一个参数为 NULL 时,继承的是上一次strtok
返回的指针
在源码中,可以发现 delim
的值如下:
1 | const char delim[] = ".,?!"; |
- 对应的 ascii 分别为:
0x2e 0x2c 0x3f 0x21
注意这个 0x21
,如果某个大 chunk 的低位也是 0x21
,那么这最后一字节就有可能被 strtok
给设置为 “\x00”,这里可以实现 off-by-null
在 init_sh
中对 gbuff
进行了初始化,申请的大小固定为“0x400”
1 | /* Allocate global buffer */ |
而我们能够写入的最大范围为“0x400”,根本写入不了 next chunk->presize
1 | read_max(gbuff, GBSIZE); |
在 cmd_irl
函数中提供了控制 gbuff
的代码:
1 | xfree(gbuff); |
于是我们可以提前在一个 chunk 中写好 next chunk->presize,先释放掉它,然后想办法让 xmalloc
重新申请到这个 chunk
- 本题目的
xfree
会自动把 target chunk 置空 - 需要利用
xrealloc
来释放 target chunk
注意:写入 next chunk->presize 时不能写入 “\x00”,取而代之的是 “/”,程序会执行以下函数来把 “/” 转换为 “\x00”:
1 | void remove_slash(char *fname) { |
- 使用
"rip."+"a"*(0x400-4)
就可以触发
测试代码如下:(这里只是演示效果,堆风水随时可以更改)
1 | presize = 0xdead |
现在要思考的问题就是:如何申请到这个 target chunk
如果释放的 gbuff
进入了 tcache bin,那它很快就可以被申请出来(tcache bin 采用 LIFO),所以在释放 gbuff
前必须先将 tcache bin 填满,并且把这个 target chunk 放入 tcache bin 头部
接下来释放的 gbuff
就会进入 unsortedbin,并且申请到 target chunk
1 | rip("3","d"*0x100) |
- 当
"rip."+"a"*(0x400-4)
执行时,会联通 next chunk->presize 一直定位到 next chunk->size 的低字节,然后把这个整体当做fname
接下来的思路就很明确了,就是搭建堆风水实现堆重叠,然后利用重叠来修改 file->fdata
使其指向“flag1”
在搭建堆风水的过程中,有以下几点需要注意:
- 由于程序的输入会被“\x00”截断,所以伪造 unlink attack 的时候需要从后往前写入数据
- 触发 unlink 时,除了常规检查以外,还会进行如下检查:
1 | if (p->fd_nextsize->bk_nextsize != p |
- 如果
p->fd_nextsize
写有数据,并且该 chunk 的大小大于 “0x400”,那么程序就会认为该 chunk 是 large chunk,并在 unlink 时触发这个检查(由于本程序的特殊写入机制,p->fd_nextsize
必须有数据)
在调试堆风水的过程中,发现“0x421”大小的 Chunk 难以利用,于是改为了“0x321”(和上面的测试样例有差别),最后费了九牛二虎之力终于搞好了
完整 exp 如下:
1 | from pwn import * |
小结:
堆风水搭得我吐血,不过还是学到了新的利用姿势