SMC,即Self Modifying Code,动态代码加密技术,指通过修改代码或数据,阻止别人直接静态分析,然后在动态运行程序时对代码进行解密,达到程序正常运行的效果,而计算机病毒通常也会采用SMC技术动态修改内存中的可执行代码来达到变形或对代码加密的目的,从而躲过杀毒软件的查杀或者迷惑反病毒工作者对代码进行分析。
0x00 前言
缘起于2021mrctf逆向的Dynamic Debug。本菜鸡re复现路上的第一道smc保护的题目。
0x01 什么是smc?
先来看官方注释
SMC,即Self Modifying Code,动态代码加密技术,指通过修改代码或数据,阻止别人直接静态分析,然后在动态运行程序时对代码进行解密,达到程序正常运行的效果,而计算机病毒通常也会采用SMC技术动态修改内存中的可执行代码来达到变形或对代码加密的目的,从而躲过杀毒软件的查杀或者迷惑反病毒工作者对代码进行分析。
大白话:软件在运行前,先通过加密部分代码,达到绕过检测或阻止静态分析的目的,之后在运行程序时,再对代码进行解密,保证程序的正常运行。因此,遇到smc的题目的话,必须得动调了,静态分析再怎么修复也不可能是正确的。
0x02 前置知识:PE文件结构
在研究smc的实现之前,首先我们要了解前置知识:PE文件结构。
为了防止大家看不懂英文,给大家附上一张中文版本
图片来源:https://blog.csdn.net/adam001521/article/details/84658708
作者:adam001521
下面浅浅介绍一下各部分用途:
MS-DOS header+DOS stub:早期为了DOS和Windows系统共存设计
PE文件标志:第一张图中没显示,应该是存在stub和PE header之间。 4个字节,也就是“PE/0/0”
PE Header+Optional Header(可选头部):存放PE文件的很多重要信息,比如文件包含的段(Sections)数、时间戳、装入基址和程序入口点等信息。
段头部+段实体
0x03 smc的 c实现
通常来说,SMC使用汇编去写会比较好,因为它涉及更改机器码,但SMC也可以直接通过C、C++来实现。先来看一下展示smc思路的伪代码
IF .运行条件满足
CALL DecryptProc (Address of MyProc);对某个函数代码解密
........
CALL MyProc ;调用这个函数
........
CALL EncryptProc (Address of MyProc);再对代码进行加密,防止程序被Dump
简单实现
首先需要新增一个段,例如我们实现创建 qaq这么一个新段
#pragma code\_seg(".qaq")
void qaqq()
{
cout<<"WIN";
}
void d()
{
;
}
#pragma code\_seg()
#pragma comment(linker,"/SECTION:.qaq,ERW")
我们想加密一个新段,就得先找到他,即实现寻址,最简单的办法莫过于指针赋值:
char \* b1\=(char\*)abc;
char \* c1\=(char\*)d;
int i\=0;
for(;b1<c1;b1++)
{
i++;
}
void\* a1\=(char\*)abc;
for(int i\=0;i<32;i++)
\*((BYTE\*)a1+i)^\=key;
这样大概我们就实现了一个简单的smc
#include<iostream>
#include<Windows.h>
using namespace std;
#pragma code\_seg(".aaa")
void qaqq()
{
cout << "you WIN";
}
void d()
{
;
}
#pragma code\_seg()
#pragma comment(linker, "/SECTION:.ddd,ERW")
int main()
{
int key;
cout << "input you key:" << endl;
cin \>> key;
char\* b1 \= (char\*)qaqq;
char\* c1 \= (char\*)d;
int i \= 0;
for (; b1 < c1; b1++)
{
i++;
}
void\* a1 \= (char\*)qaqq;
for (int i \= 0; i < 32; i++)
{
\*((BYTE\*)a1 + i) ^\= key;
}
qaqq();
system("PAUSE");
}
运行时报错,找到原因是由于 在进行异或加密处理之后,机器码发生了变化,这导致加密代码无法被识别而报错。
所以需要我们手改一下机器码。
利用studyPE+,PEid,exeinfo等查看一下段的地址,然后利用 winhex之类的工具修改为加密之后的机器码就可以了。这样程序就可以正常运行了。
0x04 在SMC中加入密码学算法
还记得吗,上文源码中采取了最简单的异或方式来进行加密,既然可以使用异或,当然也可以使用其他更复杂的加密方式。
使用更复杂的加密方式,其实和使用异或是一个道理。区别无非是要编写加解密函数,更换加密方式而已。仍然是smc加密三步走:
编写加解密函数-->计算加密前后机器码-->利用十六进制读取工具修改机器码。
详细过程可以通过上文体悟,也可以看下面这篇大佬实操文章。过程都是一样的,这里不再赘述。
https://bbs.pediy.com/thread-92375.htm
0x05 smc ctf实战
[2021羊城杯] BabySmc
相对其他smc题目来说,确实是挺baby的
奇怪的main函数
首先跟进一下main函数中的byte_140001085。发现一大串数据。
这里我们直接按c是无法恢复数据为代码的,问题应该是出在上面调用的sub_14001E30函数。
BOOL sub\_140001E30()
{
\_BYTE \*v0; // r9
\_\_int64 v1; // rdx
DWORD flOldProtect; // \[rsp+20h\] \[rbp-8h\] BYREF
VirtualProtect(lpAddress, qword_14002AD88 - (_QWORD)lpAddress, 0x40u, &flOldProtect);
v0 = lpAddress;
v1 = qword_14002AD88;
if ( (unsigned __int64)lpAddress < qword_14002AD88 )
{
do
{
*v0 = __ROR1__(*v0, 3) ^ 0x5A;//关键代码,循环3次右移+异或
++v0;
v1 = qword_14002AD88;
}
while ( (unsigned __int64)v0 < qword_14002AD88 );
v0 = lpAddress;
}
return VirtualProtect(v0, v1 - (_QWORD)v0, flOldProtect, &flOldProtect);
}
在此处下个断点,开始动调。随便输入字符串,程序断在该位置
继续f8单步 弹出关于rip的一个对话框,这是由于下面不是代码区域,ida会报错,点否。此时数据发生了改变
现在要恢复这些看似为数据的代码:从main函数头开始选定到第一个retn,右键->analyze selected area->force 强制转换。
至此,smc成功被破解。代码被恢复,就可以直接反编译了
这一段是base64加密,不过加了个异或(四位一循环,分别异或A6,A9,A3,AC)
flag字符串验证比较
理一下解题思路:
换表base64+异或
逆向的步骤就是 异或+换表base64
异或部分处理灰常简单,直接上python脚本
a\=list('H>oQn6aqLr{DH6odhdm0dMe\`MBo?lRglHtGPOdobDlknejmGI|ghDb<4')
x\=\[0XA6,0XA3,0XA9,0XAC\]
for i in range(len(a)):
a\[i\]=ord(a\[i\]) ^ x\[i%4\]
print(a)
下面处理base64加密部分,先找到密码表,shift+e导出数据
接着编写换表脚本。输出结果解密得到flag
a=list('H>oQn6aqLr{DH6odhdm0dMe\`MBo?lRglHtGPOdobDlknejmGI|ghDb<4')
x=\[0XA6,0XA3,0XA9,0XAC\]
for i in range(len(a)):
a\[i\]=ord(a\[i\]) ^ x\[i%4\]
print(a)
b=\[ 0xE4, 0xC4, 0xE7, 0xC7, 0xE6, 0xC6, 0xE1, 0xC1, 0xE0, 0xC0,
0xE3, 0xC3, 0xE2, 0xC2, 0xED, 0xCD, 0xEC, 0xCC, 0xEF, 0xCF,
0xEE, 0xCE, 0xE9, 0xC9, 0xE8, 0xC8, 0xEB, 0xCB, 0xEA, 0xCA,
0xF5, 0xD5, 0xF4, 0xD4, 0xF7, 0xD7, 0xF6, 0xD6, 0xF1, 0xD1,
0xF0, 0xD0, 0xF3, 0xD3, 0xF2, 0xD2, 0xFD, 0xDD, 0xFC, 0xDC,
0xFF, 0xDF, 0x95, 0x9C, 0x9D, 0x92, 0x93, 0x90, 0x91, 0x96,
0x97, 0x94, 0x8A, 0x8E\]
base64="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
def base(a):
for i in range(len(a)):
k=b.index(a\[i\])
print(base64\[k\],end='')
base(a)
[2020网鼎杯] joker
IDA打开程序就看到有保护,头疼。
尝试f5,发现因为堆栈不平衡,无法直接反编译,所以修改一下
勾选堆栈指针,快捷键alt+k,将SP修改为零,如果下面还遇到同理
浅re一下
omg 和wrong 两个函数
char \*\_\_cdecl wrong(char \*a1)
{
char \*result; // eax
int i; // \[esp+Ch\] \[ebp-4h\]
for ( i = 0; i <= 23; ++i )
{
result = &a1[i];
if ( (i & 1) != 0 )
a1[i] -= i;
else
a1[i] ^= i;
}
return result;
}
wrong函数首先对输入的flag的每个字节根据 与运算 1 后是否为真进行了异或下标的操作
int \_\_cdecl omg(char \*a1)
{
int result; // eax
int v2\[24\]; // \[esp+18h\] \[ebp-80h\]
int i; // \[esp+78h\] \[ebp-20h\]
int v4; // \[esp+7Ch\] \[ebp-1Ch\]
v4 = 1;
qmemcpy(v2, &unk\_4030C0, sizeof(v2));
for ( i = 0; i <= 23; ++i )
{
if ( a1\[i\] != v2\[i\] )
v4 = 0;
}
if ( v4 == 1 )
result = puts("hahahaha\_do\_you\_find\_me?");
else
result = puts("wrong ~~ But seems a little program");
return result;
}
omg:wrong得到的结果跟一个全局变量unk_4030C0比较(flag字符串比较函数)
result="fkcd\\x7fagd;Vka{&;Pc\_MZq\\x0c7f"
i=0
flag=""
for j in result:
if(i&1):
flag+=chr(ord(j)+i)
else:
flag+=chr(ord(j)^i)
i+=1
print flag
浅逆一下 出了一个 fake flag 出题人 he tui
这样的话 ,main函数中剩余的那个encrypt函数应该就是真正的加密函数了。然鹅由于加了smc保护,无法反汇编该函数
回去观察一下main函数,通过上图标注的,涉及encrypt函数的循环 可以推测,出题者这里先给函数的代码段进行了加密,然后在运行的时候再用这层循环进行解密,相当于**加了一层壳。**还能怎么办呢,动态调试呗,定位函数调用,之前下个断点 把程序跑起来
利用od自带的中文搜索引擎定位到该函数
可以看到这里,00401805~0040182b非常符合for循环优化后的指令序列,[ebp-0xC]就是i,jocker.00401500就是ecrypt()的地址。因此我们下断点到循环结束的地方,然后F9,
在f7步入解密后的ecrypt函数内部
接下来使用olldump脱壳
脱壳后的程序用ida打开,encrypt函数已经被解密,被命名为start函数
浅逆一下,得到前19位flag还缺5位
result2="\\x0e\\x0d\\x09\\x06\\x13\\x05\\x58\\x56\\x3e\\x06\\x0c\\x3c\\x1f\\x57\\x14\\x6b\\x57\\x59\\x0d"
flag=""
haha="hahahaha\_do\_you\_find\_me?"
for i in range(19):
flag+=chr(ord(haha\[i\])^ord(result2\[i\]))
print(flag)
还剩一个finally函数,用脱壳后的程序分析一下
int \_\_cdecl sub\_40159A(\_BYTE \*a1)
{
unsigned int v1; // eax
int result; // eax
char v3; // \[esp+13h\] \[ebp-15h\]
char v4; // \[esp+14h\] \[ebp-14h\]
char v5; // \[esp+15h\] \[ebp-13h\]
char v6; // \[esp+16h\] \[ebp-12h\]
char v7; // \[esp+17h\] \[ebp-11h\]
int v8; // \[esp+18h\] \[ebp-10h\]
int v9; // \[esp+1Ch\] \[ebp-Ch\]
v3 = '%';
v4 = 't';
v5 = 'p';
v6 = '&';
v7 = ':';
v1 = time(0);
srand(v1);
v9 = rand() % 100;
v8 = 0;
if ( (\*a1 != '%') == v9 )
result = puts("Really??? Did you find it?OMG!!!");
else
result = puts("I hide the last part, you will not succeed!!!");
return result;
}
最后五位为v3~v7与一个随机数异或的结果,然而这个随机数只是看似随机。因为flag最后一个字节一定是‘}’,那么用‘:’^‘}’=0x47计算出随机数,然后使用“%tp&:”分别异或0x47得到最后5个字节。
完整脚本:
result2="\\x0e\\x0d\\x09\\x06\\x13\\x05\\x58\\x56\\x3e\\x06\\x0c\\x3c\\x1f\\x57\\x14\\x6b\\x57\\x59\\x0d\\x47\\x47\\x47\\x47\\x47"
flag=""
haha="hahahaha\_do\_you\_fin%tp&:"
for i in range(24):
flag+=chr(ord(haha\[i\])^ord(result2\[i\]))
print(flag)
[MRCTF2021]Dynamic_debug
64位elf文件,话不多说,直接开始动调
绕过长度检测
smc加密伪代码修复
按c强制转换代码
在差不多的随意位置下断点。动调,输入32位字符绕过长度限制
开始位置按p创建函数,f5反编译,看到了主函数
跟进关键解密函数发现,得到的伪代码没有变量识别,非常难看。就在这卡了好久。看到了wjh大佬的blog。了解到这里可以尝试修复堆栈我们可以尝试着在这部分之上使用 Keypatch 手动加入一个 push rbp; mov rbp, rsp让 IDA 能够识别出堆栈上的变量,紧接着再 F5,就可以看到比较舒服的伪代码了。
tea算法识别+破解
一眼tea好吧。通过循环执行了 32 次,并且在循环内部对一个变量反复增加 delta 常数 (0x9E3779B9),循环内部出现了 TEA 运算逻辑等特征。
最后标准tea解密解码即可
#include <cstdio>
void encrypt(unsigned int\* v, const unsigned int\* k)
{
unsigned int v0 \= v\[0\], v1 \= v\[1\], sum \= 0, i;
unsigned int delta \= 0x9E3779B9;
unsigned int k0 \= k\[0\], k1 \= k\[1\], k2 \= k\[2\], k3 \= k\[3\];
for (i \= 0; i < 32; i++)
{
sum += delta;
v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 \>> 5) + k1);
v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 \>> 5) + k3);
}
v\[0\] \= v0;
v\[1\] \= v1;
}
void decrypt(unsigned int\* v, unsigned int\* k)
{
unsigned long v0 \= v\[0\], v1 \= v\[1\], sum \= 0xC6EF3720, i;
unsigned long delta \= 0x9e3779b9;
unsigned long k0 \= k\[0\], k1 \= k\[1\], k2 \= k\[2\], k3 \= k\[3\];
for (i \= 0; i < 32; i++)
{
v1 \-= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 \>> 5) + k3);
v0 \-= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 \>> 5) + k1);
sum \-= delta;
}
v\[0\] \= v0;
v\[1\] \= v1;
}
int main()
{
unsigned int v\[\] \= {
0x5585A199, 0x7E825D68, 0x944D0039, 0x71726943, 0x6A514306,c 0x4B14AD00, 0x64D20D3F, 0x9F37DB15, 0
};
unsigned int k\[4\] \= { 0x6B696C69, 0x79645F65, 0x696D616E, 0x67626463 };
for (int i \= 0; i < 8; i += 2) decrypt(v + i, k);
printf("%s", v);
return 0;
}
0x06 后记
浅浅总结一下学习SMC的感受趴
最简单的SMC保护效果是很弱的,因为在程序运行的某一时刻,它一定是解密完成的,这时也就暴露了,使用动态分析运行到这一时刻即可过掉保护;
复杂一点需要你找到修改代码段的算法,但是同样的,你只需要根据静态分析获得解密算法,就可直接写出解密脚本提前解密这段代码。所以SMC通常是配合反追踪技术或是嵌套的使用。
更深入的关于smc的知识,期待自己进一步的学习。
0x07 参考文章
https://www.52pojie.cn/thread-1184425-1-1.html
https://bbs.pediy.com/thread-263816-1.htm
https://bbs.pediy.com/thread-140865.htm
https://bbs.pediy.com/thread-92375.htm
https://blog.wjhwjhn.com/archives/220/
- 本文作者: 绿冰壶
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1431
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!