汇编基础:
二进制左移一位 相当于原来的2倍=X2,十六进制左移一位相当于原来的16倍=x16
十进制左移一位相当于扩大10倍
反过来二进制右移一位 相当于原来的1/2十六进制右移一位相当于原来的1/16
十进制左移一位相当于 原来的1/10
对内存的理解: 内存中的每个存储单元就相当于 ,图书馆里书架的每个格子,比如说有10行10列,每一行用
0-9标识,每一列也用0-9标识。RAM相当于一个矩阵 行列组合 当CPU要读取数据的时候,CPU会向地址总线传送行位和列位,在X86处理器中到达行和列的地址译码器。这就确定数据位置了然后从数据总线将数据传给CPU进
二进制正数转换负数方法
(1)正负表示方法
用字节的最高位表示:"1"表示"正","0"表示"负"
(2)计算机中数字是以哪个码储存的?
补码
(3) 负数 的二进制补码转换成十进制的方法
1、把补码“取反”(把二进制数的各位“1”换“0”,“0”换“1”。比如“101010”取反后为“010101”)
2、把取反后的二进制数“加1”
3、最后用常规的方法把“加1”后的二进制数转换为十进制数
第一章 基础知识
ASCII:美国信息交换标准码(American Standard Code for information Inerchange)
为8位 128个代码26个小写字母26个大写字母10个数字32个字符号33个控制代码和一个空格
intelCPU在内存中低位存入低地址 高位存入高地址
Unicode:是ASCII字符编码的一个扩展。称为"宽字符集"
API函数:它是区分字符集的 A代表ANSI是以单字节表示 W表示WideChars即Unicode是以双字节表示.其实USER32.DLL没有32位Messagebox函数入口点的, MessageboxA(ANSI版) MessageboxW(Unicode版) 编译器自己选择用哪个
MessageBoxEx 其实 在windows9x/2000/xp下直接调用这个函数
int MessageBoxEx(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType,
WORD wLanguageId //代表语言标识
);
在windows9x中大多使用ASCI来内部操作的 ,但是编程时可以调用MessageBoxW(Unicode)
在windows98中调用MessageBoxW, 其实内部最终还是会依次调用以下函数转换位ASCI的.
WideCharTOMultiByte()//取得字符串长度;GlobalAlloc//按字节长度分配内存,在调用WideCharTOMultiByte() 将字符串转换为ASCI字符串 紧接着还是调用ASCI版的MessageboxA来显示窗口, 然后在 GlobalFree释放内存.
在2000/xp中最终会使用unicode字符串 ,将调用的ASCI函数 转换为 Unicode的 ,例如调用MessageboxA 接下来会直接转换为Unicode 然后在调用MessageBoxW, 如果直接调用Unicode函数会减少系统开销 省去MessageBoxA的步骤
WIN32:用于32位版本的windows的API称作WIN32。
Windows注册表:
主键类型
HKEY_CLASSES_ROOT 简称 HKCR 包含了文件扩展名和 COM 组件类的注册信息。 HKEY_CURRENT_USER 简称 HKCU 包含了登陆用户相关的软件配置和参数 . HKEY_LOCAL_MACHINE 简称 HKLM 用来控制系统和软件的设置 HKEY_USERS 简称 HKU 包含关于动态加载的用户配置文件和默认的配置文件的信息 , 同时还包含了 HKEY_CURRENT_USER 中的信息 HKEY_CURRENT_CONFIG 包含 了启动时本地计算机系统使用的硬件配置文件和相关信息注册表相关函数: 包含于ADVAPI32.DLL中
打开子键
LONG RegOpenKeyEx(HKEY hkey, //要打开的主键句柄或标准项名
LPRCTSTR lpSubKEy,//要打开的子键名地址
DWORD ulOptions,//保留 ,必须为0
REGSAM samDesired,//存取掩码
PHKEY phkResult //存放打开子键句柄的地址
);
RegQueryValueEx 获取一个项的设置值
用Win32API操作windows注册表的基本步骤
用 RegOpenKey() 或 RegOpenKeyEx() 打开想要操作的主键获得一个句柄 将句柄传递给 RegQueryValueEx(),RegSetValueEX() 等函数来读写相应的键值 操作完毕后用 RegCloseKey() 关闭先前获得的句柄保护模式简介:
80x86(80386及其以后的各带CPU)可以在实模式,保护模式和虚拟86模式下运转,实模式实古老的MS-DOS运行环境 windows是保护模式下运行的
虚拟内存(virtual memory)
在保护模式下CPU的寻址方式内存是线性的,因为这时段寄存器的意义不同 段寄存器存放段选择子,只是全局描述表(Global Descriptor Table,简称GDT)或本地描述表(Local Descriptor Table,简称LDT)的一个指针。不同段寄存器有不同的属性(读,写,执行,特权级)
Win32每个进程都有属于自己的虚拟空间,32位进程地址空间是4GB,因为32位指针拥有0x00000000`0xFFFFFFFF之间任何一个地址。虚拟内存不是真正的内存,它通过映射(Map)的方法使可用的虚拟内存达到4GB,2GB用于程序,2GB用于系统。
虚拟内存实现的方法和过程:
程序被启动时,系统创建一个新进程,并分配给它 2GB 的虚拟地址 ( 只是地址 , 不是内存 ); 虚拟内存管理器 ( virtual memory Manager) 将程序代码映射到那个应用程序的虚拟地址中的某个位置,并把所有代码读取到物理地址中 其他项目 如堆栈 , 数据的空间是从物理内存中分配的 , 并映射到虚拟地址空间中保护模式的权限级别(privilege level):
最少分4层 ring0,ring1,ring2,ring3. ring0为最好权限 ,ring3最低
第二章代码分析技术
认识PE格式 区段
.text编译或者汇编结束产生的一种区段,内容是指令代码;
.rdata 运行期只读数据;
.data 初始化的数据段;
.idata 包含其外来DLL的函数及数据信息,即输入表;
.rsc 包含模块的全部资源,图标菜单。。等
PE在磁盘上的数据结构和在内存中是一致的,主要是将PE文件某地方映射到地址空间中
PE名词:
入口点(Entry Point) 程序执行入口
文件偏移地址(File offset):文件在磁盘上时各数据地址叫做文件偏移地址或者物理地址(RAW offset)
虚拟地址:所有程序访问存储器所使用的逻辑地址
基地址(Imagebase):映射到内存中的制定地址的初始值,不同编译器出来的程序基地址可能不同 大多可能固定不变
相对虚拟地址:RVA 是内存中相对于PE文件装入地址(基地址)的偏移量
相对虚拟地址(RVA)=虚拟内存地址(virual address)-基地址(Imagebase)
在x86系统中,每个内存分页的大小是4KB,0x1000字节,每个区段按0x1000之倍数的内存编译位置开始。如上图所示磁盘文件区段是0x400字节的倍数,在每个区段中多余的部分用0填充
公式为
File offset=RVA-OK
File offset = VA-Imagebase-OK
PE数据结构详解
PE起始为DOS头部 包括DOS MZ,DOS STUB(一个完整的DOS程序用于DOS系统)
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
PE文件头:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE文件标识 PE/0/0
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase; //装入内存时的VA
DWORD SectionAlignment;//装入内存时的 对齐值
DWORD FileAlignment;//文件对齐值
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;//装入内存后映像大小,从基地址到最后一块
DWORD SizeOfHeaders;//DOS头PE头 区块表总尺寸
DWORD CheckSum;//校验和 IMAGEENHLP.DLL中的CheckSumMappedFile可以计算,驱动系统DLL才需要这个值 默认为0
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//IMAGE_DATA_DIRECTORY组成的结构数组共16个
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
代码指令机器码的计算:
短转移(short jump):无条件转移和条件转移机器码都为2字节,转移范围为-128-到+127
长转移(long jump):无跳转转移机器码为5个字节,条件转移为6个字节.无条件转移要用1个字节表示转移条件,剩下4字节表示转移偏移,条件转移要用2个字节来当转移条件,剩下4表示转移偏移
子程序调用CALL:两种CALL一种是普通类似与长转移另一种是有寄存器参数堆栈等信息的
指令修改技巧:
逆向分析技术:
高级语言中子程序依赖堆栈来传递参数,也就是说调用者把参数压进堆栈,然后子程序在取出使用
调用约定是指调用例程时参数的传递顺序和约定平衡堆栈的程序。
实例:
比如 我们有这样一个C函数
#include<stdio.h>
long test(int a,int b)
{
a = a + 1;
b = b + 100;
return a + b;
}
void main()
{
printf("%d",test(1000,2000));
}
写成32位汇编就是这样
;//
.386
.model flat,stdcall ;这里我们用stdcall 就是函数参数 压栈的时候从最后一个开始压,和被调用函数负责清栈
option casemap:none ;区分大小写
includelib msvcrt.lib ;这里是引入类库 相当于 #include<stdio.h>了
printf PROTO C:DWORD,:VARARG ;这个就是声明一下我们要用的函数头,到时候 汇编程序会自动到msvcrt.lib里面找的了
;:VARARG 表后面的参数不确定 因为C就是这样的printf(const char *, ...);
;这样的函数要注意 不是被调用函数负责清栈 因为它本身不知道有多少个参数
;而是有调用者负责清栈 下面会详细说明
.data
szTextFmt BYTE '%d',0 ;这个是用来类型转换的,跟C的一样,字符用字节类型
a dword 1000 ;假设
b dword 2000 ;处理数值都用双字 没有int 跟long 的区别
;/
.code
_test proc ;A:DWORD,B:DWORD
push ebp
mov ebp,esp
mov eax,dword ptr ss:[ebp+8]
add eax,1
mov edx,dword ptr ss:[ebp+0Ch]
add edx,100
add eax,edx
pop ebp
retn 8
_test endp
_main proc
push dword ptr ds:b ;反汇编我们看到的b就不是b了而是一个[*****]数字 dword ptr 就是我们在ds(数据段)把[*****]
;开始的一个双字长数值取出来
push dword ptr ds:a ;跟她对应的还有 byte ptr ****就是取一个字节出来 比如这样 mov al,byte ptr ds:szTextFmt
;就把 % 取出来 而不包括 d
call _test
push eax ;假设push eax的地址是×××××
push offset szTextFmt
call printf
add esp,8
ret
_main endp
end _main
;// 下面介绍堆栈的变化
首先要明白的是 操作堆栈段 ss 只能用 esp或ebp寄存器 其他的寄存器eax ebx edx等都不能够用 而 esp永远指向堆栈栈顶 ebp用来 在堆栈段
里面寻址
push 指令是压栈 ESP=ESP-4
pop 指令是出栈 ESP=ESP+4
我们假设main函数一开始堆栈定是 ESP=400
push dword ptr ds:b ;ESP-4=396 ->里面的值就是 2000 就是b的数值
push dword ptr ds:a ;ESP-4=392 ->里面的值就是 1000 就是a的数值
call test ;ESP-4=388->里面的数值是什么?这个太重要了 就是我们用来找游戏函数的原理所在。
里面的数值就是call test 指令下一条指令的地址->即push eax的地址×××××
到了test函数里面
push ebp ;ESP-4=384->里面保存了当前ebp的值 而不是把ebp清零
mov ebp,esp ;这里ESP=384就没变化了,但是 ebp=esp=384,为什么要这样做呢 因为我们要用ebp到堆栈里面找参数
mov eax,dword ptr ss:[ebp+8] ;反汇编是这样的 想想为什么a就是[ebp+8]呢
;我们往上看看堆栈里地址392处就保存着a的值 这里ebp=384 加上8正好就是392了
;这样就把传递过来的1000拿了出来eax=1000
add eax,1 ;相当于 a+1了 eax=1001
mov edx,dword ptr ss:[ebp+0Ch] ; 0Ch=12 一样道理这里指向堆栈的地址是384+12=396 就是2000了 edx=2000
add edx,100 ;相当于 b+100 edx=2100
add eax,edx ;eax=eax+edx=1001+2100=3101 这里eax已经保存了最终的结果了
;因为win32汇编一般用eax返回结果 所以如果最终结果不是在eax里面的话 还要把它放到eax
;比如假设我的结果保存在变量nRet里面 最后还是要这样 mov eax,dword ptr nRet
pop ebp ;ESP=384+4=388 而保存在栈顶384的值 保存到 ebp中 即恢复ebp原来的值
;因为一开始我们就把ebp的值压栈了,mov ebp,esp已经改变了ebp的值,这里恢复就是保证了堆栈平衡
retn 8 ;ESP+8->396 这里retn是由系统调用的 我们不用管 系统会自动把EIP指针指向 原来的call的下一条指令
;由于是系统自动恢复了call那里的压栈所以 真正返回到的时候ESP+4就是恢复了call压栈的堆栈
;到了这个时候 ESP=400 就是函数调用开始的堆栈,就是说函数调用前跟函数调用后的堆栈是一样的
;这就是堆栈平衡
由于我们用stdcall上面retn 8就是被调用者负责恢复堆栈的意思了,函数test是被调用者,所以负责把堆栈加8,call 那里是系统自动恢复的
push eax ;ESP-4=396->里面保存了eax的值3101
;上面已经看到了eax保存着返回值,我们要把它传给printf也是通过堆栈传递
push offset szTextFmt ;ESP-4=392->里面保存了szTextFmt的地址 也就是C里面的指针 实际上没有什么把字符串传递的,我们传的都是地址
;无论是在汇编或C 所以在汇编里没有什么字符串类型 用最多的就是DWORD。嘿嘿游戏里面传递参数 简单多了
call printf ;ESP-4=388->里面保存了下一条指令的地址
add esp,8 ;ESP+8=400 恢复了调用printf前的堆栈状态
;上面说了由于printf后面参数是:VARARG 这样的类型是有调用者恢复堆栈的 所以printf里面没有retn 8之类的指令
;这是由调用者负责清栈 main是调用者 所以下面一句就是 add esp,8 把堆栈恢复到调用printf之前
;而call printf那里的压栈 是由系统做的 恢复的工作也是系统完成 我们不用理 只是知道里面保存是返回地址就够
;了
ret ;main 函数返回 其他的事情是系统自动搞定 我们不用理 任务完成
动态分析技术:
OD调试器
使用符号库( lib ),可以让 Od 以函数名显示 DLL 中的函数,例如 MFC42.DLL 是以序号输出函数的,要让以函数名显示相关输出函数。加载方法单击菜单“ Debug/Select import libraries ”来打开导入库窗口。 将 OD 附加到一个正在运行的程序上,附加后程序会停在 NTDLL.DLL 的 DbgBreakPoint 处。然后安 shift+F9 继续运行来调试。如果附加不成功 , 可以巧妙利用 OD 的即使调试器功能来调试。 调试隐藏进程:用类似冰刃的程序得到隐藏进程 PID ,然后在控制台窗口用 -p 参数附加即可,注意 PID 是十进制 c:\ollydbg.exe –p pid如果附加不成功 可以利用OD即时调试器 例子:运行A.exe,其会调用B。exe,此时用OD附加B.exe,OD会无响应。解决办法:在“Options/Jus-in-time”中设置OD为即时调试器,将B.exe的入口改成CC,即INT3指令,同时记下原指令。运行A.exe,其调用B.exe,运行到INT3指令会导致异常,OD会作为即时调试器启动并加载B.exe,此时再将INT3指令恢复原指令,继续调试。
按 CTRL+N 可以打开当前领空的函数输入表。进入一个 CALL 之后 按 CTRL+F9 执行到返回该领空的调用该 CALL 的空间。按 ALT+ F9 可以退出 DLL 调用的领空到本程序调用该 CALL 的空间 . INT3 断点,直接按 F2 下断,其实就是 CC 指令,机器码是 CCH 。不过是被 OD 隐藏了,缺点是有些程序会检测防范 API 被下断,检查 API 的首地址是否为 CCH ,解决办法:断点下在函数内部或底部 或者不在入口下断 硬件断点,跟 DRx 调试寄存器有关系 总过有 8 个 DR0-DR3 :调试地址寄存器,保存需要监视的地址,如设置硬件断点。 DR4—DR5 :保留。 DR6 :调试寄存器组状态寄存器。 DR7 :调试寄存器组控制寄存器。硬件断点只用 DR0-DR3 4 个寄存器 所以只能设置 4 个硬件断点。 内存断点可以一次性的 查看 ALT+M 在访问上设置断点,执行中断后,断点就被删除。 消息断点:运行程序之后 在操作窗口之后,按 ALT+W 就会显示窗口各类参数,比如 BUTTON , EDIT 在上面右键之后 选择想要断的消息。注意:断到消息地址之后 不能用 ALT+F9 或者 CTRL+F9 执行返回 要用到一次性内存断点之后,系统底层就会通过它返回到程序领空 条件断点根据条件来设置断点 用命令行 bp eax == 0400000 或者用 shift+F2 在代码上输入命令 按存储器条件中断 位于《加密与解密 第三版》 P37 例如输入命令 bp CreateFileA,[STRING [esp+4]] == “c: \\1212.txt ” 条件记录断点看 P38