Windows程序字符串处理与逆向分析
Windows程序字符串处理与逆向分析
文章目录
- Windows程序字符串处理与逆向分析
- 1. 字符串类型
- 1.1 C风格字符串 (char*)
- 1.2 宽字符字符串 (wchar_t*)
- 1.3 C++标准字符串
- 2. 字符串处理函数
- 2.1 字符串长度计算 - strlen
- 2.2 字符串复制 - strcpy/strncpy
- 2.3 字符串比较 - strcmp
- 2.4 字符串搜索 - strstr
- 3. 字符串存储方式
- 3.1 不同存储位置的汇编特征
- 4. 安全与混淆技术
- 4.1 字符串加密的汇编实现
- 4.2 安全字符串处理的汇编对比
- 逆向工程实战技巧
- 识别字符串操作的启发式方法
- 字符串混淆的对抗技术
1. 字符串类型
1.1 C风格字符串 (char*)
代码示例:
#include <stdio.h>
#include <string.h>void demonstrate_c_string() {char str1[] = "Hello World";char* str4 = malloc(20);strcpy(str4, "Dynamic String");printf("str1: %s\n", str1);// 内联汇编详细解释__asm {; === 汇编解释:访问C风格字符串 ===mov esi, offset str1 ; ESI = 字符串首地址(在x86中,offset获取标号地址); 在内存中:48 65 6C 6C 6F 20 57 6F 72 6C 64 00 ("Hello World"的ASCII)mov al, [esi] ; AL = 字节[ESI] = 'H' (0x48); 这是直接内存访问,ESI是基地址,[ESI]表示该地址处的字节mov bl, [esi + 1] ; BL = 字节[ESI+1] = 'e' (0x65); 通过基地址+偏移量访问字符串中的特定字符; === 逆向工程识别特征 ===; 1. 连续字节序列,以00结尾; 2. 常见访问模式:mov reg, [base + index]; 3. 循环通常以cmp byte ptr [reg], 0检测结束}free(str4);
}
汇编层详细分析:
; 编译后在.data段或栈上的实际布局
_str1 db 'H','e','l','l','o',' ','W','o','r','l','d',0
; 内存地址: 0x00403000: 48 65 6C 6C 6F 20 57 6F 72 6C 64 00; 访问字符串的典型汇编代码
mov eax, offset _str1 ; EAX = 0x00403000 (字符串起始地址)
mov cl, [eax] ; CL = 0x48 ('H')
inc eax ; EAX = 0x00403001
mov cl, [eax] ; CL = 0x65 ('e')
1.2 宽字符字符串 (wchar_t*)
代码示例:
#include <stdio.h>
#include <wchar.h>void demonstrate_wide_string() {wchar_t wstr1[] = L"Hello World";wchar_t* wstr3 = malloc(20 * sizeof(wchar_t));wcscpy(wstr3, L"宽字符串");// 内联汇编详细解释__asm {; === 汇编解释:访问宽字符串 ===mov esi, offset wstr1 ; ESI指向宽字符串首地址; 内存布局:48 00 65 00 6C 00 6C 00 6F 00 20 00 57 00 6F 00 72 00 6C 00 64 00 00 00; 小端序:低字节在前,所以'H'存储为0x0048mov ax, [esi] ; AX = 字[ESI] = 0x0048 ('H'); 注意:这里用AX(16位)而不是AL(8位),因为宽字符是2字节mov bx, [esi + 2] ; BX = 字[ESI+2] = 0x0065 ('e'); 每个字符占2字节,所以+2而不是+1; === 逆向工程识别特征 ===; 1. 内存中每2字节一组,以0000结尾; 2. 使用字(word)操作而不是字节(byte)操作; 3. Windows API调用使用stdcall,参数从右向左压栈}free(wstr3);
}
宽字符串汇编细节:
; 宽字符串在内存中的实际存储
_wstr1 dw 0048h, 0065h, 006Ch, 006Ch, 006Fh, 0020h, 0057h, 006Fh, 0072h, 006Ch, 0064h, 0000h
; 或者以字节形式:48 00 65 00 6C 00 6C 00 6F 00 20 00 57 00 6F 00 72 00 6C 00 64 00 00 00; 宽字符串操作的典型汇编
mov edi, offset _wstr1 ; EDI指向宽字符串
mov ax, [edi] ; AX = 'H' (0x0048)
add edi, 2 ; 宽字符:每次+2字节
mov ax, [edi] ; AX = 'e' (0x0065); 宽字符串函数调用(MessageBoxW)
push 0 ; uType = MB_OK
push offset _wtitle ; lpCaption
push offset _wstr1 ; lpText (宽字符串)
push 0 ; hWnd = NULL
call MessageBoxW ; 调用宽字符版本API
1.3 C++标准字符串
代码示例:
#include <string>void demonstrate_cpp_string() {std::string str1 = "Hello World";const char* c_str = str1.c_str();// 内联汇编详细解释__asm {; === 汇编解释:std::string内部结构 ===mov ecx, offset str1 ; ECX指向string对象(this指针); std::string典型内存布局(MSVC实现):; [ECX] : 字符串数据指针(或小型字符串优化时的内联缓冲区); [ECX+4] : 字符串长度; [ECX+8] : 缓冲区容量mov eax, [ecx] ; EAX = 字符串数据指针; 如果是小型字符串优化(SSO),[ECX]可能直接包含字符串内容mov cl, [eax] ; CL = 第一个字符; === 逆向工程识别特征 ===; 1. 识别std::basic_string模板的虚函数表; 2. 常见函数调用:call std::basic_string<char>::c_str; 3. 小型字符串优化(SSO):字符串直接存储在对象内部}
}
std::string逆向分析特征:
; MSVC中std::string的典型汇编模式
lea ecx, [ebp+str1] ; ECX = this指针(std::string对象地址)
call std::basic_string<char,std::char_traits<char>,std::allocator<char>>::c_str
; 返回值为EAX,指向字符串数据; 或者直接访问内部结构(依赖具体实现)
mov eax, [ebp+str1] ; 假设这是数据指针
mov cl, byte ptr [eax] ; 读取第一个字符; 小型字符串优化(SSO)的识别
; 当字符串较短时,可能直接存储在栈上的string对象内
; 而不是通过指针访问堆内存
2. 字符串处理函数
2.1 字符串长度计算 - strlen
汇编层详细解释:
#include <string.h>void demonstrate_strlen() {char str[] = "Reverse Engineering";size_t len = strlen(str);// 内联汇编展示strlen工作原理__asm {; === 汇编解释:strlen实现原理 ===mov edi, str ; EDI指向字符串开始xor ecx, ecx ; ECX = 0dec ecx ; ECX = 0xFFFFFFFF (最大计数值)xor al, al ; AL = 0 (要扫描的终止字节)repne scasb ; 重复执行:扫描字节[EDI]与AL比较; 工作原理:; 1. 比较[EDI]与AL (0); 2. 如果相等,ZF=1,停止; 3. 否则EDI++,ECX--; 4. 重复直到ECX=0或找到匹配not ecx ; 对ECX取反:0xFFFFFFFE -> 0x00000001dec ecx ; ECX--,得到实际长度mov len, ecx; === 指令细节 ===; REPNE: Repeat While Not Equal - 当ZF=0且ECX≠0时重复; SCASB: Scan String Byte - 比较AL与[EDI],并设置EDI++; 最终EDI指向终止字节后一个位置,ECX包含剩余计数}
}
strlen的多种汇编实现:
; 方法1:使用repne scasb(编译器优化版本)
strlen_repne:mov edi, [esp+4] ; 字符串指针mov ecx, -1 ; 最大计数xor eax, eax ; 搜索0字节repne scasb ; 扫描直到找到0not ecx ; 计算长度dec ecxmov eax, ecxret; 方法2:手动循环(调试版本更易读)
strlen_loop:mov edx, [esp+4] ; 字符串指针xor eax, eax ; 长度计数器
scan_loop:cmp byte ptr [edx+eax], 0 ; 检查当前字符是否为0je found_end ; 如果是,跳转到结束inc eax ; 否则计数器+1jmp scan_loop ; 继续循环
found_end:ret; 方法3:优化手动循环(一次检查4字节)
strlen_optimized:mov edx, [esp+4] ; 字符串指针mov eax, edx ; EAX也指向开始
align_loop:test edx, 3 ; 检查是否4字节对齐jz aligned ; 如果对齐,继续cmp byte ptr [edx], 0 ; 检查当前字节je found ; 如果是0,结束inc edx ; 指针前进jmp align_loop ; 继续对齐循环
2.2 字符串复制 - strcpy/strncpy
汇编层详细解释:
#include <string.h>void demonstrate_strcpy() {char src[] = "Copy this string";char dest[50];strcpy(dest, src);// 内联汇编展示strcpy工作原理__asm {; === 汇编解释:strcpy实现 ===mov esi, offset src ; ESI = 源字符串指针mov edi, offset dest ; EDI = 目标缓冲区指针cld ; 清除方向标志DF=0(向前移动)copy_loop:mov al, [esi] ; AL = 当前源字符mov [edi], al ; [EDI] = AL (复制到目标)inc esi ; 源指针前进inc edi ; 目标指针前进test al, al ; 检查刚复制的字符是否为0jnz copy_loop ; 如果不是0,继续循环; === 优化版本使用rep movsb ===mov esi, offset srcmov edi, offset destmov ecx, length_of_src ; 如果知道长度,可以更快rep movsb ; 重复复制ECX次; === 安全风险分析 ===; 如果src长度 > dest缓冲区大小,会导致缓冲区溢出; 攻击者可能覆盖返回地址或重要数据}
}
strcpy安全漏洞的汇编表现:
; 不安全的strcpy使用
push offset src_string ; 源字符串(可能很长)
push offset small_buffer ; 小缓冲区(比如16字节)
call strcpy ; 危险的调用!
add esp, 8; 攻击场景:
; [栈布局]
; 0028FF00: small_buffer (16字节) <- 可能被覆盖
; 0028FF10: 保存的EBP <- 被覆盖导致栈破坏
; 0028FF14: 返回地址 <- 被覆盖可能执行恶意代码
; 0028FF18: 其他重要数据 <- 全部被破坏; 安全版本:strncpy
push 16 ; 最大复制长度
push offset src_string ; 源字符串
push offset small_buffer ; 目标缓冲区
call strncpy ; 安全的复制
add esp, 12
2.3 字符串比较 - strcmp
汇编层详细解释:
#include <string.h>void demonstrate_strcmp() {char str1[] = "password";char str2[] = "password";int result = strcmp(str1, str2);// 内联汇编展示strcmp工作原理__asm {; === 汇编解释:strcmp实现 ===mov esi, offset str1 ; ESI = 第一个字符串mov edi, offset str2 ; EDI = 第二个字符串compare_loop:mov al, [esi] ; AL = str1的当前字符mov bl, [edi] ; BL = str2的当前字符cmp al, bl ; 比较两个字符jne different ; 如果不相等,跳转test al, al ; 检查是否到达字符串结尾(0)jz equal ; 如果都是0,字符串相等inc esi ; 移动到下一个字符inc edijmp compare_loop ; 继续比较equal:xor eax, eax ; 返回0(相等)jmp donedifferent:sbb eax, eax ; 巧妙设置返回值or al, 1 ; 如果不相等,返回-1或1; 具体:如果AL<BL,返回-1;如果AL>BL,返回1done:mov result, eax; === 优化版本使用repe cmpsb ===mov esi, offset str1mov edi, offset str2mov ecx, 0FFFFFFFFh ; 最大比较长度repe cmpsb ; 重复比较直到不相等; 结束后:[ESI-1]和[EDI-1]是不相等的字符位置}
}
strcmp在逆向工程中的关键作用:
; 序列号检查的典型模式
call get_user_input ; 获取用户输入
push eax ; 用户输入的序列号
push offset valid_serial ; 正确的序列号
call strcmp
add esp, 8
test eax, eax ; 检查返回值
jnz invalid_serial ; 如果不等于0,跳转到错误处理; 密码验证模式
mov esi, user_password
mov edi, correct_password
compare_loop:mov al, [esi]cmp al, [edi]jnz access_denied ; 任何一个字符不匹配就拒绝test al, aljz access_granted ; 同时到达结尾,验证通过inc esiinc edijmp compare_loopaccess_granted:; 验证成功的代码
access_denied:; 验证失败的代码
2.4 字符串搜索 - strstr
汇编层详细解释:
#include <string.h>void demonstrate_strstr() {char text[] = "The quick brown fox jumps over the lazy dog";char keyword[] = "fox";char* found = strstr(text, keyword);// 内联汇编展示strstr工作原理__asm {; === 汇编解释:strstr实现 ===mov esi, offset text ; ESI = 主字符串mov edi, offset keyword ; EDI = 要查找的子串outer_loop:mov al, [esi] ; AL = 主字符串当前字符test al, al ; 检查是否主字符串结束jz not_found ; 如果结束,没找到mov al, [edi] ; AL = 子串第一个字符cmp al, [esi] ; 比较第一个字符jne next_char ; 不匹配,检查下一个位置; 第一个字符匹配,检查剩余字符push esi ; 保存主字符串当前位置push edi ; 保存子串开始位置mov ecx, 0 ; 偏移量计数器check_inner:mov al, [edi + ecx] ; 子串当前字符test al, al ; 子串结束?jz found_substring ; 全部匹配成功!cmp al, [esi + ecx] ; 比较主字符串对应位置jne next_char_pop ; 不匹配,继续外层循环inc ecx ; 匹配,检查下一个字符jmp check_innernext_char_pop:pop edi ; 恢复寄存器pop esinext_char:inc esi ; 主字符串位置前进jmp outer_loopfound_substring:pop edi ; 清理栈pop edi ; 注意:这里弹出但不恢复ESImov found, esi ; 返回匹配开始位置jmp donenot_found:mov found, 0 ; 返回NULLdone:; === 性能考虑 ===; 实际实现会使用更高效的算法如Boyer-Moore; 但基本原理相同:外层循环主串,内层循环比较子串}
}
3. 字符串存储方式
3.1 不同存储位置的汇编特征
详细汇编分析:
#include <stdio.h>
#include <stdlib.h>// 全局变量 - .data段
char global_str[] = "Global String";// 常量 - .rdata段(只读)
const char* const_str = "Constant String";void demonstrate_storage() {// 栈分配char stack_str[] = "Stack String";// 堆分配char* heap_str = malloc(50);strcpy(heap_str, "Heap String");// 内联汇编展示不同存储位置的访问__asm {; === 全局变量访问 ===mov eax, offset global_str; 编译时确定地址,如:mov eax, 00403000hmov cl, [eax] ; 直接内存访问; === 栈变量访问 === lea edx, [ebp-20] ; stack_str在栈上的位置; EBP是栈帧基址,局部变量通过EBP-偏移访问mov bl, [edx] ; 访问栈上数据; === 堆变量访问 ===mov esi, heap_str ; ESI = 堆分配的内存地址; 堆地址运行时确定,需要通过指针间接访问mov dl, [esi] ; 间接内存访问; === 常量访问 ===mov edi, const_str ; EDI = 常量字符串地址mov al, [edi] ; 访问只读内存段}free(heap_str);
}
各存储段的具体汇编表现:
; 编译后的内存布局示例
.data ; 可读写数据段
global_str db 'Global String',0 ; 00403000.rdata ; 只读数据段
const_str dd offset aConstant ; 00404000
aConstant db 'Constant String',0 ; 00404010.code ; 代码段
demonstrate_storage procpush ebpmov ebp, espsub esp, 40h ; 为局部变量分配栈空间; 栈变量:stack_str在[ebp-14h]lea eax, [ebp-14h]mov byte ptr [eax], 'S' ; 初始化栈字符串; 堆分配调用push 32h ; 50字节call mallocadd esp, 4mov [ebp+heap_str], eax ; 保存堆指针; 访问示例mov ecx, offset global_str ; 全局变量直接地址mov edx, [ebp+heap_str] ; 堆变量通过指针lea eax, [ebp-14h] ; 栈变量通过EBP偏移mov esp, ebppop ebpret
demonstrate_storage endp
4. 安全与混淆技术
4.1 字符串加密的汇编实现
详细汇编解释:
#include <windows.h>// XOR加密函数
void xor_encrypt(char* data, size_t len, char key) {for (size_t i = 0; i < len; i++) {data[i] ^= key;}
}// 运行时解密
char* decrypt_string(const char* encrypted, size_t len, char key) {char* decrypted = malloc(len + 1);// 内联汇编展示解密过程__asm {; === 汇编解释:XOR解密循环 ===mov esi, encrypted ; ESI = 加密数据源mov edi, decrypted ; EDI = 解密目标mov ecx, len ; ECX = 数据长度mov dl, key ; DL = XOR密钥decrypt_loop:mov al, [esi] ; 读取加密字节xor al, dl ; XOR解密:AL = AL ^ DLmov [edi], al ; 存储解密字节inc esi ; 源指针前进inc edi ; 目标指针前进loop decrypt_loop ; ECX--,如果≠0继续循环mov byte ptr [edi], 0 ; 添加字符串终止符}return decrypted;
}void demonstrate_obfuscation() {// 加密的字符串(在IDA中显示为乱码)const char encrypted[] = {0x25, 0x2A, 0x2F, 0x2F, 0x2E, 0x00};char* secret = decrypt_string(encrypted, 5, 0x45);// 动态获取API(避免导入表暴露)HMODULE hUser32 = GetModuleHandleA("user32.dll");__asm {; === 汇编解释:动态API解析 ===push offset aUser32 ; "user32.dll"call GetModuleHandleAmov hUser32, eaxpush offset aMessageBoxA ; "MessageBoxA" push eax ; user32.dll句柄call GetProcAddress ; 动态获取函数地址; 调用动态获取的APIpush 0 ; uTypepush offset aTitle ; lpCaptionpush secret ; lpTextpush 0 ; hWndcall eax ; 调用MessageBoxA}free(secret);
}
逆向工程中识别加密字符串:
; 加密字符串在数据段的表现
encrypted_data db 25h, 2Ah, 2Fh, 2Fh, 2Eh, 0 ; 看起来像乱码; 解密函数的典型模式
decrypt_function:mov esi, encrypted_data ; 加密数据源mov edi, buffer ; 输出缓冲区mov ecx, length ; 数据长度mov al, key ; 解密密钥
decrypt_loop:mov bl, [esi]xor bl, al ; XOR操作mov [edi], blinc esiinc ediloop decrypt_loop; 动态API调用的识别
call GetModuleHandleA ; 获取DLL句柄
push offset api_name ; API函数名
push eax ; DLL句柄
call GetProcAddress ; 获取函数地址
; 之后通过寄存器或栈间接调用
4.2 安全字符串处理的汇编对比
安全vs不安全实现的汇编对比:
// 不安全版本
void unsafe_copy(const char* input) {char buffer[16];strcpy(buffer, input); // 没有边界检查
}// 安全版本
void safe_copy(const char* input) {char buffer[16];strncpy(buffer, input, sizeof(buffer) - 1);buffer[sizeof(buffer) - 1] = '\0';
}
对应的汇编代码对比:
; 不安全版本的汇编
unsafe_copy:push ebpmov ebp, espsub esp, 10h ; 分配16字节栈空间; 危险的strcpy调用push [ebp+8] ; input参数lea eax, [ebp-10h] ; buffer地址push eaxcall strcpy ; 没有长度限制!add esp, 8mov esp, ebppop ebpret; 安全版本的汇编
safe_copy:push ebpmov ebp, espsub esp, 10h ; 分配16字节栈空间; 安全的strncpy调用push 0Fh ; 最大15字符push [ebp+8] ; input参数lea eax, [ebp-10h] ; buffer地址push eaxcall strncpy ; 有长度限制add esp, 0Ch; 确保终止符mov byte ptr [ebp-1], 0 ; buffer[15] = 0mov esp, ebppop ebpret
逆向工程实战技巧
识别字符串操作的启发式方法
在IDA Pro中的分析模式:
; 模式1:repne scasb - 字符串长度计算
mov edi, [ebp+string_ptr]
xor eax, eax
mov ecx, 0FFFFFFFFh
repne scasb ; -> strlen; 模式2:repe cmpsb - 字符串比较
mov esi, [ebp+str1]
mov edi, [ebp+str2]
mov ecx, length
repe cmpsb ; -> strcmp/memcmp; 模式3:rep movsb - 字符串复制
mov esi, [ebp+src]
mov edi, [ebp+dst]
mov ecx, length
rep movsb ; -> strcpy/memcpy; 模式4:手动循环 - 自定义字符串处理
mov esi, [ebp+string]
process_loop:mov al, [esi]cmp al, 'a'jb skip_charcmp al, 'z'ja skip_charsub al, 20h ; 小写转大写mov [esi], al
skip_char:inc esitest al, aljnz process_loop
字符串混淆的对抗技术
识别和解决字符串混淆:
; 加密字符串的识别特征
encrypted_string db 0A7h, 0C3h, 92h, 15h, 0F4h, 0; 查找解密循环
mov esi, encrypted_data
mov edi, output_buffer
mov ecx, string_length
mov al, xor_key
decrypt_loop:mov bl, [esi]xor bl, al ; XOR解密mov [edi], blinc esiinc ediloop decrypt_loop; 动态调试技巧:在解密后设置内存断点
; 1. 找到解密函数
; 2. 在解密完成后(循环结束后)设置内存写入断点
; 3. 运行程序,当明文字符串被使用时触发断点
; 4. 查看明文字符串内容
通过深入理解这些汇编层面的字符串操作模式,逆向工程师可以更有效地分析程序逻辑、定位关键代码、识别安全漏洞,并对抗各种字符串混淆技术。