smc总结

smc总结

SMC(Software-Based Memory Encryption)是一种局部代码加密技术,它可以提高恶意代码的抗分析能力,但也存在一些局限性。作为一名CTF比赛的爱好者,虽然经常接触SMC加密技术,但是对其原理和实现并不深入了解,只是停留在解题的层面。最近,在为新生出题的过程中,产生了将SMC加密技术应用在出题上的想法,于是决定深入探究这门技术的原理、实现和应用。本文旨在分享作者学习SMC加密技术的心得,以及介绍如何利用SMC加密技术设计出有趣和有挑战性的CTF题目
本文的结构如下:第一部分介绍SMC加密技术的基本概念和原理;第二部分介绍SMC加密技术的实现方法和工具;第三部分介绍SMC加密技术在CTF出题中的应用和案例

SMC加密技术的基本概念和原理

SMC(Software-Based Memory Encryption)是一种局部代码加密技术,它可以将一个可执行文件的指定区段进行加密,使得黑客无法直接分析区段内的代码,从而增加恶意代码分析难度和降低恶意攻击成功的可能性。
SMC的基本原理是在编译可执行文件时,将需要加密的代码区段(例如函数、代码块等)单独编译成一个section(段),并将其标记为可读、可写、不可执行(readable, writable, non-executable),然后通过某种方式在程序运行时将这个section解密为可执行代码,并将其标记为可读、可执行、不可写(readable, executable, non-writable)。这样,攻击者就无法在内存中找到加密的代码,从而无法直接执行或修改加密的代码。
具体介绍见原文:https://zhuanlan.zhihu.com/p/624554464

SMC加密技术的实现方法

PS:为了方便,部分示例代码省去了对字节码加密和解密的操作,请注意辨别

1.数组保存字节码->调整内存执行权限->函数指针运行

先提取加密段代码的字节码
写入加密段代码,通过调试工具获取加密代码对应的字节码,例如:

1
2
3
4
5
byte addData[] = {0x55,0x89,0xe5,0x8b,0x55,0x8,0x8b,0x45,0xc,0x1,0xd0,0x5d,0xc3};
// int add(int a,int b)
// {
// return a+b;
// }

通过VirtualProtect函数调整内存为可执行的,定义函数指针,指向addData,调用函数指针

BOOL VirtualProtect(
[in] LPVOID lpAddress, // 要更改保护属性的页区域的起始地址
[in] SIZE_T dwSize, // 要更改保护属性的区域的大小(以字节为单位)
[in] DWORD flNewProtect,// 新的保护属性值
[out] PDWORD lpflOldProtect // 用于接收原来保护属性值的指针
);
第三个参数flNewProtect包括但不限于:
PAGE_EXECUTE 可执行
PAGE_EXECUTE_READ 可读可执行
PAGE_EXECUTE_READWRITE 可读可写可执行
最简单的一种实现方式,以add函数为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "mytool.h"
typedef int (*FnAdd)(int,int);
byte addData[] = {0x55,0x89,0xe5,0x8b,0x55,0x8,0x8b,0x45,0xc,0x1,0xd0,0x5d,0xc3};
// int add(int a,int b)
// {
// return a+b;
// }
int main()
{
DWORD fOldProtect = 0;
if(!VirtualProtect(addData,sizeof(addData),PAGE_EXECUTE,&fOldProtect))
{
perror("VirtualProtect failed");
system("pause");
}
FnAdd add = (FnAdd)addData;
printf("%d",add(3,4));//7
system("pause");
return 0;
}

运行成功,在IDA中查看效果
Alt text
Alt text
(未加密的)函数如期的到了data段
通过这种方式即可实现大部分的加密算法,例如tea,base64,rc4等
但是对于一些复杂的逻辑加密,可能需要引入外界的库函数,比如说CTF中简单的输入输出提示,需要用到printf,scanf等库函数
举例如下,当add中嵌套了printf时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "mytool.h"
typedef int (*FnAdd)(int,int);
byte addData[] = {0x55,0x89,0xe5,0x83,0xec,0x28,0x8b,0x55,0x8,0x8b,0x45,0xc,0x1,0xd0,0x89,0x45,0xf4,0x8b,0x45,0xf4,0x89,0x44,0x24,0x4,0xc7,0x4,0x24,0x6a,0x50,0x40,0x0,0xe8,0x21,0x26,0x0,0x0,0x8b,0x45,0xf4,0xc9,0xc3};
// int add(int a,int b)
// {
// int c = a+b;
// printf("%d",c);
// return c;
// }
int main()
{
DWORD fOldProtect = 0;
if(!VirtualProtect(addData,sizeof(addData),PAGE_EXECUTE,&fOldProtect))
{
perror("VirtualProtect failed");
system("pause");
}
FnAdd add = (FnAdd)addData;
add(3,4);
system("pause");
return 0;
}

正常生成但是运行失败,汇编窗口动调发现在call指令时发生了报错
分析可知,call指令后跟的是printf的相对地址,在转换到data段之后相对地址发生改变,访问到了非printf地址
第一种解决方法:可以定义函数指针,在加密函数内通过函数指针来利用库函数
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "mytool.h"
typedef int (*PRINTF) (const char *__format, ...);
typedef int (*FnAdd)(int,int,PRINTF);
byte addData[] = {0x55,0x89,0xe5,0x83,0xec,0x28,0x8b,0x55,0x8,0x8b,0x45,0xc,0x1,0xd0,0x89,0x45,0xf4,0x66,0xc7,0x45,0xf1,0x25,0x64,0xc6,0x45,0xf3,0x0,0x8b,0x45,0xf4,0x89,0x44,0x24,0x4,0x8d,0x45,0xf1,0x89,0x4,0x24,0x8b,0x45,0x10,0xff,0xd0,0x8b,0x45,0xf4,0xc9,0xc3};
// int add(int a,int b,PRINTF print)
// {
// int c = a+b;
// char format[] = "%d";//原.rdata段数据,尽量使用栈来保存函数参数
// print(format,c);
// return c;
// }
int main()
{
DWORD fOldProtect = 0;
if(!VirtualProtect(addData,sizeof(addData),PAGE_EXECUTE,&fOldProtect))
{
perror("VirtualProtect failed");
system("pause");
}
FnAdd add = (FnAdd)addData;
add(3,4,printf);
system("pause");
return 0;
}

IDA中伪代码:
Alt text
从这个例子可以看出,这种传参方式很容易暴露自己的意图,此地无银三百两的感觉,并且会出现调用多个库函数时代码可读性差的问题
为了优化隐蔽性和可读性,我们可以利用kernel32.dll模块来获取LoadLibraryA和GetProcAddress函数的地址,进而动态加载我们需要的模块和函数。
具体的实现步骤在Get_Func函数中有详细说明。
下面是修改后的程序,可正常运行。

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//mytool.h
typedef HINSTANCE (*LOADLIBRARY)(LPCSTR);//LoadLibraryA的地址
typedef FARPROC (*GETPROCADDRESS)(HINSTANCE, LPCSTR);//GetProcAddress的地址
int Get_Func(int fnNum){//通过传参来返回GetProcAddress函数地址和LoadLibraryA函数地址
asm(
"mov %esp, %ebp\n"
"push $0\n"
"push $0x7373\n"
"push $0x65726464\n"
"push $0x41636f72\n"
"push $0x50746547\n" // GetProcAddress
"push $0\n"
"push $0x41797261\n"
"push $0x7262694c\n"
"push $0x64616f4c\n" // LoadLibraryA
"mov %esp, %ecx\n"
"push %ecx\n" // esp传参过去
"cmp $0,0x8(%ebp)\n"//传参是0就得到LoadLibraryA的地址
"jnz label2\n"
"call Get_Load\n"
"jmp label1\n"
"label2:"
"call Get_Proc\n"//传参是1就得到GetProcAddress的地址
"label1:"
"mov %ebp, %esp\n"
"pop %ebp\n"
"ret\n"

"Func_GetModule:"
"push %ebp\n"
"mov %esp, %ebp\n"
"sub $0x20, %esp\n"
"push %esi\n"
"mov %fs:0x30, %esi\n" // PEB结构体地址
"mov 0xc(%esi), %esi\n" // LDR结构体地址
"mov 0x1c(%esi), %esi\n" // list
"mov (%esi), %esi\n" // list 第二项kernel32或者kernelbase
"mov 0x8(%esi), %esi\n" // dllbase
"mov %esi, %eax\n"
"pop %esi\n"
"mov %ebp, %esp\n"
"pop %ebp\n"
"ret\n"

"Func_GetAddr:"
"push %ebp\n"
"mov %esp, %ebp\n"
"sub $0x20, %esp\n"
"mov 0x8(%ebp), %edx\n" // dllbase
"mov 0x3c(%edx), %esi\n" // if_anew
"lea (%edx, %esi), %esi\n" // NT header
"mov 0x78(%esi), %esi\n" // 导出表RVA
"lea (%edx, %esi), %esi\n" // 导出表VA
"mov 0x1c(%esi), %edi\n" // EAT RVA
"lea (%edx, %edi), %edi\n" // EAT VA
"mov %edi, -0x4(%ebp)\n" // 保存
"mov 0x20(%esi), %edi\n" // ENT RVA
"lea (%edx, %edi), %edi\n" // ENT VA
"mov %edi, -0x8(%ebp)\n" // 保存
"mov 0x24(%esi), %edi\n" // EOT RVA
"lea (%edx, %edi), %edi\n" // EOT VA
"mov %edi, -0xc(%ebp)\n" // 保存
"xor %eax, %eax\n"
"jmp tag_cmpfirst\n"
"tag_cmpLoop:"
"inc %eax\n"
"tag_cmpfirst:"
"mov -0x8(%ebp), %esi\n" // ENT
"mov (%esi, %eax, 4), %esi\n" // RVA
"lea (%edx, %esi), %esi\n" // 函数名称字符串地址
"mov 0xc(%ebp), %edi\n"
"mov 0x10(%ebp), %ecx\n" // 循环次数,ebp+0x10是传来的参数
"repe cmpsb\n"
"mov -0xc(%ebp), %esi\n"
"jne tag_cmpLoop\n"
"mov -0xc(%ebp), %esi\n" // EOT
"xor %edi, %edi\n" // 为了不影响结果清空edi
"mov (%esi, %eax, 2), %di\n" // word类型,EAT表索引
"mov -0x4(%ebp), %edx\n" // EAT
"mov (%edx, %edi, 4), %esi\n" // 函数地址RVA
"mov 0x8(%ebp), %edx\n" // dllbase
"lea (%edx, %esi), %eax\n" // 最终函数地址
"mov %ebp, %esp\n"
"pop %ebp\n"
"ret $0xc\n" // 在栈中弹出三个参数

"Get_Load:"
"push %ebp\n"
"mov %esp, %ebp\n"
"sub $0x10, %esp\n"
"call Func_GetModule\n" // 获取kernel模块基址
"mov %eax, -0x4(%ebp)\n" // 保存模块基址
"lea 0xc(%ebp), %ecx\n" // load地址
"push $0xc\n"
"push %ecx\n"
"push %eax\n"
"call Func_GetAddr\n" // 三个参数,第一个模块基址,第二个字符串地址,第三个字符串长度 loadlibrarya
"mov %ebp, %esp\n"
"pop %ebp\n"
"ret\n"
"Get_Proc:"
"push %ebp\n"
"mov %esp, %ebp\n"
"sub $0x10, %esp\n"
"call Func_GetModule\n" // 获取kernel模块基址
"mov %eax, -0x4(%ebp)\n" // 保存模块基址
"push $0xe\n"
"lea 0x1c(%ebp), %ecx\n" // Get字符串
"push %ecx\n"
"push -0x4(%ebp)\n" // 模块基址
"call Func_GetAddr\n" // 三个参数,第一个模块基址,第二个字符串地址,第三个字符串长度 GetProcAddress
"mov %ebp, %esp\n"
"pop %ebp\n"
"ret\n"
);
}
1
 	

这里就是通过Get_Func来获取GetProcAddress函数地址和LoadLibraryA函数地址
从而在加密函数内可以调用这两个函数来获取其他库函数地址
Get_Func函数具体实现方式大致是(大佬略过~~):
通过FS段寄存器获取TEB的地址,然后通过TEB的ProcessEnvironmentBlock成员(偏移量为0x30)获取PEB的地址,再通过PEB结构体获取其成员Ldr(加载模块列表)(偏移量为0xc),通过Ldr结构体访问InInitializationOrderModuleList成员(导入模块列表)(偏移量0x1c),InInitializationOrderModuleList的第二个成员即为kernel32.dll模块,我们想要的GetProcAddress函数地址和LoadLibraryA函数地址都在kernel32.dll里面了,通过了解PE文件结构访问其导出表即可

好了,转成字节码试一下效果
原函数转成字节码之后代码如下:

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
#include "mytool.h"
typedef int (*PRINTF) (const char *__format, ...);
typedef int (*GET_FUNC)(int);
typedef int (*FnAdd)(int,int,GET_FUNC);
byte addData[] = {0x55,0x89,0xe5,0x83,0xec,0x48,0xc7,0x4,0x24,0x0,0x0,0x0,0x0,0x8b,0x45,0x10,0xff,0xd0,0x89,0x45,0xf4,0xc7,0x4,0x24,0x1,0x0,0x0,0x0,0x8b,0x45,0x10,0xff,0xd0,0x89,0x45,0xf0,0xc7,0x45,0xdd,0x6d,0x73,0x76,0x63,0xc7,0x45,0xe1,0x72,0x74,0x2e,0x64,0x66,0xc7,0x45,0xe5,0x6c,0x6c,0xc6,0x45,0xe7,0x0,0x8d,0x45,0xdd,0x89,0x4,0x24,0x8b,0x45,0xf4,0xff,0xd0,0x89,0x45,0xec,0xc7,0x45,0xd6,0x70,0x72,0x69,0x6e,0x66,0xc7,0x45,0xda,0x74,0x66,0xc6,0x45,0xdc,0x0,0x8d,0x45,0xd6,0x89,0x44,0x24,0x4,0x8b,0x45,0xec,0x89,0x4,0x24,0x8b,0x45,0xf0,0xff,0xd0,0x89,0x45,0xe8,0xc7,0x45,0xd2,0x25,0x64,0xa,0x0,0x8b,0x55,0x8,0x8b,0x45,0xc,0x1,0xd0,0x89,0x44,0x24,0x4,0x8d,0x45,0xd2,0x89,0x4,0x24,0x8b,0x45,0xe8,0xff,0xd0,0x8b,0x55,0x8,0x8b,0x45,0xc,0x1,0xd0,0xc9,0xc3};//add字节码
// int add(int a,int b,GET_FUNC get_func)
// {
// LOADLIBRARY load = (LOADLIBRARY)get_func(0);
// GETPROCADDRESS ProcAddress = (GETPROCADDRESS)get_func(1);
// char Module_name[] = "msvcrt.dll";
// HINSTANCE mAddress = load(Module_name);
// char fnName[] = "printf";
// PRINTF print = (PRINTF)ProcAddress(mAddress,fnName);
// char format[] = "%d\n";
// print(format,a+b);
// return a+b;
// }
int main()
{
DWORD fOldProtect = 0;
if(!VirtualProtect(addData,sizeof(addData),PAGE_EXECUTE,&fOldProtect))
{
perror("VirtualProtect failed");
system("pause");
}
FnAdd add = (FnAdd)addData;
add(3,4,Get_Func);
system("pause");
return 0;
}

运行成功,输出7
在IDA中的伪代码窗口如下:
Alt text
Alt text

隐藏性有所提升,但是还可以进一步提升
我们可以把Get_Func函数也通过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
#include "mytool.h"
typedef int (*PRINTF) (const char *__format, ...);
typedef int (*GET_FUNC)(int);
typedef int (*FnAdd)(int,int,GET_FUNC);
byte addData[] = {0x55,0x89,0xe5,0x83,0xec,0x48,0xc7,0x4,0x24,0x0,0x0,0x0,0x0,0x8b,0x45,0x10,0xff,0xd0,0x89,0x45,0xf4,0xc7,0x4,0x24,0x1,0x0,0x0,0x0,0x8b,0x45,0x10,0xff,0xd0,0x89,0x45,0xf0,0xc7,0x45,0xdd,0x6d,0x73,0x76,0x63,0xc7,0x45,0xe1,0x72,0x74,0x2e,0x64,0x66,0xc7,0x45,0xe5,0x6c,0x6c,0xc6,0x45,0xe7,0x0,0x8d,0x45,0xdd,0x89,0x4,0x24,0x8b,0x45,0xf4,0xff,0xd0,0x89,0x45,0xec,0xc7,0x45,0xd6,0x70,0x72,0x69,0x6e,0x66,0xc7,0x45,0xda,0x74,0x66,0xc6,0x45,0xdc,0x0,0x8d,0x45,0xd6,0x89,0x44,0x24,0x4,0x8b,0x45,0xec,0x89,0x4,0x24,0x8b,0x45,0xf0,0xff,0xd0,0x89,0x45,0xe8,0xc7,0x45,0xd2,0x25,0x64,0xa,0x0,0x8b,0x55,0x8,0x8b,0x45,0xc,0x1,0xd0,0x89,0x44,0x24,0x4,0x8d,0x45,0xd2,0x89,0x4,0x24,0x8b,0x45,0xe8,0xff,0xd0,0x8b,0x55,0x8,0x8b,0x45,0xc,0x1,0xd0,0xc9,0xc3};
byte get_func[] = {0x55, 0x89, 0xE5, 0x89, 0xE5, 0x6A, 0x00, 0x68, 0x73, 0x73, 0x00, 0x00, 0x68, 0x64, 0x64, 0x72, 0x65, 0x68, 0x72, 0x6F, 0x63, 0x41, 0x68, 0x47, 0x65, 0x74, 0x50, 0x6A, 0x00, 0x68, 0x61, 0x72, 0x79, 0x41, 0x68, 0x4C, 0x69, 0x62, 0x72, 0x68, 0x4C, 0x6F, 0x61, 0x64, 0x89, 0xE1, 0x51, 0x83, 0x7D, 0x08, 0x00, 0x75, 0x07, 0xE8, 0x91, 0x00, 0x00, 0x00, 0xEB, 0x05, 0xE8, 0xA8, 0x00, 0x00, 0x00, 0x89, 0xEC, 0x5D, 0xC3, 0x55, 0x89, 0xE5, 0x83, 0xEC, 0x20, 0x56, 0x64, 0x8B, 0x35, 0x30, 0x00, 0x00, 0x00, 0x8B, 0x76, 0x0C, 0x8B, 0x76, 0x1C, 0x8B, 0x36, 0x8B, 0x76, 0x08, 0x89, 0xF0, 0x5E, 0x89, 0xEC, 0x5D, 0xC3, 0x55, 0x89, 0xE5, 0x83, 0xEC, 0x20, 0x8B, 0x55, 0x08, 0x8B, 0x72, 0x3C, 0x8D, 0x34, 0x32, 0x8B, 0x76, 0x78, 0x8D, 0x34, 0x32, 0x8B, 0x7E, 0x1C, 0x8D, 0x3C, 0x3A, 0x89, 0x7D, 0xFC, 0x8B, 0x7E, 0x20, 0x8D, 0x3C, 0x3A, 0x89, 0x7D, 0xF8, 0x8B, 0x7E, 0x24, 0x8D, 0x3C, 0x3A, 0x89, 0x7D, 0xF4, 0x31, 0xC0, 0xEB, 0x01, 0x40, 0x8B, 0x75, 0xF8, 0x8B, 0x34, 0x86, 0x8D, 0x34, 0x32, 0x8B, 0x7D, 0x0C, 0x8B, 0x4D, 0x10, 0xF3, 0xA6, 0x8B, 0x75, 0xF4, 0x75, 0xE9, 0x8B, 0x75, 0xF4, 0x31, 0xFF, 0x66, 0x8B, 0x3C, 0x46, 0x8B, 0x55, 0xFC, 0x8B, 0x34, 0xBA, 0x8B, 0x55, 0x08, 0x8D, 0x04, 0x32, 0x89, 0xEC, 0x5D, 0xC2, 0x0C, 0x00, 0x55, 0x89, 0xE5, 0x83, 0xEC, 0x10, 0xE8, 0x6F, 0xFF, 0xFF, 0xFF, 0x89, 0x45, 0xFC, 0x8D, 0x4D, 0x0C, 0x6A, 0x0C, 0x51, 0x50, 0xE8, 0x80, 0xFF, 0xFF, 0xFF, 0x89, 0xEC, 0x5D, 0xC3, 0x55, 0x89, 0xE5, 0x83, 0xEC, 0x10, 0xE8, 0x51, 0xFF, 0xFF, 0xFF, 0x89, 0x45, 0xFC, 0x6A, 0x0E, 0x8D, 0x4D, 0x1C, 0x51, 0xFF, 0x75, 0xFC, 0xE8, 0x60, 0xFF, 0xFF, 0xFF, 0x89, 0xEC, 0x5D, 0xc3};
// int add(int a,int b,GET_FUNC get_func)
// {
// LOADLIBRARY load = (LOADLIBRARY)get_func(0);
// GETPROCADDRESS ProcAddress = (GETPROCADDRESS)get_func(1);
// char Module_name[] = "msvcrt.dll";
// HINSTANCE mAddress = load(Module_name);
// char fnName[] = "printf";
// PRINTF print = (PRINTF)ProcAddress(mAddress,fnName);
// char format[] = "%d\n";
// print(format,a+b);
// return a+b;
// }
int main()
{

DWORD fOldProtect = 0;
if(!VirtualProtect(addData,sizeof(addData),PAGE_EXECUTE,&fOldProtect)||
!VirtualProtect(get_func,sizeof(get_func),PAGE_EXECUTE,&fOldProtect) )
{
perror("VirtualProtect failed");
system("pause");
}
FnAdd add = (FnAdd)addData;
GET_FUNC get_fn = (GET_FUNC)get_func;
add(3,4,get_fn);
system("pause");
return 0;
}

实现效果:
Alt text

2.新建可执行的节段存储加密函数

Linux下把加密代码放到数据段:

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
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

// 一个全局变量,用来存放机器指令
unsigned char code[8] = {0};

// 一个函数指针,用来指向机器指令的地址
typedef int (*func_ptr)();

int main()
{
// 获取当前系统的内存页大小
int pagesize = sysconf(_SC_PAGESIZE);
printf("pagesize = %d\n", pagesize);

// 计算全局变量code的地址所在的内存页的起始地址
unsigned long start = (unsigned long)code & ~(pagesize - 1);
printf("start = %p\n", (void *)start);

// 计算全局变量code的地址所在的内存页的结束地址
unsigned long end = ((unsigned long)code + sizeof(code) + pagesize - 1) & ~(pagesize - 1);
printf("end = %p\n", (void *)end);

// 计算全局变量code所占用的内存页的长度
size_t len = end - start;
printf("len = %ld\n", len);

// 给全局变量code所在的内存页可执行权限
int ret = mprotect((void *)start, len, PROT_READ | PROT_EXEC);
if (ret == -1)
{
perror("mprotect");
return -1;
}

// 在全局变量code中写入一段机器指令,其功能是将rax加1并返回
// 该指令的二进制码为:48 ff c0 c3
code[0] = 0x48;
code[1] = 0xff;
code[2] = 0xc0;
code[3] = 0xc3;

// 将函数指针指向全局变量code的地址
func_ptr f = (func_ptr)code;

// 调用函数指针,执行机器指令
int result = f();
printf("result = %d\n", result);

return 0;
}