前言:
某些安卓题强制要求arm环境才能跑,模拟器不管是脱壳还是动调都有很多问题,最近搞了个真机重新配一遍安卓逆向环境
这道题算非常经典的vm题了,用这道题来熟悉一下vm题基本流程

有三个函数我们点进去看一下具体逻辑

第一个函数很明显就是vm_init的结构,我们改名为vm_init,并创建结构体帮助静态分析

在空白部分右键,添加结构体

结构体名就叫vm_cpu,下面按d键快捷键给它增加成员变量
光标对准vm_cpu struc点一次d是在最前面增加成员变量
对准field_0点一次d是更改一次field_0的类型,也可按y直接输入类型
对准vm_cpu ends点一次d是在末尾增加成员变量

接下来修改成员变量的名称按n快捷键,改为eax,ebx等
由于vm_cpu中有个数组叫oplist[]
我们先创建新结构体
1 | struct opcode |
这里稍微再提一下怎么确定每个成员变量占多少字节,不要看ida给你强转出来的类型,而要看上下成员之间的差值,比如(_BYTE )(a1 + 24) = -15;和(_QWORD )(a1 + 32) = sub_B5F;从a1+24到a1+32,占了8个字节。所以这里_opcode类型是qword。下面是全部改完之后的结构体

再回到第一个函数,对准函数参数列表按y,把a1类型改为vm_cpu*,然后函数就变得美观了,下面的qword_2022A8是给vm设置的栈空间,我们也可以改名vm_stack

第二个函数很容易看出来是vm_run,分发器控制程序执行,第三个函数是验证flag正确与否,就不展开讲了
第二个函数把参数也改成vm_cpu*
1 | unsigned __int64 __fastcall vm_run(vm_cpu *a1) |
我们看到逻辑已经很明确了,就是rip指向的地址对应的值不等于0xf4时,一直调用sub_E6E
1 | unsigned __int64 __fastcall sub_E6E(vm_cpu *a1) |
sub_E6E点进去就发现和我们的vm基础里的dispatcher结构一模一样,现在我们回头处理一下没命名的handle函数就可以正式逆向了
1 | unsigned __int64 __fastcall sub_B5F(vm_cpu *a1) |
这个函数的意思是,v2是栈中偏移,相当于ss:[ebp+v2]=vm_stack[v2],当操作码=-15也就是0xF1的时候,调用这个函数,下一个地址的数(_BYTE )(a1->vm_rip+1))就是选择码,(_BYTE )(a1->vm_rip+2))就是操作数
选择码=0xE1 执行mov eax ss:[ebp+v2]
选择码=0xE2 执行mov ebx ss:[ebp+v2]
选择码=0xE3 执行mov ecx ss:[ebp+v2]
选择码=0xE4 执行mov ss:[ebp+v2] eax
选择码=0xE5 执行mov edx ss:[ebp+v2]
选择码=0xE7 执行mov ss:[ebp+v2] ebx
1 | unsigned __int64 __fastcall sub_A64(vm_cpu *a1) |
相当于xor eax ebx
1 | unsigned __int64 __fastcall sub_AC5(vm_cpu *a1) |
把flag读入栈上
相当于call read,并判断flag长度
1 | unsigned __int64 __fastcall sub_956(vm_cpu *a1) |
什么都没干,rip只是加1,这是所有指令都要有的
1 | unsigned __int64 __fastcall sub_A08(vm_cpu *a1) |
mul eax edx
1 | unsigned __int64 __fastcall sub_8F0(vm_cpu *a1) |
xchg eax ebx
1 | unsigned __int64 __fastcall sub_99C(vm_cpu *a1) |
实现了eax=ecx+2ebx+3eax应该是自定义指令
1 | #include<iostream> |
这里其实可以把栈信息一起打印出来的,但是这道题非常简单没有涉及到栈的其他操作,只是简单把flag放在栈上,所以这里不打印了
结果:
1 | read flag&&judge len |
正常一道普通的vm逆向题到这里看汇编逆向写exp就结束了,但这道题还有坑
这道题汇编前半部分很明显不对,因为flag总长度才21,栈上怎么索引到50多了,所以交叉引用找到真正的check函数,而且汇报中有两次输入,第二次输入才是真的
1 | unsigned __int64 sub_F00() |
exp:
1 | from z3 import * |
学过计算机组成原理的应该都知道,程序的运行是靠cpu解释可执行文件中操作码来实现的功能,vm逆向顾名思义就是自己定义了小型cpu并定义了指令集,将程序的代码转换自定义的操作码(opcode),然后在程序执行时再通过解释这些操作码,选择对应的函数执行,从而实现程序原有的功能。
虚拟机的入口函数,对虚拟机环境进行初始化,初始化一般包括
寄存器初始化(eax,ebx,ecx,edx,eip)
把handle函数和操作码连接在一起
给虚拟机的栈空间vm_stack分配内存
虚拟机开始运行的地方
调度器,解释opcode,并选择对应的handle函数执行,当handle执行完后会跳回这里,形成一个循环。
处理器,当rip走到对应操作码时调用对应操作函数,并接受操作数。
程序可执行代码转换成的操作码
在这种情况下,如果要逆向程序,就需要对整个emulator结构进行逆向,理解程序功能,还需要结合opcode进行分析,整个程序逆向工程将会十分繁琐。这是一个一般虚拟机结构:
有的虚拟机涉及lable的创造和调用,起到跳转目的
在比赛中,虚拟机题目常常有两种考法:
给可执行程序和opcode,逆向emulator,结合opcode文件,推出flag
只给可执行程序,逆向emulator,构造opcode,读取flag
拿到一个虚拟机之后,一般有以下几个逆向过程:
分析虚拟机入口,搞清虚拟机的输入,或者opcode位置
理清虚拟机结构,包括Dispatcher和各个Handler
逆向各个Handler,分析opcode的意义
根据opcode运行时打印出对应汇编代码,根据汇编代码逻辑进行逆向
1 | typedef struct |
1 | typedef struct |
1 |
|
1 | void mov(vm_cpu *cpu); |
1 | unsigned char vm_code[] = { |
一般是一个全局数组,用于存放虚拟机的栈
1 | vm_stack = malloc(0x512); |
1 | void vm_run(vm_cpu *cpu) |
1 | void vm_dispatcher(vm_cpu *cpu) |
先根据上面的特征静态分析判断函数是vm中的哪一部分,如果静态分析困难,那就动调一下
找到对应部分后创建上述结构体来帮助分析
自己把vm还原在vscode里,并在dispatcher部分加上打印这条指令的代码,有的时候也可以打印stack,和当前执行完指令的内存状态(各个变量的值)
根据打印出的汇编指令,手动逆向
具体使用方法还需在实战中不断练习,具体做题流程可看我的vm题单真题
拖了很久的rust逆向都一直没学,正好编译原理要做rust语法分析器,顺便把rust语言学一下
rust不通过GC(garbage collection)机制管理内存,例如python,golang等基于GC机制的编程语言会在exe运行时不断寻找虚拟地址中无用的内存空间.这会大大降低运行速度
rust使用所有权机制管理内存,这也使得它相比与手动开辟内存的c/c++更安全
我们rust在逆向中通常用于网络编程,游戏编程,wasm,嵌入式.所以写游戏外挂,实现检测外挂都必须要学习rust.
rust中变量声明要用let.rust中每个变量类型可以自己指定,也可交给编译器推断,每个变量类型可以声明可变也可声明不可变.(注:如果要声明常量类型时,常量名一定要全大写,并且必须显示指定类型.例如const MAX:u32=10;)
有符号:i8, i16, i32, i64, i128, isize
无符号:u8, u16, u32, u64, u128, usize
f32(32 位单精度)
f64(64 位双精度,默认)
true
false
&str → 字符串切片(不可变)
String → 堆分配的可变字符串
1 | fn main() { |
输出结果:
1 | The value of n is 5 |
1 | // if |
loop 一直循环
while 有条件的循环
for
可以通过break跳出循环,也可以通过continue继续当前循环.这和c++是一样的
1 | // loop |
match的用法和if-else很像,但是要注意match要把所有情况包含在内,不然编译阶段就报错
1 | let x = 5; |
基本的函数定义是fn fucnction(a:i32,b:i32) -> i32其中箭头右面的是返回值类型.rust函数表达力非常强
1 | fn add(a:i32,b:i32)->i32{ |
闭包可以理解成python里的lambda差不多,相当于匿名函数
1 | fn main() { |
简单来说,枚举(enum)就是用来表示“一个值可能属于几种互斥情况之一”,也就是“有限状态或选择”。
换句话说,它适合表示有多种可能性,但每次只能选一个的场景。
1 | enum TrafficLight { |
结构体和enum不一样的点在于声明结构体时,要把内部变量的类型写出来.而enum就不用
其中对#[derive(Debug)]的解释:
| 部分 | 含义 | 记忆小技巧 |
|---|---|---|
#[] |
Rust 的 属性(attribute)标记,用来告诉编译器对后面的结构体/枚举做某些处理 | “井号括号 → 给编译器的指令” |
derive |
自动 派生/生成实现 trait 的代码 | “derive = 自动生成某种功能” |
(Debug) |
指定生成的 trait 是 Debug |
“Debug = 调试打印能力” |
1 | #[derive(Debug)] |
Rust 很多地方受 JavaScript 影响,在实例化结构体的时候用 JSON 对象的 key: value 语法来实现定义:
实例化时:
结构体类名 {
字段名 : 字段值,
…
}
(1)在结构体内部用impl关键字实现内联函数:
1 | struct Node{ |
(2)用结构体中self指针实现
1 | struct Node{ |
(3)结构体实现构造函数
1 | struct Node{ |
(4)self的用法:
self 小写 = 当前对象实例指针。
Self 大写 = 当前类型名。
self指针也分为可变和不可变的,可变的要在self前加关键字mut
1 | #[derive(Debug)] |
输出:
1 | Person { |
self还有一个不经常用的用法:就是如果传入参数是self(不带&)的话:
1 | struct Node { |
1.基本定义:元组就是把多个不同类型的值组合在一起的复合类型。
语法:
1 | let tup: (i32, f64, char) = (500, 6.4, 'a'); |
2.访问元素
有两种方式:
方式一:解构
1 | let tup = (500, 6.4, 'a'); |
方式二:点语法(下标访问)
注意第一个元素下标是0
1 | let tup = (500, 6.4, 'a'); |
3.特点:可以包含不同类型的值
长度固定,不能改变
4.打印可利用{:?}来打印
后进先出 (LIFO) 的数据结构,内存分配和释放都非常快。
大小在编译时必须确定。
栈上的数据一般是 固定大小、生命周期明确的值。
1 | let x = 42; // i32,大小固定 4 字节 |
内存大小运行时才能确定。
需要手动申请(在 Rust 中由所有权系统管理,避免泄漏)。
分配和释放开销比栈大,但适合存放 动态大小或不确定大小的数据。
堆和栈上数据都有所有权的这个概念,但是栈上数据拷贝时不会move(转移所有权),而是使用copy(复制一个样本),堆会move
栈上的数据:如果它的类型实现了 Copy trait(比如 i32、bool、char、浮点数、简单元组),那么赋值时不会发生“严格意义上的 move”,而是直接 复制一份值。
实际上这不是“move”,而是 copy。
1 | fn main() { |
堆上的数据:比如 String、Vec,它们没实现 Copy,赋值时会发生 move。
1 | fn main() { |
引用 (Reference)
引用本质上就是 借用 (borrow)。
借用不会转移所有权,值的所有者依然是原来的变量。
分为:
不可变引用 (&T):可以有多个,但不能和可变引用同时存在。
可变引用 (&mut T):只能有一个,且不能和不可变引用共存。(可变引用要求被引用的变量是可变的)
1 | fn main() { |
1 | #[derive(Debug,Clone)] //必须结构体里的所有字段都是可拷贝的,才可像正常u32,i32那样使用 |
clone就相当于c++中的深拷贝,解决了两个指针指向同一块内存的问题,所以clone之后就可以正常赋值,并接着使用
生命周期用语法 'a 表示:
1 | fn example<'a>(s: &'a str) { |
当结构体里有引用时,必须标注生命周期:
1 | struct Person<'a> { |
生命周期的核心思想:引用永远不能比它指向的数据活得长。
编译器在编译期检查生命周期,保证安全。
'a 是标识符,用来关联多个引用的生命周期。
&str:是String类型的一个切片.长度确定放在栈上.
String一般长度不确定,放在堆上
1 | let s1 = String::new(); // 空字符串 |
1 | let mut s = String::from("Hello"); |
1 | let s1 = String::from("Hello"); |
1 | let s = String::from("hello"); |
1 | let mut s = String::from("Hello World"); |
1 | let s = String::from("hello"); |
1 | let s = String::from("hello world"); |
1 | let s = String::from("a,b,c"); |
1 | let s = String::from("hello"); |
1 | let mut v: Vec<i32> = Vec::new(); // 空 vector |
1 | let mut v = Vec::new(); |
1 | let v = vec![1, 2, 3, 4]; |
1 | let mut v = vec![10, 20, 30]; |
1 | let mut v = vec![1, 2, 3, 4]; |
1 | let v = vec![10, 20, 30]; |
相当于c++中stl里的map
1 | use std::collections::HashMap; |