OLLVM 与去平坦化 & [RoarCTF2019] polyre 详细WP
## OLLVMllvm是一个底层虚拟机,OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,这个项目的目标是提供一个LLVM编译套件的开源分支,能够通过代码混淆和防篡改,增加对逆向工程的难度,提供更高的软件安全性。目前,OLLVM已经支持LLVM-4.0.1版本;所使用的编译器是clang。
llvm保护的大概方法是:程序主要使用一个主分发器来控制程序基本块的执行流程进而模糊每个模块之间的联系。
## 平坦化程序特征
```c
__int64 __fastcall check(__int64 a1)
{
size_t v1; // rax@13
signed int v2; // ecx@13
char v3; // ST04_1@16
signed int v4; // eax@18
signed int v5; // eax@21
signed int v6; // eax@24
signed int v7; // eax@27
signed int v9; // @1
int v10; // @1
signed int v11; // @0
srand(0x64u);
v10 = 0;
v9 = 706310565;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( v9 == -2109444161 )
{
v7 = 1322041670;
if ( *(_BYTE *)(a1 + 3) == 100 )
v7 = 867560817;
v9 = v7;
}
if ( v9 != -2069803162 )
break;
++v10;
v9 = 706310565;
}
if ( v9 != 306556692 )
break;
v4 = 1322041670;
if ( *(_BYTE *)a1 == 97 )
v4 = 564228819;
v9 = v4;
}
if ( v9 != 341947172 )
break;
v6 = 1322041670;
if ( *(_BYTE *)(a1 + 2) == 99 )
v6 = -2109444161;
v9 = v6;
}
if ( v9 != 564228819 )
break;
v5 = 1322041670;
if ( *(_BYTE *)(a1 + 1) == 98 )
v5 = 341947172;
v9 = v5;
}
if ( v9 != 682671478 )
break;
v3 = *(_BYTE *)(a1 + v10);
*(_BYTE *)(a1 + v10) = rand() % 5 + v3 - 97 + 97;
v9 = -2069803162;
}
if ( v9 != 706310565 )
break;
v1 = strlen((const char *)a1);
v2 = 306556692;
if ( v10 < v1 )
v2 = 682671478;
v9 = v2;
}
if ( v9 != 867560817 )
break;
v11 = 4;
v9 = 1000770411;
}
if ( v9 == 1000770411 )
break;
if ( v9 == 1322041670 )
{
v11 = 0;
v9 = 1000770411;
}
}
return (unsigned int)v11;
}
```
## 尝试进行函数的恢复
1. 函数的开始地址为序言的地址
2. 序言的后继为主分发器
3. 后继为主分发器的块为预处理器
4. 后继为预处理器的块为真实块
5. 无后继的块为retn块
6. 剩下的为无用块
## deflat 去平坦化脚本使用方法
### deflat.py
最早版本 https://github.com/SnowGirls/deflat
修改版 https://github.com/cq674350529/deflat
### 安装 angr 模块
脚本需要 angr 模块,官方建议使用创建虚拟环境后使用
进入虚拟环境后 pip install angr
### 示例 1
这是脚本自带的 sample`check_passwd_x8664_flat`
拖入 IDA 看看,发现是平坦化处理了
关键之处在于寻找程序的 address
注意到“函数的开始地址为序言的地址”,因此我们需要在序言中找函数起始位置
如上图,地址为 0x4009A0
运行脚本 (注意需要把下图两个文件从上层目录复制过来)
`python deflat.py -f path/to/binary --addr hexaddress`
```python
python deflat.py -f check_passwd_x8664_flat --addr 0x4009A0
```
运行结果
发现新生成的程序已经成功去平坦化
## 示例 2 —— polyre WP
### 去平坦化
IDA 打开程序
找序言的函数起始位置
运行程序
`python deflat.py -f polyre --addr 0x400620`
运行结果,成功去平坦化
### 分析代码
```c
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
signed __int64 v4; //
int i; //
int v6; //
int v7; //
char s1; //
char s; //
unsigned int v10; //
char *input; //
int v12; //
bool v13; //
unsigned __int8 v14; //
int v15; //
char *v16; //
int v17; //
int v18; //
bool v19; //
char *v20; //
int v21; //
bool v22; //
__int64 v23; //
bool v24; //
__int64 v25; //
__int64 v26; //
__int64 v27; //
__int64 v28; //
int v29; //
int v30; //
char *v31; //
int v32; //
int v33; //
bool v34; //
v10 = 0;
memset(s, 0, 0x30uLL);
memset(s1, 0, 0x30uLL);
printf("Input:", 0LL);
input = s;
if ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 ) // 这行代码出现很多次,但对程序本身并无实际意义,是去平坦化的遗留产物
goto LABEL_43;
while ( 1 )
{
__isoc99_scanf((__int64)"%s", (__int64)input); //
v6 = 0;
if ( dword_603058 < 10 || (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) == 0 )
break;
LABEL_43:
__isoc99_scanf((__int64)"%s", (__int64)input);
}
while ( 1 )
{
do
v12 = v6;
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 );
v13 = v12 < 64;
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 )
;
if ( !v13 )
break;
v14 = s;
do
v15 = v14;
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 );
if ( v15 == 10 )
{
v16 = &s;
*v16 = 0;
break;
}
v17 = v6 + 1;
do
v6 = v17;
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 );
}
for ( i = 0; ; ++i ) // 关键循环
{
do
v18 = i;
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 );
do
v19 = v18 < 6; // 外层循环 6 次
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 );
if ( !v19 )
break;
do
v20 = s;
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 );
v4 = *(_QWORD *)&v20; // 将输入转为 8 个 signed long long 类型
v7 = 0;
while ( 1 )
{
v21 = v7;
do
v22 = v21 < 64; // 内层循环 64 次
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 );
if ( !v22 )
break;
v23 = v4;
v24 = v4 < 0;
if ( v4 >= 0 ) // 若 v4 大于 0
{
v27 = v4;
do
v28 = 2 * v27; // v4 * 2
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 );
v4 = v28;
}
else // 若 v4 小于 0
{
v25 = 2 * v4; // v4 * 2
do
v26 = v25;
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 );
v4 = v26 ^ 0xB0004B7679FA26B3LL; // 之后异或
}
v29 = v7;
do
v7 = v29 + 1;
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 );
}
v30 = 8 * i;
v31 = &s1;
if ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 )
LABEL_55:
*(_QWORD *)v31 = v4;
*(_QWORD *)v31 = v4;
if ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 )
goto LABEL_55;
v32 = i + 1;
}
do
v33 = memcmp(s1, &unk_402170, 0x30uLL); // 最后按字节进行比较
while ( dword_603058 >= 10 && (((_BYTE)dword_603054 - 1) * (_BYTE)dword_603054 & 1) != 0 )
;
if ( v34 )
puts("Wrong!");
else
puts("Correct!");
return v10;
}
```
### 伪代码
加密算法大致是:输入 48,平分 6 组,将每组 8 字节转化为 long 类型的值,对每组进行加密,先判断正负,然后将值乘 2,随后根据正负异或 0xB0004B7679FA26B3,循环 64 次,最后进行比较
```c
i=0
#x=?
while(i<64):
if x>=0:
x=x<<1
else:
x=(x<<1)^0xB0004B7679FA26B3
i=i+1
```
### 逆向求解
这里我们需要注意,输入被转为 signed long long(有符号 int64)存为 v4
每次运算前需要判断 v4 的正负
**但是我们逆向写代码时是不能根据正负来判断运算分支的!!!**
原因就是我们无法得知 `x = (x << 1) ^ 0xB0004B7679FA26B3` 或者 `x << 1` 运算结束后,x 作为有符号数到底是正数负数
即,即使 a 为正/负数,经过上述运算之后也可能变成相反符号的 b;因此我们在求逆时,不能根据 b 为正数,就使用 `b << 1` 来运算,这就会出问题
那如何才能准确判断分支呢?
我们注意到,一个数乘 2 之后一定是偶数,一个偶数 `^ 0xB0004B7679FA26B3` 一定是奇数。因此我们求逆运算时必须根据奇偶性判断分支
即,若 x 为偶数,使用 `x >> 1` 求逆
若 x 为奇数,使用 `(x ^ 0xB0004B7679FA26B3) >> 1` 求逆
这就是本题最核心的地方
程序中的 v4 是一个有符号数,这就会导致一些正数溢出变成负数,即正数不会一直乘 2 下去,超过 long long 的取值范围 `0x7fffffffffffffff` 后就会溢出变成负数,通过下面代码也能明显看出正负交替(PS. 乘 2 操作是通过左移 1 位实现的)
另一处需要注意的就是:
加密运算时,负数 x 经过 `(x << 1 ) ^ 0xB0004B7679FA26B3` 变成了正数 y ;解密时,正数 y 经过 `(y ^ 0xB0004B7679FA26B3) >> 1` 一定还是正数,因此,当在负数分支时,解密脚本最后要和 0x800000000000000 进行或运算,这样只会修改第一位变成 1(有符号数第一位决定正负,正为 0 负为 1)
### 完整解密脚本
```python
origin = [0xbc8ff26d43536296,
0x520100780530ee16,
0x4dc0b5ea935f08ec,
0x342b90afd853f450,
0x8b250ebcaa2c3681,
0x55759f81a2c68ae4]
key = 0xB0004B7679FA26B3
data = ""
for value in origin:
for i in range(0, 64):
# 判断奇偶
parity = value & 1
if parity == 1:
value = (value ^ key) >> 1
value = value | 0x8000000000000000
else:
value = value >> 1
print(hex(value))
j = 0
while (j < 8):
# 因为是小端序,需要从最后一个字节开始取
data += chr(value & 0xFF)
# 右移 8 位,倒着取字节
value = value >> 8
j += 1
print(data)
```
附:各种类型的取值范围:
| 类型 | 字节 | 范围 |
| ------------------ | ------------ | :----------------------------------------------------------: |
| short int | 2byte(word)| 0~32767(0~0x7fff)-32768~-1(0x8000~0xffff) |
| unsigned short int | 2byte(word)| 0~65535(0~0xffff) |
| int | 4byte(dword) | 0~2147483647(0~0x7fffffff)-2147483648~-1(0x80000000~0xffffffff) |
| unsigned int | 4byte(dword) | 0~4294967295(0~0xffffffff) |
| long int | 8byte(qword) | 正: 0~0x7fffffffffffffff 负: 0x8000000000000000~0xffffffffffffffff |
| unsigned long int| 8byte(qword) | 0~0xffffffffffffffff |
### 运行结果
```python
python -u "polyre.py"
0x6666367b67616c66
0x63362d3039333932
0x2d363563342d3032
0x3539612d30376162
0x6631643365383537
0x7d38
flag{6ff29390-6c20-4c56-ba70-a95758e3d1f8}
```
#### 参考链接
利用符号执行去除控制流平坦化
https://paper.seebug.org/1059/#polyre 好好好好好好好好 我看成了坦克化 哇,遇到大神了,学习了,非常受用啊 你好根据你的提示我手动测试 py报错了 AttributeError: module 'keystone' has no attribute 'KS_ARCH_X86' 我安装了 keystone还是报错 forever_果酱 发表于 2022-4-3 17:05
你好根据你的提示我手动测试 py报错了 AttributeError: module 'keystone' has no attribute 'KS_A ...
建议参考 angr 官方文档
https://docs.angr.io/introductory-errata/install 挺好的,学习到了 挺好的,学习到了 这些样本不够典型,只加入了 平坦化 混淆,没有参与编译器优化,应该是 DEBUG 阶段的程序,脱离实际,无法应用 Tsihen 发表于 2022-6-14 21:01
这些样本不够典型,只加入了 平坦化 混淆,没有参与编译器优化,应该是 DEBUG 阶段的程序,脱离实际,无法 ...
CTF本身就是针对某一知识点的特化应用,和所谓实务确实不是一条线
页:
[1]
2