本文会使用IDA分析局部变量,全局变量在内存的存储
目录
使用IDA分析局部变量
使用IDA分析全局变量
总结
使用IDA分析局部变量
#include <stdio.h>
int main()
{
int nNum = 1;
float fNum = 2.5;
char ch = 'A';
printf("int %d, float %f, char %c", nNum, fNum, ch);
return 0;
}
使用IDA跟进到main函数,F5调出反汇编
摁下tab键,进入汇编界面,Rename函数名为main
Rename函数名为main
CODE XREF: 当前函数被一个或多个地方引用,摁下CTRL+X可以查看调用main()的地方
局部变量汇编分析
.text:00411760 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00411760 main proc near ; CODE XREF: j_main↑j
.text:00411760
.text:00411760 var_FC = qword ptr -0FCh
.text:00411760 var_24 = byte ptr -24h
.text:00411760 var_1D = byte ptr -1Dh
.text:00411760 var_14 = dword ptr -14h
.text:00411760 var_8 = dword ptr -8
.text:00411760 argc = dword ptr 8
.text:00411760 argv = dword ptr 0Ch
.text:00411760 envp = dword ptr 10h
.text:00411760
.text:00411760 push ebp ; 把调用者函数的EBP压入堆栈
.text:00411761 mov ebp, esp ; 开辟栈帧
.text:00411763 sub esp, 0E4h ; 开辟函数局部变量空间
.text:00411769 push ebx ; 保存易失性寄存器
.text:0041176A push esi
.text:0041176B push edi
.text:0041176C lea edi, [ebp+var_24] ; ebp+var_24 <==> ebp-24h lea取局部变量地址到edi EDI = EBP-36 36个字节要初始化作为局部变量的空间
.text:0041176F mov ecx, 9
.text:00411774 mov eax, 0CCCCCCCCh
.text:00411779 rep stosd
.text:0041177B mov ecx, offset unk_41C003
.text:00411780 call sub_41130C ; CheckForDebuggerJustMyCode VS系统函数
.text:00411785 mov [ebp+var_8], 1 ; int nNum = 1
.text:0041178C movss xmm0, ds:dword_417BE8 ; xmm0寄存器用于处理浮点数,2.5存入寄存器
.text:00411794 movss [ebp+var_14], xmm0 ; float fNum = 2.5
.text:00411799 mov [ebp+var_1D], 41h ; char ch ='A'
.text:0041179D movsx eax, [ebp+var_1D] ; 把ch扩展为4个字节,为压栈做准备
.text:004117A1 push eax ; ch压栈
.text:004117A2 cvtss2sd xmm0, [ebp+var_14] ; float拓展为double
.text:004117A7 sub esp, 8 ; 开辟堆栈空间
.text:004117AA movsd [esp+0FCh+var_FC], xmm0 ; double入栈
.text:004117AF mov ecx, [ebp+var_8]
.text:004117B2 push ecx ; 把nNum的值保存到ecx后入栈
.text:004117B3 push offset aIntDFloatFChar ; 格式化字符串后入栈 offset取地址4个字节大小
.text:004117B8 call sub_4113A2 ; 调用printf函数
.text:004117BD add esp, 14h ; 平衡堆栈
.text:004117C0 xor eax, eax ; 把eax清0,等价于 return 0
.text:004117C2 pop edi ; 恢复易失性寄存器
.text:004117C3 pop esi
.text:004117C4 pop ebx
.text:004117C5 add esp, 0E4h ; 清理main函数开辟的局部变量空间
.text:004117CB cmp ebp, esp
.text:004117CD call sub_411230 ; CheckEsp() 用于在程序的运行时检查堆栈指针是否被篡改
.text:004117D2 mov esp, ebp ; 恢复栈帧
.text:004117D4 pop ebp
.text:004117D5 retn ; 将程序的控制流转移到调用它的指令的下一条指令
.text:004117D5 main endp
main函数主体堆栈,堆栈存储的是函数的局部变量,函数参数
- push EBP EBP存储当前函数的栈底地址,调用者函数栈底地址压入堆栈
- mov EBP,ESP 开辟栈帧
- sub esp, 0xXXX 为函数局部变量开辟空间
- push ebx,esi,edi 保存易失性寄存器
- 为局部变量空间堆栈初始化为0xCC
- 如果main函数有新的局部变量或者调用函数,都会引发堆栈变化
- main函数执行完毕开始恢复堆栈
- pop 寄存器 恢复易失性寄存器
- add esp,0xXX 恢复局部变量的空间
- mov esp,ebp ESP=EBP=函数调用者的栈底地址
- pop ebp 恢复原先的栈底
- retn 将执行main后下一条汇编指令地址给EIP寄存器,恢复程序控制流转,也恢复了原先的栈顶
一些细节:
rep stosd:完成对局部变量的初始化
.text:00411769 push ebx ; 保存易失性寄存器
.text:0041176A push esi
.text:0041176B push edi
.text:0041176C lea edi, [ebp+var_24] ; ebp+var_24 <==> ebp-24h lea取局部变量地址到edi
.text:0041176F mov ecx, 9
.text:00411774 mov eax, 0CCCCCCCCh
.text:00411779 rep stosd ; 把9个栈空间初始化为0xCC
汇编:
- rep 重复操作,重复操作的次数是ecx的值
- stosd:把EAX值复制到EDI寄存器指向的地址,一次复制4个字节
- stosw:把EAX值复制到EDI寄存器指向的地址,一个复制2个字节
- stosb:把EAX值复制到EDI寄存器指向的地址,一次复制1个字节
之前的操作中,保存易失性寄存器,每个寄存器8个字节,共计24字节
lea edi, [ebp+var_24] ; ebp+var_24 <==> ebp-24h edi寄存器指向main函数局部变量空间的尾地址
下面代码,给局部变量空间初始化
000F176F B9 09 00 00 00 mov ecx,9
000F1774 B8 CC CC CC CC mov eax,0CCCCCCCCh
000F1779 F3 AB rep stos dword ptr es:[edi]
"rep stos dword ptr es:[edi]" 的执行步骤如下:
- 使用ES:EDI指定目标内存区域的起始地址。
- 使用ECX指定要重复执行的次数。
- 将EAX中的值作为DWORD数据写入ES:[EDI]指向的内存位置,然后根据DF(方向标志位)决定EDI是递增还是递减,以定位下一个内存位置。
- 重复步骤3,直到ECX达到零,或根据DF决定的方向不再满足条件。
因此,"rep stos dword ptr es:[edi]" 指令将会将EAX寄存器中的DWORD数据连续地写入以ES:[EDI]为起点的目标内存区域,并且重复执行次数由ECX指定。这通常用于快速地内存初始化或者进行内存拷贝操作。
但是在64位下,不再使用ES:EDI模式寻址,64位模式下采用了平坦模型(Flat Model),所有线性地址都直接映射到相同的物理地址空间,即,你可以直接使用RDI寄存器作为指针来操作内存地址,而无需使用ES寄存器进行段寻址。
在x64dbg下验证
DF(Direction Flag)是x86处理器的一个标志位,用于指示字符串操作的方向。
当DF标志位被设置为0时,字符串操作是从低地址向高地址进行的,也就是正向(forward)操作。例如,使用REP MOVS指令将数据从源内存区域复制到目标内存区域时,数据会按照从源地址递增到目标地址的顺序进行复制。
当DF标志位被设置为1时,字符串操作是从高地址向低地址进行的,也就是反向(backward)操作。例如,使用REP MOVS指令将数据从目标内存区域复制到源内存区域时,数据会按照从源地址递减到目标地址的顺序进行复制。
得以验证在64位下,直接可以通过EDI寻址操作,不再使用ES:EDI寻址
为什么要每个字节初始化为0xCC
0xCC表示int33异常 0xCC指令是软中断指令,它会导致CPU进入一个中断处理例程。如果程序在调用函数时忘记初始化局部变量,那么在访问这些未初始化的变量时,CPU会立即进入一个中断处理例程,程序会停止运行,并且IDE或调试器会提示有异常产生。
这个地址存储的是浮点数
可以在Edit-->Operand type,选择合适的数据类型,正确的话,就会把16进制数据转换为对应的值。选择floatinteresting point
常见的操作数大小指定符号:
byte ptr
:指定一个数据操作数为8位字节(byte)。word ptr
:指定一个数据操作数为16位字(word)。- dword ptr:指定一个数据操作数为32位字 (dword)
mmword ptr
是一个指示符,指示使用 64 位内存操作。
下面这两句代码在不同的工具中解析虽然不同,但意思一样。估计IDA 7.0 解析时是按照x86,而VS2019是按照x64,x64中已经不使用DS:EDI寻址了
VS2019: movss xmm0,dword ptr [__real@40200000 (0AF7BE8h)]
IDA: movss xmm0, ds:dword_417BE8
dword ptr [__real@40200000 (0AF7BE8h)] = ds:dword_417BE8 都表示以后面地址为首,取四个字节的数据
xmm0
是 XMM 寄存器中的第一个寄存器- 在 x86 指令集中,有 8 个 xmm 寄存器(xmm0~xmm7),每个寄存器的大小为 128 位(16 字节)
- movss:将一个单精度浮点数(32 位)从源操作数移动到目标操作数。
- movsx:将源操作数进行符号扩展后移动到目标操作数,源:8位,目标:16位,从低位存,高 8 位设置为源操作数的符号位,不改变大小,拓展位数 默认情况下,内存操作数被假定为 32 位的双字(DWORD)数据类型。
- cvtss2sd:将单精度浮点数(4)转换为双精度浮点数(8)
- xor:用于执行两个二进制数按位异或操作。当两个对应位的值不同时,结果位为 1,否则为 0。
cmp指令:用于比较两个操作数的大小关系,并根据比较结果设置标志位。cmp
指令会将 operand1
减去 operand2
的值,并根据结果设置标志位寄存器(如零标志位、符号标志位等)。
- 零标志位(ZF):如果两个操作数相等,则 ZF 被设置为 1;否则被设置为 0。
- 符号标志位(SF):如果结果为负数,则 SF 被设置为 1;否则被设置为 0。
- 位标志位(CF):用于无符号数比较,如果
operand1
小于operand2
,则 CF 被设置为 1;否则被设置为 0。
使用IDA分析全局变量
#include <stdio.h>
int nNum = 1;
float fNum = 2.5;
char ch = 'A';
int main()
{
printf("int %d, float %f, char %c", nNum, fNum, ch);
return 0;
}
局部变量汇编分析
.text:00411760 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00411760 main proc near ; CODE XREF: j_main↑j
.text:00411760
.text:00411760 var_D8 = qword ptr -0D8h
.text:00411760 argc = dword ptr 8
.text:00411760 argv = dword ptr 0Ch
.text:00411760 envp = dword ptr 10h
.text:00411760
.text:00411760 push ebp
.text:00411761 mov ebp, esp ; 开辟栈帧
.text:00411763 sub esp, 0C0h ; 给局部变量开辟空间
.text:00411769 push ebx ; 保存易失性寄存器
.text:0041176A push esi
.text:0041176B push edi
.text:0041176C mov edi, ebp ; 从EDI从低地址向高地址初始化局部变量空间
.text:0041176E xor ecx, ecx
.text:00411770 mov eax, 0CCCCCCCCh
.text:00411775 rep stosd
.text:00411777 mov ecx, offset unk_41C003
.text:0041177C call sub_41130C
.text:00411781 movsx eax, byte_41A03C ; 把字符拓展为4字节,压入堆栈
.text:00411788 push eax
.text:00411789 cvtss2sd xmm0, dword_41A038 ; 把float拓展为double
.text:00411791 sub esp, 8
.text:00411794 movsd [esp+0D8h+var_D8], xmm0 ; 把xmm0压入堆栈 这种压入堆栈方式相当于先抬升,esp变小,在压入xmm,从低地址填充到高地址
.text:00411799 mov ecx, dword_41A034
.text:0041179F push ecx
.text:004117A0 push offset aIntDFloatFChar ; 压入的是字符串的地址,共计四个字节
.text:004117A5 call sub_4113A2
.text:004117AA add esp, 14h ; ch(4) + xmm0(8) + ecx(4) + 字符串地址(4) = 20字节 = 0x14 平衡堆栈
.text:004117AD xor eax, eax ; 清0 eax,相当于 return 0
.text:004117AF pop edi ; 恢复易失性寄存器
.text:004117B0 pop esi
.text:004117B1 pop ebx
.text:004117B2 add esp, 0C0h ; 恢复函数局部变量空间
.text:004117B8 cmp ebp, esp
.text:004117BA call sub_411230 ; CheckEsp检查当前函数栈帧是否被破坏
.text:004117BF mov esp, ebp ; 恢复栈帧
.text:004117C1 pop ebp
.text:004117C2 retn
.text:004117C2 main endp
总结
- 全局变量保存在data段,在程序编译链接就确定了;局部变量保存在stack段
- 访问局部变量往往是通过[ebp-0xXXX],全局变量往往是一个地址[0xXXX]
- 把局部变量或者全局变量压入堆栈需要使用寄存器
- 给局部变量赋值,mov [esp-0xXXX],值
- 函数有返回值往往借助寄存器,值或者地址