Windows程序字符串处理与逆向分析
文章目录
- Windows程序字符串处理与逆向分析
-
- [1. 字符串类型](#1. 字符串类型)
-
- [1.1 C风格字符串 (char*)](#1.1 C风格字符串 (char*))
- [1.2 宽字符字符串 (wchar_t*)](#1.2 宽字符字符串 (wchar_t*))
- [1.3 C++标准字符串](#1.3 C++标准字符串)
- [2. 字符串处理函数](#2. 字符串处理函数)
-
- [2.1 字符串长度计算 - strlen](#2.1 字符串长度计算 - strlen)
- [2.2 字符串复制 - strcpy/strncpy](#2.2 字符串复制 - strcpy/strncpy)
- [2.3 字符串比较 - strcmp](#2.3 字符串比较 - strcmp)
- [2.4 字符串搜索 - strstr](#2.4 字符串搜索 - strstr)
- [3. 字符串存储方式](#3. 字符串存储方式)
-
- [3.1 不同存储位置的汇编特征](#3.1 不同存储位置的汇编特征)
- [4. 安全与混淆技术](#4. 安全与混淆技术)
-
- [4.1 字符串加密的汇编实现](#4.1 字符串加密的汇编实现)
- [4.2 安全字符串处理的汇编对比](#4.2 安全字符串处理的汇编对比)
- 逆向工程实战技巧
1. 字符串类型
1.1 C风格字符串 (char*)
代码示例:
c
#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);
}
汇编层详细分析:
assembly
; 编译后在.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*)
代码示例:
c
#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'存储为0x0048
mov 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);
}
宽字符串汇编细节:
assembly
; 宽字符串在内存中的实际存储
_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++标准字符串
代码示例:
cpp
#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逆向分析特征:
assembly
; 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
汇编层详细解释:
c
#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 = 0
dec 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 -> 0x00000001
dec ecx ; ECX--,得到实际长度
mov len, ecx
; === 指令细节 ===
; REPNE: Repeat While Not Equal - 当ZF=0且ECX≠0时重复
; SCASB: Scan String Byte - 比较AL与[EDI],并设置EDI++
; 最终EDI指向终止字节后一个位置,ECX包含剩余计数
}
}
strlen的多种汇编实现:
assembly
; 方法1:使用repne scasb(编译器优化版本)
strlen_repne:
mov edi, [esp+4] ; 字符串指针
mov ecx, -1 ; 最大计数
xor eax, eax ; 搜索0字节
repne scasb ; 扫描直到找到0
not ecx ; 计算长度
dec ecx
mov eax, ecx
ret
; 方法2:手动循环(调试版本更易读)
strlen_loop:
mov edx, [esp+4] ; 字符串指针
xor eax, eax ; 长度计数器
scan_loop:
cmp byte ptr [edx+eax], 0 ; 检查当前字符是否为0
je found_end ; 如果是,跳转到结束
inc eax ; 否则计数器+1
jmp 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
汇编层详细解释:
c
#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 ; 检查刚复制的字符是否为0
jnz copy_loop ; 如果不是0,继续循环
; === 优化版本使用rep movsb ===
mov esi, offset src
mov edi, offset dest
mov ecx, length_of_src ; 如果知道长度,可以更快
rep movsb ; 重复复制ECX次
; === 安全风险分析 ===
; 如果src长度 > dest缓冲区大小,会导致缓冲区溢出
; 攻击者可能覆盖返回地址或重要数据
}
}
strcpy安全漏洞的汇编表现:
assembly
; 不安全的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
汇编层详细解释:
c
#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 edi
jmp compare_loop ; 继续比较
equal:
xor eax, eax ; 返回0(相等)
jmp done
different:
sbb eax, eax ; 巧妙设置返回值
or al, 1 ; 如果不相等,返回-1或1
; 具体:如果AL<BL,返回-1;如果AL>BL,返回1
done:
mov result, eax
; === 优化版本使用repe cmpsb ===
mov esi, offset str1
mov edi, offset str2
mov ecx, 0FFFFFFFFh ; 最大比较长度
repe cmpsb ; 重复比较直到不相等
; 结束后:[ESI-1]和[EDI-1]是不相等的字符位置
}
}
strcmp在逆向工程中的关键作用:
assembly
; 序列号检查的典型模式
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, al
jz access_granted ; 同时到达结尾,验证通过
inc esi
inc edi
jmp compare_loop
access_granted:
; 验证成功的代码
access_denied:
; 验证失败的代码
2.4 字符串搜索 - strstr
汇编层详细解释:
c
#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_inner
next_char_pop:
pop edi ; 恢复寄存器
pop esi
next_char:
inc esi ; 主字符串位置前进
jmp outer_loop
found_substring:
pop edi ; 清理栈
pop edi ; 注意:这里弹出但不恢复ESI
mov found, esi ; 返回匹配开始位置
jmp done
not_found:
mov found, 0 ; 返回NULL
done:
; === 性能考虑 ===
; 实际实现会使用更高效的算法如Boyer-Moore
; 但基本原理相同:外层循环主串,内层循环比较子串
}
}
3. 字符串存储方式
3.1 不同存储位置的汇编特征
详细汇编分析:
c
#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, 00403000h
mov 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);
}
各存储段的具体汇编表现:
assembly
; 编译后的内存布局示例
.data ; 可读写数据段
global_str db 'Global String',0 ; 00403000
.rdata ; 只读数据段
const_str dd offset aConstant ; 00404000
aConstant db 'Constant String',0 ; 00404010
.code ; 代码段
demonstrate_storage proc
push ebp
mov ebp, esp
sub esp, 40h ; 为局部变量分配栈空间
; 栈变量:stack_str在[ebp-14h]
lea eax, [ebp-14h]
mov byte ptr [eax], 'S' ; 初始化栈字符串
; 堆分配调用
push 32h ; 50字节
call malloc
add esp, 4
mov [ebp+heap_str], eax ; 保存堆指针
; 访问示例
mov ecx, offset global_str ; 全局变量直接地址
mov edx, [ebp+heap_str] ; 堆变量通过指针
lea eax, [ebp-14h] ; 栈变量通过EBP偏移
mov esp, ebp
pop ebp
ret
demonstrate_storage endp
4. 安全与混淆技术
4.1 字符串加密的汇编实现
详细汇编解释:
c
#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 ^ DL
mov [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 GetModuleHandleA
mov hUser32, eax
push offset aMessageBoxA ; "MessageBoxA"
push eax ; user32.dll句柄
call GetProcAddress ; 动态获取函数地址
; 调用动态获取的API
push 0 ; uType
push offset aTitle ; lpCaption
push secret ; lpText
push 0 ; hWnd
call eax ; 调用MessageBoxA
}
free(secret);
}
逆向工程中识别加密字符串:
assembly
; 加密字符串在数据段的表现
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], bl
inc esi
inc edi
loop decrypt_loop
; 动态API调用的识别
call GetModuleHandleA ; 获取DLL句柄
push offset api_name ; API函数名
push eax ; DLL句柄
call GetProcAddress ; 获取函数地址
; 之后通过寄存器或栈间接调用
4.2 安全字符串处理的汇编对比
安全vs不安全实现的汇编对比:
c
// 不安全版本
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';
}
对应的汇编代码对比:
assembly
; 不安全版本的汇编
unsafe_copy:
push ebp
mov ebp, esp
sub esp, 10h ; 分配16字节栈空间
; 危险的strcpy调用
push [ebp+8] ; input参数
lea eax, [ebp-10h] ; buffer地址
push eax
call strcpy ; 没有长度限制!
add esp, 8
mov esp, ebp
pop ebp
ret
; 安全版本的汇编
safe_copy:
push ebp
mov ebp, esp
sub esp, 10h ; 分配16字节栈空间
; 安全的strncpy调用
push 0Fh ; 最大15字符
push [ebp+8] ; input参数
lea eax, [ebp-10h] ; buffer地址
push eax
call strncpy ; 有长度限制
add esp, 0Ch
; 确保终止符
mov byte ptr [ebp-1], 0 ; buffer[15] = 0
mov esp, ebp
pop ebp
ret
逆向工程实战技巧
识别字符串操作的启发式方法
在IDA Pro中的分析模式:
assembly
; 模式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_char
cmp al, 'z'
ja skip_char
sub al, 20h ; 小写转大写
mov [esi], al
skip_char:
inc esi
test al, al
jnz process_loop
字符串混淆的对抗技术
识别和解决字符串混淆:
assembly
; 加密字符串的识别特征
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], bl
inc esi
inc edi
loop decrypt_loop
; 动态调试技巧:在解密后设置内存断点
; 1. 找到解密函数
; 2. 在解密完成后(循环结束后)设置内存写入断点
; 3. 运行程序,当明文字符串被使用时触发断点
; 4. 查看明文字符串内容
通过深入理解这些汇编层面的字符串操作模式,逆向工程师可以更有效地分析程序逻辑、定位关键代码、识别安全漏洞,并对抗各种字符串混淆技术。