SMC

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);//传入参数为NULL时,表示获取当前程序的基址,也就是MZ
//初始化结构体指针
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、段结尾

1
#pragma code_seg()

通过上面两行代码可以生成一个段

3、设置段的属性

1
#pragma comment(linker, "/SECTION:.scode,ERW")

三个字母分别对应可执行、可读和可写

在段中插入函数

前面第一和第二行代码之间插入代码-函数,当然是可以直接插入函数的,这里是自己用汇编代码构造的函数

解释一下为什么前面先插了几个nop,首先我们要知道不同编译器使用生成段的代码得到结果是不同的

VC6生成段

VS2022生成段

可以看到VS2022中我们写的函数在生成段的中间,那么就需要定位,而VC6又不一样了,具体情况具体分析吧

所以在VS2022中,我们要先定位到插入的函数,而不能直接将整个段加密,这也是为什么插入nop的原因,nop可以用作特征码来识别,当然也可以替换或不用

定位到段的位置

先来介绍一个函数,GetModuleHandle可以得到进程的句柄,当传入的参数为NULL时,可以得到当前运行程序的基址,也就是ImageBase,这一块还需要多了解,就先简单介绍

得到基址之后我们就可以根据windows.h提供的结构体指针,得到PE头、DOS头、节表的信息

1
2
3
4
5
6
7
8
//通过获取线程句柄可以得到我们的基地址
LPVOID pModule = GetModuleHandle(NULL);//传入参数为NULL时,表示获取当前程序的基址,也就是MZ
//初始化结构体指针
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中,可以看到已经无法正常反编译

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+偏移,加密替换即可