开发的角度认识一下防止模拟执行和反调试函数(RC4算法)
一:几种常见的情况介绍
在软件开发中,尤其是在安全敏感的应用程序(如加密软件、游戏、反病毒软件等)中,防止模拟执行(也称为反模拟)和反调试是非常重要的。模拟执行和调试可以被攻击者用来逆向工程软件,提取敏感信息或绕过安全机制。
1. 防止模拟执行
模拟执行是指攻击者使用虚拟机或模拟器来运行应用程序,以便分析其行为和提取信息。为了防止这种情况,开发者可以采取以下措施:
a. 检测虚拟机环境
- 检查特征: 通过检查系统特征(如硬件信息、驱动程序、特定文件等)来判断程序是否在虚拟机中运行。例如,某些虚拟机可能会在系统中留下特定的标识符。
- 使用 API: 利用系统 API 检测是否在虚拟机中运行。例如,在 Windows 中,可以使用
IsDebuggerPresent
或CheckRemoteDebuggerPresent
函数。
b. 代码混淆
- 混淆代码: 通过代码混淆技术使得逆向工程变得更加困难。混淆可以包括重命名变量、改变控制流、插入无用代码等。
- 动态加载: 将关键代码动态加载或加密,只有在运行时才解密和执行。
c. 反模拟技术
- 时间检查: 通过测量代码执行的时间来检测是否在模拟环境中运行。虚拟机的执行速度可能与真实硬件不同。
- 环境变量检查: 检查特定的环境变量或系统配置,以识别是否在模拟环境中。
2. 防止反调试
反调试是指防止攻击者使用调试器来分析和修改程序的行为。开发者可以使用以下技术来实现反调试:
a. 检测调试器
- 使用系统 API: 在 Windows 中,可以使用
IsDebuggerPresent
函数来检查当前进程是否被调试。如果返回值为真,则表示有调试器附加。 - 检查调试标志: 通过检查进程的状态标志(如
PROCESS_DEBUG_PORT
)来判断是否有调试器附加。
b. 代码完整性检查
- 哈希校验: 在程序运行时计算关键代码段的哈希值,并与预先计算的值进行比较。如果值不匹配,可能表示代码被修改或调试。
- 自我保护: 在程序中嵌入自我保护机制,检测是否有调试器附加,并在检测到时采取措施(如退出程序)。
c. 反调试技术
- 异常处理: 利用异常处理机制来检测调试器的存在。例如,调试器在程序中设置断点时,可能会引发异常。
- 时间延迟: 在关键代码段中引入时间延迟,调试器可能会影响执行时间,从而暴露其存在。
以下是一些示例代码,展示如何在 C/C++ 中实现防止模拟执行和反调试的基本技术。这些示例包括检测调试器、检查虚拟机环境以及简单的代码混淆。
二:代码举例
1. 检测调试器
在 Windows 中,可以使用 IsDebuggerPresent
函数来检测当前进程是否被调试。
#include <windows.h>
#include <stdio.h>void checkDebugger() {if (IsDebuggerPresent()) {printf("Debugger detected! Exiting...\n");ExitProcess(1); // 退出程序} else {printf("No debugger detected.\n");}
}int main() {checkDebugger();// 其他代码...return 0;
}
2. 检查虚拟机环境
可以通过检查特定的系统特征来判断程序是否在虚拟机中运行。检查是否在 VMware 虚拟机中运行。
#include <windows.h>
#include <stdio.h>bool isRunningInVM() {// 检查特定的硬件信息HKEY hKey;if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System", 0, KEY_READ, &hKey) == ERROR_SUCCESS) {char value[256];DWORD value_length = sizeof(value);if (RegQueryValueEx(hKey, "SystemBiosVersion", NULL, NULL, (LPBYTE)value, &value_length) == ERROR_SUCCESS) {if (strstr(value, "VMware") != NULL) {RegCloseKey(hKey);return true; // 在 VMware 中运行}}RegCloseKey(hKey);}return false; // 不在虚拟机中运行
}int main() {if (isRunningInVM()) {printf("Running in a virtual machine! Exiting...\n");ExitProcess(1);} else {printf("Not running in a virtual machine.\n");}// 其他代码...return 0;
}
3. 代码混淆示例
以下是一个简单的代码混淆示例,通过重命名变量和函数来增加逆向工程的难度。
#include <stdio.h>int obfuscatedFunction(int a, int b) {return a + b; // 原本简单的加法
}int main() {int x = 5;int y = 10;int result = obfuscatedFunction(x, y);printf("Result: %d\n", result);return 0;
}
在实际应用中,可以使用更复杂的混淆技术,例如将函数体拆分、插入无用代码等。
4. 反调试技术示例
以下是一个使用异常处理来检测调试器的示例:
#include <windows.h>
#include <stdio.h>LONG WINAPI ExceptionHandler(EXCEPTION_POINTERS* ExceptionInfo) {printf("Exception detected! Possible debugger presence.\n");ExitProcess(1);return EXCEPTION_CONTINUE_SEARCH;
}void checkForDebugger() {SetUnhandledExceptionFilter(ExceptionHandler);__try {// 故意引发异常int* p = NULL;*p = 0; // 访问空指针} __except (EXCEPTION_EXECUTE_HANDLER) {printf("Caught an exception.\n");}
}int main() {checkForDebugger();// 其他代码...return 0;
}
三:关键代码认识一下
1.检测android系统cpu相关的信息文件,防止模拟执行
static __attribute__((always_inline)) void checkCpu()
//static: 表示该函数的作用域仅限于当前文件,不能被其他文件访问。
//__attribute__((always_inline)): 这是一个编译器指令,提示编译器尽可能将该函数内联,以提高性能。
{/*** check** /sys/devices/system/cpu/possible** /sys/devices/system/cpu/present** /proc/cpuinfo** */LOGD("%s", "进入checkCpu"); //使用 LOGD 函数记录进入 checkCpu 函数的日志,通常用于调试目的。FILE* file =NULL;//打开 /sys/devices/system/cpu/possible 文件file=fopen("/sys/devices/system/cpu/possible","rb"); //二进制读取模式打开char cpuStr[128];if(file==NULL){LOGD("%s", "11111");asm_exit();}//读取文件内容char* readInPtr = fgets(cpuStr, 128, file);//使用 fgets 从文件中读取一行内容到 cpuStr 数组中fclose(file);if(NULL == readInPtr) //关闭文件后,如果读取失败,则尝试打开 /sys/devices/system/cpu/present 文件,该文件包含当前可用的 CPU 列表。{file=fopen("/sys/devices/system/cpu/present","rb");readInPtr=fgets(cpuStr,128,file);LOGD("%s", cpuStr);if(readInPtr==NULL){LOGD("%s", "22222");asm_exit();}}//记录读取的 CPU 信息LOGD("%s", cpuStr);fclose(file);readInPtr=NULL;memset(cpuStr,128,sizeof(char)); //将 readInPtr 设为 NULL,并清空 cpuStr 数组。//打开 /proc/cpuinfo 文件file =fopen("/proc/cpuinfo","rb"); //cpuinfo文件包含关于 CPU 的详细信息if(file==NULL){LOGD("%s", "33333");asm_exit();}readInPtr = fgets(cpuStr, 128, file);if(NULL==readInPtr){LOGD("%s", "44444");asm_exit();}LOGD("%s", cpuStr);}
2.反调试函数,主要用来检测TracerPid是否为0
static __attribute__((always_inline)) void checkTracerpid()
{int pid = getpid(); //使用 getpid() 函数获取当前进程的进程 IDint bufsize = 256;char filename[bufsize];char line[bufsize];int tracerpid;FILE *fp;sprintf(filename, "/proc/%d/status", pid); //用 sprintf 构建当前进程的状态文件路径,格式为 /proc/<pid>/statusfp = fopen(filename, "r"); //以只读模式打开状态文件//读取文件内容if (fp != NULL) { //如果文件成功打开,逐行读取文件内容。while (fgets(line, bufsize, fp)) {if (strstr(line, "TracerPid") != NULL) { //用 strstr 查找包含 "TracerPid" 的行tracerpid = atoi(&line[10]); //通过 atoi 将 "TracerPid" 后的值转换为整数if (tracerpid != 0) {asm_exit(); //如果 tracerpid 不为 0,调用 asm_exit(),表示当前进程被跟踪。}break;}}fclose(fp);}
}void* threadCheckTracerPid(void *pVoid) {int pid = getpid(); //同样使用 getpid() 获取当前进程的进程 ID。char file_name[20] = {'\0'};sprintf(file_name, "/proc/%d/status",pid); //构建当前进程的状态文件路径。char linestr[256];int i=0, traceid;FILE *fp;//进入一个无限循环,定期检查进程的状态文件。while(1){fp = fopen(file_name,"r");if(fp == NULL){break; //如果文件无法打开,退出循环。}//读取文件内容while(!feof(fp)){fgets(linestr, 256, fp);while (fgets(linestr, 256, fp)) {if (strstr(linestr, "TracerPid") != NULL) { //逐行读取文件内容,查找 "TracerPid" 行。//如果找到,解析 tracerpid,并在不为 0 时调用 asm_exit()。traceid = atoi(&linestr[10]);if (traceid != 0) {asm_exit();}break;}}}fclose(fp);sleep(5); //线程休眠 5 秒,然后重新检查。}}
四:再来看看RC4算法叭
RC4(Rivest Cipher 4)是一种流加密算法,由 Ronald Rivest 设计。它以其简单性和速度而闻名,广泛用于各种加密协议中,如 SSL/TLS 和 WEP。以下是 RC4 算法的基本原理、加密和解密过程的详细说明。
RC4 算法概述
RC4 是一种对称密钥加密算法,意味着加密和解密使用相同的密钥。它的工作原理基于一个伪随机生成器,生成一个密钥流,然后将该密钥流与明文进行异或运算以生成密文。
主要步骤
-
密钥调度算法(KSA):
- 初始化一个状态数组
S
,其大小为 256。 - 使用给定的密钥对状态数组进行置换。
- 初始化一个状态数组
-
伪随机生成算法(PRGA):
- 生成伪随机字节流。
- 将生成的字节流与明文进行异或运算以生成密文。
加密过程
-
密钥调度算法(KSA):
def KSA(key):key_length = len(key)S = list(range(256))j = 0for i in range(256):j = (j + S[i] + key[i % key_length]) % 256S[i], S[j] = S[j], S[i] # 交换return S
-
伪随机生成算法(PRGA):
def PRGA(S):i = 0j = 0while True:i = (i + 1) % 256j = (j + S[i]) % 256S[i], S[j] = S[j], S[i] # 交换K = S[(S[i] + S[j]) % 256]yield K # 生成伪随机字节
-
加密函数:
def RC4_encrypt(key, plaintext):S = KSA(key)keystream = PRGA(S)ciphertext = bytearray()for byte in plaintext:ciphertext.append(byte ^ next(keystream)) # 异或运算return bytes(ciphertext)
解密过程
RC4 的解密过程与加密过程相同,因为它是对称的。只需使用相同的密钥和密文进行异或运算即可恢复明文。
def RC4_decrypt(key, ciphertext):return RC4_encrypt(key, ciphertext) # 使用相同的函数
示例
以下是一个完整的示例,展示如何使用 RC4 算法进行加密和解密:
def main():key = b'SecretKey' # 密钥plaintext = b'Hello, World!' # 明文# 加密ciphertext = RC4_encrypt(key, plaintext)print(f'Ciphertext: {ciphertext}')# 解密decrypted_text = RC4_decrypt(key, ciphertext)print(f'Decrypted: {decrypted_text}')if __name__ == "__main__":main()
注意事项
- 密钥长度: RC4 支持任意长度的密钥,但通常使用 40 到 2048 位的密钥。较长的密钥提供更高的安全性。
- 安全性: RC4 已被发现存在多种安全漏洞,尤其是在使用不当时。因此,在新的应用中,建议使用更安全的加密算法,如 AES。
- 流加密: RC4 是流加密算法,适合处理任意长度的数据流,但不适合处理大块数据。
案例:
bool rc4_crypt(JNIEnv* env,const char* ss)__attribute((__annotate__(("bbb"))))
{jclass fkclazz = NULL;jmethodID fkId = NULL;jobject fkObj = NULL;jmethodID fkMethodIdadd = NULL;jmethodID fkMethodIdsub = NULL;jmethodID fkMethodIdxor = NULL;fkclazz = env->FindClass( "com/xxx");fkId = env->GetMethodID( fkclazz, "<init>", "()V" ); /* 获取构造函数 */fkObj = env->NewObject( fkclazz, fkId ); /* 创建对象 */fkMethodIdadd = env->GetMethodID( fkclazz, "add", "(II)I" );fkMethodIdsub = env->GetMethodID( fkclazz, "sub", "(II)I" );fkMethodIdxor = env->GetMethodID( fkclazz, "xor", "(II)I" );//flag{You_f1nd_ME_X1a}unsigned char s[256];memset(s,0,256);//char key[256] = "0x123456789";unsigned char key[256];memset(key,0,256);// 计算 key 数组的其他元素key[0] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,20,28);key[1] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,key[0],72);key[2] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,key[0],1);key[3] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,key[0],2);key[4] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,key[0],3);key[5] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,key[0],4);key[6] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,key[0],5);key[7] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,key[0],6);key[8] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,key[0],7);key[9] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,key[0],8);key[10] = (int)env->CallIntMethod( fkObj, fkMethodIdsub,key[0],9);key[11] = (int)env->CallIntMethod( fkObj, fkMethodIdxor,key[0],key[0]);unsigned char data[256];memset(data,0,256);int result = 0;memcpy(data,ss, strlen(ss)+1);//char data[512] = {228,21,196,237,166,47,86,16,187,19,235,173,117,86,199,187,233,185,204,2,58,80,159,54,144,105,190,125,66,68,202,198,212,36,92,210,185,36,193,24,147,179,234}; //找到规律,小数保留(max117),大数减去256(min125)unsigned char datas[45]={115,3,112,89,84,0,218,91,44,20,154,4,97,87,232,255,24,222,95,226,189,204,61,124,116,119,63,225,154,170,191,158,195,47,108,102,1,0,83,24,20,245};unsigned long len = 42;jint demo=datas[0];result = (int)env->CallIntMethod( fkObj, fkMethodIdadd, 6, demo );datas[0]=result;demo=datas[20];//189result = (int)env->CallIntMethod( fkObj, fkMethodIdadd, len, demo );demo=datas[19];//226result = (int)env->CallIntMethod( fkObj, fkMethodIdxor, demo, result );result = (int)env->CallIntMethod( fkObj, fkMethodIdadd, (jint)result, 331);int ii=0;int jj=0;unsigned char kk[256]={};memset(kk,0,256);unsigned char temps = 0;// RC4 算法的核心部分for(ii=0;ii<result;ii++){s[ii]=ii; //0-255赋给skk[ii]=key[ii%len]; //将k重新计算}for(ii=0;ii<result;ii++){jj=(jj+s[ii]+kk[ii])%result; //给j赋temps=s[ii];checkTracerpid();s[ii]=s[jj];s[jj]=temps; //s[i]和s[j]交换}int i=0,j=0,t=0;jint demos=0;unsigned long k=0;unsigned char temp;checkTracerpid();for(k=0;k<len;k++){i=(i+1)%result; //固定方式生成的ij=(j+s[i])%result; //固定方式生成的jtemp=s[i];s[i]=s[j];s[j]=temp; //交换t=(s[i]+s[j])%result; //固定方式生成的t//data[k]^=s[t]; //异或运算demo=data[k];demos=s[t];data[k] = (int)env->CallIntMethod( fkObj, fkMethodIdadd, demo ,demos);}// 验证数据for(k=0;k<len;k++){checkTracerpid();if(data[k]!=datas[k]){return false;}}return true;
}