问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
初探smc动态代码保护
CTF
SMC,即Self Modifying Code,动态代码加密技术,指通过修改代码或数据,阻止别人直接静态分析,然后在动态运行程序时对代码进行解密,达到程序正常运行的效果,而计算机病毒通常也会采用SMC技术动态修改内存中的可执行代码来达到变形或对代码加密的目的,从而躲过杀毒软件的查杀或者迷惑反病毒工作者对代码进行分析。
0x00 前言 ======= 缘起于**2021mrctf逆向的Dynamic Debug**。本菜鸡re复现路上的第一道smc保护的题目。 0x01 什么是smc? ============ 先来看官方注释 > SMC,即Self Modifying Code,动态代码加密技术,指通过修改代码或数据,阻止别人直接静态分析,然后在动态运行程序时对代码进行解密,达到程序正常运行的效果,而计算机病毒通常也会采用SMC技术动态修改内存中的可执行代码来达到变形或对代码加密的目的,从而躲过杀毒软件的查杀或者迷惑反病毒工作者对代码进行分析。 大白话:软件在运行前,先通过加密部分代码,达到绕过检测或阻止静态分析的目的,之后在运行程序时,再对代码进行解密,保证程序的正常运行。因此,遇到smc的题目的话,必须得动调了,静态分析再怎么修复也不可能是正确的。 0x02 前置知识:PE文件结构 ================ 在研究smc的实现之前,首先我们要了解前置知识:PE文件结构。 ![img](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-2b873c4d34778f67520a211cd0efb6f4a23ae01d.gif) 为了防止大家看不懂英文,给大家附上一张中文版本 ![img](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-dcdbf61fe42996bbc0d8620d9f0cf129fc89c1b1.southeast) 图片来源:<https://blog.csdn.net/adam001521/article/details/84658708> 作者:[adam001521](https://blog.csdn.net/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思路的伪代码 ```php IF .运行条件满足 CALL DecryptProc (Address of MyProc);对某个函数代码解密 ........ CALL MyProc ;调用这个函数 ........ CALL EncryptProc (Address of MyProc);再对代码进行加密,防止程序被Dump ``` **简单实现** 首先需要新增一个段,例如我们实现创建 qaq这么一个新段 ```php #pragma code\_seg(".qaq") void qaqq() { cout<<"WIN"; } void d() { ; } #pragma code\_seg() #pragma comment(linker,"/SECTION:.qaq,ERW") ``` 我们想加密一个新段,就得先找到他,即实现寻址,最简单的办法莫过于指针赋值: ```php 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"); } ``` 运行时报错,找到原因是由于 在进行异或加密处理之后,机器码发生了变化,这导致加密代码无法被识别而报错。 ![image-20220327131556123](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-7bc46dbb50f3ad5691b6064584646a7b426c1fdc.png) 所以需要我们手改一下机器码。 利用studyPE+,PEid,exeinfo等查看一下段的地址,然后利用 winhex之类的工具修改为加密之后的机器码就可以了。这样程序就可以正常运行了。 ![image-20220327132603812](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-cb713d49c82bc461143c8b7662d9f150fe84d6ee.png) ![image-20220327132819757](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-e937ac703171caa7426cb8fd1a50fc3aa12007ba.png) 0x04 在SMC中加入密码学算法 ================= 还记得吗,上文源码中采取了最简单的异或方式来进行加密,既然可以使用异或,当然也可以使用其他更复杂的加密方式。 使用更复杂的加密方式,其实和使用异或是一个道理。区别无非是要编写加解密函数,更换加密方式而已。仍然是smc加密三步走: 编写加解密函数-->计算加密前后机器码-->利用十六进制读取工具修改机器码。 详细过程可以通过上文体悟,也可以看下面这篇大佬实操文章。过程都是一样的,这里不再赘述。 <https://bbs.pediy.com/thread-92375.htm> 0x05 smc ctf实战 ============== \[2021羊城杯\] BabySmc ------------------- 相对其他smc题目来说,确实是挺baby的 奇怪的main函数 ![image-20220327141410186](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-558ce226dca7ee8e678667de72046633c3c31dec.png) 首先跟进一下main函数中的byte\_140001085。发现一大串数据。 ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-a6af5b807aef9a2afb11768768b5c947ab11959f.png) 这里我们直接按c是无法恢复数据为代码的,问题应该是出在上面调用的sub\_14001E30函数。 ```php 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); } ``` 在此处下个断点,开始动调。随便输入字符串,程序断在该位置 ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-5b90d8467c6d7507b4b54dc909afe798d29f7307.png) 继续f8单步 弹出关于rip的一个对话框,这是由于下面不是代码区域,ida会报错,点否。此时数据发生了改变 ![image-20220327152149374](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-bd92dc39fe97ef81a914b2b80cbeff2bc92a1278.png) 现在要恢复这些看似为数据的代码:从main函数头开始选定到第一个retn,右键->analyze selected area->force 强制转换。 ![image-20220327152445710](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-dc3082e25bb665ec2d940946f5a2119611519fab.png) 至此,smc成功被破解。代码被恢复,就可以直接反编译了 这一段是base64加密,不过加了个异或(四位一循环,分别异或A6,A9,A3,AC) ![image-20220327153112385](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-c234f05abff56f90fba42350476bfddbb8e3e283.png) flag字符串验证比较 ![image-20220327153733619](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-7b72ebeb58e3cbf25cd8605e424382096ca26d6a.png) 理一下解题思路: 换表base64+异或 逆向的步骤就是 异或+换表base64 异或部分处理灰常简单,直接上python脚本 ```php 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导出数据 ![image-20220327154046123](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-8bb3577cf8b83404cfff3b6cd014fc1caf40284a.png) 接着编写换表脚本。输出结果解密得到flag ```php 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) ``` ![image-20220327155838720](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-a31b11b6e6e4f41b3c46fe51915aa0e6ed4a55bf.png) \[2020网鼎杯\] joker ----------------- IDA打开程序就看到有保护,头疼。 ![image-20220327164323864](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-b3e3ef3149086948f5e29e0b4f0d62c959d452e7.png) 尝试f5,发现因为堆栈不平衡,无法直接反编译,所以修改一下 ![image-20220327165652492](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-6564c328d8d02abc229c3141e3464d1431d7526b.png) 勾选堆栈指针,快捷键alt+k,将SP修改为零,如果下面还遇到同理 ![image-20220327165737041](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-ad51645fb0ef54a5331626373a9bbde38dbb9044.png) 浅re一下 omg 和wrong 两个函数 ```php 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 后是否为真进行了异或下标的操作 ```php 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字符串比较函数) ```php 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保护,无法反汇编该函数 ![image-20220327170443402](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-60feaf14b2dcfb9007a7251ce17ec40005bead9a.png) ![image-20220327171926394](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-4963816d93a5152d61a95b819ea36a615112b26a.png) 回去观察一下main函数,通过上图标注的,涉及encrypt函数的循环 可以推测,出题者这里先给函数的代码段进行了加密,然后在运行的时候再用这层循环进行解密,相当于**加了一层壳。**还能怎么办呢,动态调试呗,定位函数调用,之前下个断点 把程序跑起来 利用od自带的中文搜索引擎定位到该函数 可以看到这里,00401805~0040182b非常符合for循环优化后的指令序列,\[ebp-0xC\]就是i,jocker.00401500就是ecrypt()的地址。因此我们下断点到循环结束的地方,然后F9, ![image-20220327172833689](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-30a36849019d0a6655dfacfa158a898949c7edb7.png) 在f7步入解密后的ecrypt函数内部 ![image-20220327173029546](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-88a5b523040250bf69c2610827b0b37448793717.png) 接下来使用olldump脱壳 ![image-20220327173233828](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-a220c5e4417c54434f9a68d71e3a8a7c4f438fa4.png) 脱壳后的程序用ida打开,encrypt函数已经被解密,被命名为start函数 ![image-20220327173352453](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-2a634ebb03ea45897aea3f047988ce843cfb4912.png) 浅逆一下,得到前19位flag还缺5位 ```php 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函数,用脱壳后的程序分析一下 ```php 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个字节。 完整脚本: ```php 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文件,话不多说,直接开始动调 **绕过长度检测** ![image.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-c64ee27e0b9951d909b908ee94aa6fc16127a3da.png) **smc加密伪代码修复** 按c强制转换代码 ![image-20220327175449233](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-19a2fc3851adb6915158df3e2061531b34597040.png) 在差不多的随意位置下断点。动调,输入32位字符绕过长度限制 ![image-20220327175513153](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-3c419f2e6dd929f2b9e89c30bcb6cdb5154de632.png) ![image-20220327175634122](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-b2f7c3d630d204517ae0d357a0d9ac1e8012a666.png) 开始位置按p创建函数,f5反编译,看到了主函数 ![image-20220327180036244](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-8c17e79f29921d79313e119c4ff513f18506737a.png) 跟进关键解密函数发现,得到的伪代码没有变量识别,非常难看。就在这卡了好久。看到了wjh大佬的blog。了解到这里可以尝试修复堆栈我们可以尝试着在这部分之上使用 Keypatch 手动加入一个 **push rbp; mov rbp, rsp**让 IDA 能够识别出堆栈上的变量,紧接着再 F5,就可以看到比较舒服的伪代码了。 ![image-20220327180519898](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-07e4fe4d04078be49ad064fb4bab2cfa86a1d561.png) **tea算法识别+破解** 一眼tea好吧。通过循环执行了 32 次,并且在循环内部对一个变量反复增加 delta 常数 (0x9E3779B9),循环内部出现了 TEA 运算逻辑等特征。 ![image-20220327180554303](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-8a26e3111aad037a6b13cce641c3ef5ecabd6d16.png) 最后标准tea解密解码即可 ```php #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://bbs.pediy.com/thread-271790.htm> [https://blog.csdn.net/qq\_32072825/article/details/121657090](https://blog.csdn.net/qq_32072825/article/details/121657090)
发表于 2022-03-29 09:41:37
阅读 ( 5747 )
分类:
其他
0 推荐
收藏
0 条评论
请先
登录
后评论
绿冰壶
学生
18 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!