SMC 自解密代码


简介

自修改代码(SMC, Self Modifying Code)是在程序执行过程中修改其自身指令的技术。通常,这种做法旨在通过减少指令路径长度来提高性能,或者为了避免重复代码,从而简化维护。自修改代码一般是有意进行的,目的是优化或保护程序的运行,而不是由于错误导致的意外修改。

在逆向工程中,自修改代码通常表现为大量的无序代码,未被执行的部分无法直接分析。通过动态修改代码或数据,程序能够在运行时自我解密,防止静态分析工具的破解。这种技术常用于动态代码加密中,旨在通过修改代码来阻止逆向分析,直到程序执行到相应时刻才会恢复正常的可执行代码,从而达到反调试、反逆向的效果。

实现与破解

SMC 的实现方式有很多种,可以通过修改 PE 文件的 Section Header、使用 API Hook 实现代码加密和解密、使用 VMProtect 等第三方加密工具等。

在 CTF 中常见的形式是首先对.text段的代码进行加密,然后通过 SMC 进行自解密修改。

而一般程序的.text段是没有写权限的,在进行自修改之前,程序需要修改目标内存的权限。

内存权限修改

在不同的系统中定义有不同的 API 函数来修改内存权限。

  • 在 Linux 系统中,可以通过mprotect函数修改目标内存的权限。

mprotect是 Linux 系统的一个 API 函数,可以改写内存权限。这个函数的原型如下:

1
int mprotect(void *addr, size_t len, int prot);

第一个参数:开始地址(为了页对齐,该地址必须是一个内存页的起始地址,即页大小整数倍,1 页 = 4k = 0x1000)

第二个参数:指定长度(长度也应该是页的整倍数,即 0x1000 的整数倍)

第三个参数:指定属性(r=4(读)、w=2(写)、x=1(执行))

  • 在 Windows 系统中,调用VirtualProtect函数实现内存权限的修改。

VirtualProtect是 Windows 操作系统中的一个 API 函数,它允许应用程序改变一个内存页的保护属性。这个函数的原型如下:

1
2
3
4
5
6
BOOL VirtualProtect(
LPCVOID lpAddress, // 要改变保护属性的内存页的起始地址
SIZE_T dwSize, // 内存页的大小
DWORD flNewProtect, // 新的保护属性
PDWORD lpflOldProtect // 存储旧的保护属性
);

第一个参数:起始地址,同样也必须对齐,即页大小整数倍,1页 = 4k = 0x1000

第二个参数:指定长度(长度也应该是页的整倍数,即 0x1000 的整数倍)

第三个参数:

  • PAGE_READONLY:页面为只读。
  • PAGE_READWRITE:页面为可读写。
  • PAGE_WRITECOPY:页面为写时复制(COW)模式。
  • PAGE_EXECUTE:页面为可执行。
  • PAGE_EXECUTE_READ:页面为可执行和可读。
  • PAGE_EXECUTE_READWRITE:页面为可执行、可读和可写。
  • PAGE_NOACCESS:页面无访问权限(即不可读、不可写、不可执行)。

第四个参数:指向一个 DWORD 类型的变量,函数会把原先的保护标志存储在此变量中。如果不关心旧的保护属性,可以传入 NULL

因此我们也可以通过搜索程序是否使用了这两个函数来结合判断是否进行了 SMC。

实现

环境为 Linux 系统环境 gcc 编译器

下面我们通过实现简单的 SMC 代码来理解其原理。

我们编写一段 C 语言代码,在其中插入汇编代码,功能为输出字符串。

进行编译后,将func函数部分的字节码进行xor 0xAA加密,然后作为我们要解密的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

void func(){
__asm__ __volatile__(
"mov $1, %rdi\n\t"
"mov $0x0a434d53, %rsi\n\t"
"push %rsi\n\t"
"mov %rsp,%rsi\n\t"
"mov $4, %rdx\n\t"
"mov $1, %rax\n\t"
"syscall\n\t"
"mov $60, %rax\n\t"
"xor %rdi, %rdi\n\t"
"syscall\n\t"
);
}

void main(int argc, char *argv[])
{
func();
}

接下来我们编写 SMC 代码,将加密后的函数字节码作为数据插入到代码中。

加密算法为xor,异或值是0xAA,同样解密算法也是xor

首先调用内存权限修改函数给加密函数内存添加可写权限,然后再通过解密算法解密函数的代码数据。

这就实现了一段简单的 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
#include <stdint.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

//被加密的函数字节码
unsigned char code[] = {
0xff, 0xe2, 0x23, 0x4f, 0xe2, 0x6d, 0x6d, 0xab, 0xaa, 0xaa, 0xaa, 0xe2, 0x6d, 0x6c, 0xf9, 0xe7,
0xe9, 0xa0, 0xfc, 0xe2, 0x23, 0x4c, 0xe2, 0x6d, 0x68, 0xae, 0xaa, 0xaa, 0xaa, 0xe2, 0x6d, 0x6a,
0xab, 0xaa, 0xaa, 0xaa, 0xa5, 0xaf, 0xe2, 0x6d, 0x6a, 0x96, 0xaa, 0xaa, 0xaa, 0xe2, 0x9b, 0x55,
0xa5, 0xaf, 0x3a, 0xf7, 0x69
};

void main() {
size_t page_size = sysconf(_SC_PAGE_SIZE);
size_t code_size = sizeof(code);

mprotect((void *)((uintptr_t)code & ~(page_size - 1)), 0x1000, 7);

// 解密函数字节码
for (size_t i = 0; i < code_size; i++) {
code[i] ^= 0xAA;
}

// 将解密后的字节码转换为函数指针调用
void (*func)() = (void (*)())code;
func();
}

破解

针对这样的 SMC 我们有两种破解方法:

  • 动态解密法:

动态调试目标程序,待 SMC 代码运行解密后直接使用 IDA 分析。

ida 反编译编译后的文件分析代码逻辑。

发现这跟我们写的代码逻辑大差不差。

接下来我们通过动态解密废破解这个 SMC 程序。

我们在最后一行处打一个断点,然后动态调试让程序运行到这里。

到了这里code函数已经解密完毕,我们跟进查看。

在函数开始处按快捷键c将数据定义为代码,然后按快捷键p将代码定义为函数。

之后F5反编译。

可以看到反编译成功,这就是动态调试解密法。

我们也可以使用 Dump 功能将解密后的 SMC 字节数据 Dump 出来,对密文进行静态替换,再使用 IDA 进行分析。

不过这种方法对比上一种方法或者是下面的静态解密法来说比较麻烦,这里就不细讲了。

  • 静态解密法:

找到对代码或数据加密的逻辑后,根据代码逻辑编写 idapython 逆向解密脚本解密代码。

分析代码逻辑,程序对函数起始地址0x404060到相对偏移0x35处的代码进行异或解密,异或值为0xAA

  • idapython解密脚本
1
2
3
4
5
6
7
8
import idc

addr = 0x404060
for i in range(0x35):
val = idc.get_bytes(addr + i, 1)
val = ord(val)
val ^= 0xAA
idc.patch_byte(addr + i, val)

快捷键Shift+F2打开运行脚本代码窗口,选择 Python。

然后将代码复制进去运行即可。

之后就是定义代码定义函数,然后再进行反编译了。

同样,反编译成功。

总之这两种方法就是哪个方便用哪个。

例题

动态解密法

例题:2024 Newstar SMC

  • 分析

拿到附件,程序为 Linux 下的 ELF 文件,ida 反编译分析代码逻辑。

一眼丁真,发现程序调用了mprotect修改encrypt函数内存权限,判断可能存在 SMC。

然后程序通过scanf函数向变量s中输入字符串,长度要求必须为 28 字节。

之后的代码逻辑很明显就是通过循环异或对encrypt函数进行解密了。

这里我们对照着encrypt函数的情况来查看一下。

很明显encrypt函数的汇编代码都是有问题的,这就是进行过加密的函数。

接下来程序会通过上面的for循环逻辑对每字节进行异或解密。

然后就是通过encrypt函数判断输入是否正确了。

这里我们可以通过动态解密法来破解 SMC。

在比较输入处打一个断点,然后动态调试让程序运行起来,这样可以直接让程序执行完 SMC 代码。

运行完之后我们进入encrypt函数查看代码。

可以看到代码仍然是乱序的。

我们首先在encrypt函数处按快捷键u将其取消定义,然后再按快捷键c将其解释为代码,然后再按快捷键p将其解释为函数。

然后即可F5反编译,反编译后我们就可以看到encrypt函数的代码逻辑了。

可以看到是一段非常复杂的等式,我们直接通过 z3 来进行求解。

  • exp
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
from z3 import *
import struct

s=z3.Solver()

v2,v3,v4,v5,v6,v7,v8=Ints("v2 v3 v4 v5 v6 v7 v8")

s.add(5 * (v3 + v2) + 4 * v4 + 6 * v5 + v6 + 9 * v8 + 2 * v7 == 0xD5CC7D4FF)
s.add(4 * v8 + 3 * v5 + 6 * v4 + 10 * v3 + 9 * v2 + 9 * v7 + 3 * v6 == 0x102335844B)
s.add(9 * v6 + 4 * (v5 + v4) + 5 * v3 + 4 * v2 + 3 * v8 + 10 * v7 == 0xD55AEABB9)
s.add(9 * v3 + 5 * v2 + 9 * v8 + 2 * (v4 + 2 * v5 + 5 * v6 + v7) == 0xF89F6B7FA)
s.add(5 * v6 + 9 * v5 + 7 * v2 + 2 * v3 + v4 + 3 * v8 + 9 * v7 == 0xD5230B80B)
s.add(8 * v8 + 6 * v5 + 10 * v4 + 5 * v3 + 6 * v2 + 3 * v7 + 9 * v6 == 0x11E28ED873)
s.add(v2 + 4 * (v4 + v3 + 2 * v5) + 9 * v6 + v7 + 3 * v8 == 0xB353C03E1)

if s.check() == sat:
m=s.model()

v=[0]*7
v[0]=m[v2].as_long()
v[1]=m[v3].as_long()
v[2]=m[v4].as_long()
v[3]=m[v5].as_long()
v[4]=m[v6].as_long()
v[5]=m[v7].as_long()
v[6]=m[v8].as_long()

dec = b''.join(struct.pack('<I', val) for val in v)
result = dec.decode('ascii')
print(result)
#flag{D0_Y0u_Kn0w_sMC_4nD_Z3}

静态解密法

例题:[网鼎杯 2020 青龙组]jocker

  • 分析

ida 反编译分析,看到// positive sp value has been detected, the output may be wrong!

这是因为栈不平衡,ida 识别报错。

点击菜单的 Options-> General->Stack pointer 来打开 ida 的栈指针设置,分析栈不平衡的位置。

发现异常处,程序在调用了__Z7encryptPc函数和__Z7finallyPc函数后栈帧值不断减少。

按快捷键alt+k,将两处调用函数地方的汇编指令栈帧值修改为 0。

之后再进行反编译,可以看到 ida 没有再报错了。

接下来我们分析程序逻辑。

main函数调用了VirtualProtect函数修改encrypt函数的内存权限为可写。

然后调用scanf函数向Str变量输入字符串,如果输入长度不等于 24 则报错退出。

然后将输入的字符串复制到Destination变量中,将Str变量作为wrong函数和omg函数的参数传入处理。

最后的for循环就是 SMC 的解密逻辑。

接下来我们跟进查看wrong函数和omg函数的代码逻辑。

wrong函数是一个加密逻辑,对奇数进行偏移加密,对偶数进行异或加密。

接下来分析omg函数。

可以看到omg函数将输入和unk_4030c0数据进行了按位比较,判断unk_4030c0应该就是密文。

我们根据密文和代码逻辑,尝试写一下解密逻辑。

1
2
3
4
5
6
7
8
9
10
enc = [0x66, 0x6B, 0x63, 0x64, 0x7F, 0x61, 0x67, 0x64, 0x3B, 0x56, 0x6B, 0x61, 0x7B, 0x26, 0x3B, 0x50, 0x63, 0x5F, 0x4D, 0x5A, 0x71, 0x0C, 0x37, 0x66]
result=""

for i in range(len(enc)):
if i % 2 == 1:
result += chr(enc[i] + i)
else:
result += chr(enc[i] ^ i)
print(result)
# flag{fak3_alw35_sp_me!!}

试了一下,这个 flag 是假的。

接下来继续逆向 SMC 部分。

可以看到程序对encrypt函数进行了逐字节异或,解密encrypt函数。

我们这里就使用静态解密法来编写 idapython 代码解密encrypt函数。

  • idapython解密脚本

根据解密encrypt函数逻辑编写 idapython 解密脚本。

1
2
3
4
5
6
7
8
import idc
addr = 0x401500 # encrypt函数的地址
for i in range(187):
#获取地址字节,1代表一个字节
b = get_bytes(addr + i, 1)
#修改地址内存的字节
#第一个参数为指定地址,第二个参数为修改后的值
idc.patch_byte(addr + i, ord(b) ^ 0x41)

然后运行 idapython脚本,这里前面讲过了。

接下来我们修复解密后的encrypt函数代码。

从函数开始处开始按快捷键u将所有指令设置为无定义。

然后在函数开头处按快捷键c将所有数据内容转换为代码。

最后在函数开始处按快捷键p定义为函数。

然后就可以F5反编译了。

继续分析代码逻辑,encrypt函数的参数是存储输入内容的变量。

encrypt函数将输入与Buffer变量存储的内容进行逐字节异或,如果最后的结果不等于密文则报错。

我们跟进查看Buffer变量的内容,可以看到是wrong函数中输出的字符串。

我们根据代码逻辑,将密文与Buffer变量进行逐字节异或运算后即可得出符合的输入。

根据逻辑编写解密代码。

  • exp
1
2
3
4
5
6
7
key='hahahaha_do_you_find_me?'
enc=[0x0000000E, 0x0000000D, 0x00000009, 0x00000006, 0x00000013, 0x00000005, 0x00000058, 0x00000056, 0x0000003E, 0x00000006, 0x0000000C, 0x0000003C, 0x0000001F, 0x00000057, 0x00000014, 0x0000006B, 0x00000057, 0x00000059, 0x0000000D]
flag=""
for i in range(len(enc)):
flag+=chr(ord(key[i])^enc[i])
print(flag)
#flag{d07abccf8a410c

只解出了前半部分,继续分析程序。

可以看到,在encrypt函数调用成功之后继续调用了finally函数。

跟进分析finally函数代码逻辑。

程序中将%tp&:复制到了v3变量中,我们可以猜测这是后半部分的密文。

尝试用先前逻辑进行解密,结果不行。

我们知道 flag 的最后一位一定是},我们可以尝试将}与密文最后一位进行异或。

然后将异或结果与密文进行异或,尝试是否可以解密。

尝试成功,得到flag。

  • exp
1
2
3
4
5
6
7
8
9
10
11
12
13
key='hahahaha_do_you_find_me?'
enc=[0x0000000E, 0x0000000D, 0x00000009, 0x00000006, 0x00000013, 0x00000005, 0x00000058, 0x00000056, 0x0000003E, 0x00000006, 0x0000000C, 0x0000003C, 0x0000001F, 0x00000057, 0x00000014, 0x0000006B, 0x00000057, 0x00000059, 0x0000000D]
flag=""
for i in range(len(enc)):
flag+=chr(ord(key[i])^enc[i])
print(flag)

enc_end="%tp&:"
key=ord(enc_end[-1])^ord("}")
for i in range(len(enc_end)):
flag+=chr(ord(enc_end[i])^key)
print(flag)
#flag{d07abccf8a410cb37a}

后言

参考:smc加密研究 - V1ct0r的博客 (gdufs-king.github.io)
参考:SMC自解码总结-安全客 - 安全资讯平台 (anquanke.com)
参考:探究SMC局部代码加密技术以及在CTF中的运用 - SecPulse.COM | 安全脉搏