SMC
参考文章:https://www.anquanke.com/post/id/238645
https://blog.csdn.net/PandaOS/article/details/46575441?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1.pc_relevant_antiscanv2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1.pc_relevant_antiscanv2&utm_relevant_index=2
原理
SMC,是self-modifying-code的缩写 ,即自我修改的代码,通过程序运行后执行相关代码功能,对加密的代码数据进行解密,让其恢复正常功能
实现
1、要有两个函数,一个用于加密,一个用于解密,两个是对应的
2、找到要SMC的代码地址,然后提前在程序开始的地方设置对该地址数据的解密函数
3、取出要进行SMC的代码的字节码,对其使用准备好的加密函数进行加密
4、用这串加密的数据替换原代码的字节码
题目
https://buuoj.cn/challenges#[网鼎杯 2020 青龙组]jocker
解题过程
打开ida反编译看到
点进这个函数ida直接报错
查看汇编代码
点击函数跟进
发现一长串没被识别的数据段,这种情况可能是花指令导致,也可能是SMC,结合之前有一个循环异或,可以猜测是SMC,接下来有两种方法解密
使用IDC对加密数据进行解密
首先要找到被加密数据段的初始地址以及加密数据段的长度,这里长度在for循环里面,初始位置
点击函数跳转
黄色段就是初始地址,可以看到这里函数入口并不是pop开始,所以也可以猜到被加密了
idc脚本如下(shift+f2)打开
1 2 3 4 5 6 7 8 9 10 11 12
| #include <idc.idc>
static main() { auto addr = 0x401500; auto i = 0; for(i=0;i<187;i++) { PatchByte(addr+i,Byte(addr+i)^0x41); } }
|
得到了解密的数据段
动态调试
因为程序自带解密函数,只需要在解密函数后面下断点,运行到断点处,被加密的数据段就能成功解密
这是未解密的,运行后
这时候只需要先对函数按U,取消定义,然后选中按C——force转为汇编代码
然后按p定义成函数,再按F5就可以正常反编译了
SMC+反调试技术
因为smc可以采取动态调试来还原加密代码,所以可以采用反调试技术来阻止动态调试,所以这时候就需要绕过
狗狗的秘密
在数据段发现这里类型SMC,猜测是SMC自修改,所以需要定位到解密函数
在exports发现了一个函数
TLS回调函数会在oep也就是程序入口前执行
这个函数会在main函数之前执行,所以先进去看看
把鼠标放在上面发现他是个指针,被赋值为smc的函数段,v8同理,所以接下来这一段应该就是smc解密代码
很明显是tea,不会写idctea脚本,所以打算采取动调,但是前面有几个反调试,需要绕过,保证能走到解密代码这一步
对于if语句,只需要用jnz或jz替换即可
所以相对来说还行,主要是要先定位到这里
也可以去这些函数列表看一看
动调完成还是发现出错,所以里面的值可能被引用修改,对着变量按x查看交叉引用
发现在此之前被修改过,但是动调的时候显示是0,很奇怪,不知道什么时候赋值的,所以第一次进入 tea函数需要先对dword_915168的值在hex窗口进行修改
要注意在这个窗口中数据以小端序存储
修改好之后,下好断点后f9让程序跑起来就可以了
之后就是前面说的,先取消定义,然后转为代码,再定义函数
寻找怎么修改hex值的时候发现了 可以添加查看窗口,就很方便,在VM逆向里面可能会看起来更方便
SMC实现
下面所说的段就是节
先贴源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<Windows.h> #include<malloc.h>
#pragma code_seg(".scode") void _declspec(naked) Func() { __asm { nop nop nop nop } __asm { push ebp mov ebp, esp sub esp, 0C0h } printf("helloworld"); __asm { add esp, 0C0h pop ebp ret } } #pragma code_seg() #pragma comment(linker, "/SECTION:.scode,ERW")
void decode() { LPVOID pModule = GetModuleHandle(NULL); PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(pModule); PIMAGE_NT_HEADERS32 pNTheader = (PIMAGE_NT_HEADERS32)((DWORD)pModule + pDosHeader->e_lfanew); PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTheader + 4); PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER)); PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + sizeof(IMAGE_OPTIONAL_HEADER32)); for (int i = 0; i < pFileHeader->NumberOfSections; i++, pSectionHeader++) { if (!strcmp((const char*)pSectionHeader->Name, ".scode")) { PBYTE pSection = (PBYTE)((DWORD)pModule + pSectionHeader->VirtualAddress); for (DWORD k = 0; k < pSectionHeader->SizeOfRawData; ++k) { if (*(PDWORD)pSection == 0xC4C4C4C4) { char comp[] = {0xcc,0xcc,0xcc,0xcc,0xcc,0xcc}; printf("找到函数段"); for (;; ++k) { if (strncmp((const char*)(pSection), comp, 6)) { *(pSection) ^= 0x54; printf("%02X", *(pSection)); pSection++; } else { printf("不在函数段内,退出加密"); return; } } } pSection += 4; } printf("解密成功\n"); } } } int main() { decode(); Func(); return 0; }
|
这里只做了一个简单的异或加密
解释一下一些实现的步骤,如果要实现比较难的加密算法,需要先将原本数据DUMP下来,加密后再重新放入exe文件中,exe文件存放解密代码
插入段
1、生成段
生成名称为.socde的段,如果不填写则默认添加到.text段后面
1
| #pragma code_seg(".scode")
|
2、段结尾
通过上面两行代码可以生成一个段
3、设置段的属性
1
| #pragma comment(linker, "/SECTION:.scode,ERW")
|
三个字母分别对应可执行、可读和可写
在段中插入函数
前面第一和第二行代码之间插入代码-函数,当然是可以直接插入函数的,这里是自己用汇编代码构造的函数
解释一下为什么前面先插了几个nop,首先我们要知道不同编译器使用生成段的代码得到结果是不同的
可以看到VS2022中我们写的函数在生成段的中间,那么就需要定位,而VC6又不一样了,具体情况具体分析吧
所以在VS2022中,我们要先定位到插入的函数,而不能直接将整个段加密,这也是为什么插入nop的原因,nop可以用作特征码来识别,当然也可以替换或不用
定位到段的位置
先来介绍一个函数,GetModuleHandle可以得到进程的句柄,当传入的参数为NULL时,可以得到当前运行程序的基址,也就是ImageBase,这一块还需要多了解,就先简单介绍
得到基址之后我们就可以根据windows.h提供的结构体指针,得到PE头、DOS头、节表的信息
1 2 3 4 5 6 7 8
| LPVOID pModule = GetModuleHandle(NULL); PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)(pModule); PIMAGE_NT_HEADERS32 pNTheader = (PIMAGE_NT_HEADERS32)((DWORD)pModule + pDosHeader->e_lfanew); PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTheader + 4); PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + sizeof(IMAGE_FILE_HEADER)); PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + sizeof(IMAGE_OPTIONAL_HEADER32));
|
接着就是遍历,比较段的名字,这样就可以定位到我们插入的段
定位段位置
我们先前得到的句柄+该节的VirtualAddress就是节开始的地方
因为我们加密时是单字节加密,所以转为(PBYTE)型
根据特征代码定位
因为SMC会在程序运行中进行解密,所以我们要定位到函数位置
nop的机器码是0x90,异或加密后是0xC4,注意这里的小段序问题
判断结束位置
在这个例子中,结束代码之后都是0xCC,这样就可以使用字符串比较,然后判断是否到达末尾
1 2 3 4 5 6 7 8 9 10 11
| if (strncmp((const char*)(pSection), comp, 6)) { *(pSection) ^= 0x54; printf("%02X", *(pSection)); pSection++; } else { printf("不在函数段内,退出加密"); return; }
|
修改exe文件
这里我把加密后的数据打印了出来,方便贴到exe中
效果展示
运行exe发现
最后打印的就是段中的代码结果
ida中,可以看到已经无法正常反编译
另外实现
更多的可以看去文章头部的链接看看
我们可以手动定位到加密的位置,这样就比较麻烦,因为还要计算大小,不过代码倒是比较简单
1 2 3 4 5 6
| for (DWORD i = 0; i < 30; ++i) { *(unsigned char*)(0x00411780 + i) ^= 0x54; printf("%02X", *(unsigned char*)(0x00411780 + i)); } printf("解密成功\n");
|
理论上还有另外一种实现方法,直接读取文件中的信息,然后将foa转为rva,这样的话我们就可以找到我们smc的地址,这里可以新建段也可以直接在.text段尾部插入,但是要注意长度够不够,同样现插入特征码,使用strncmp进行比较,因为空的节表剩余是0,长度计算可以使用strlen,估计的。晚点再实现
foa转rva的话就是计算相对该段的pointerofrawdata的偏移,然后转为在内存中的偏移即可也就是imagebase+该段的virtualaddress+偏移,加密替换即可