suibiankankan
基础知识
分析的一般途径和策略
- 学会软件的操作和使用方法 $\to$ 推测出软件的设计思想和编程思路
- 静态分析:阅读反汇编的程序清单,利用人机交互的提示信息了解片段所完成的功能,宏观了解软件的编程思路
- 动态跟踪:首先完成反反调试,并解密加密程序,了解初始化工作,获得各个模块之间的中间结果
- 粗跟踪:不跟踪调用等指令,仅根据执行结果分析程序的功能
- 细跟踪:针对性跟踪分析关键模块
文本字符
ASCII 和 Unicode
Unicode 是 ASCII 的扩展,所有字符都是 16 位
字节存储顺序
小端序(Little-endian):高位字节存入高地址,低位字节存入低地址
大端序(Big-endian):高位字节存入低地址,低位字节存入高地址
字节序 | 0 | 1 | 2 | 3 |
---|---|---|---|---|
大端序 | 12 | 34 | 56 | 78 |
小端序 | 78 | 56 | 34 | 12 |
Windows
Win 32 API
32位API与64位API在名称和功能上基本没有变化
Windows运转核心为DLL动态链接库
- KERNEL32.DLL:操作系统核心功能服务,进程与线程控制、内存管理、文件访问等
- USER32.DLL:负责处理用户接口,包括键盘和鼠标输入、窗口和菜单管理等
- GDI:图形设备接口,允许程序在屏幕和打印机上显示文本和图形
Windows消息机制
Windows使用Message提供应用程序与应用程序、应用程序与操作系统之间的通信
常用的函数如下:
SendMessage
调用一个窗口的窗口函数,将一条消息发送给那个窗口。除非消息处理完毕,否则不会返回
LRESULT SendMessage(
HWND hwnd, // 目的窗口的句柄
UINT Msg, // 消息标识符
WPARAM wParam, // 消息的WPARAM域
LPARAM lParam // 消息的LPARAM域
);
返回值:消息投递成功,返回非零
WM_COMMAND
当用户从菜单或按钮中选择一条命令或者一个控件时该消息被发送给它的父窗口,或者当一个快捷键被释放时发送该消息。
WM_COMMAND
wNotifyCode = HIWORD(wParam); // 通告函数
wID = LOWORD(wParam); // 菜单条目、控件或快捷键的标识符
hwndCtl = (HWND) lParam; // 控件句柄
返回值:如果应用程序处理这条消息,则返回值为零
WM_DESTORY
当一个窗口被销毁时发送该消息。该消息对应 0x02
,没有参数
返回值:如果应用程序处理这条消息,则返回值为零
WM_GETTEXT
当需要将一个窗口的文本复制到一个由呼叫程序提供的缓冲区中时,发送该消息。该消息对应 0x0D
WM_GETTEXT
wParam = (WPARAM) cchTextMax; // 需要复制的字符数
lParam = (LPARAM) lpszText; // 接收文本的缓冲区地址
返回值:被复制的字符数
WM_QUIT
当应用程序调用 PostQuitMessage
时,生成 WM_QUIT
消息,对应 0x12
WM_QUIT
nExitCode = (int) wParam; // 退出代码
无返回值
WM_LBUTTONDOWN
光标停在窗口客户区且点击左键时,发送此消息
如果鼠标未捕捉,将下发给光标下的窗口,否则发送给捕获鼠标动作的窗口
对应 0x201
WM_LBUTTONDOWN
fwkeys = wParam; // key旗帜
xPos = LOWORD(lParam); // 光标的水平位置
yPos = HIWORD(lParam); // 光标的垂直位置
返回值:如果应用程序处理了这条消息,返回值为零
虚拟内存
- 应用程序不会直接访问物理地址
- 虚拟内存管理器通过虚拟地址的访问请求来控制所有的物理地址访问
- 每个应用都有独立的寻址空间,不同应用程序的地址空间是彼此隔离的
- DLL程序没有私有空间,总是被映射到其他应用程序的地址空间中,作为程序的一部分运行
动态分析技术
逆向分析技术
Win32
启动
程序先执行启动代码,随后调用 WinMain
函数
实例中的系统调用:
Call KERNEL32.GetVersion ; 确定Windows系统版本
Call KERNEL32.GetCommandLineA ; 指向系统的完整命令行的指针
Call KERNEL32.GetStartupInfoA ; 获取一个进程的启动信息
Call KERNEL32.GetModuleHandleA ; 返回进程地址空间执行文件基地址
call 00401000 ; 调用WinMain
call 004012EC ; 退出程序
ret
通常无需关注启动,直接查看
WinMain
即可
函数调用
编译器通常使用 call
和 ret
指令来调用函数
call
指令将其之后的指令地址压入栈顶,ret
指令则返回到调用位置
有时可能利用寄存器进行间接调用,如: call eax
参数传递
栈
调用函数时,将参数压入栈中
对于不同语言,有不同的调用约定
类型 | C/C++(__cdecl) | pascal | stdcall | fastcall |
---|---|---|---|---|
参数传递顺序 | 从右向左 | 从左向右 | 从右向左 | 使用寄存器和栈 |
平衡栈 | 调用者 | 子程序 | 子程序 | 子程序 |
VARARG | 是 | 否 | 允许* |
VARARG表示参数个数可以不确定
stdcall中,如果参数个数不确定,需要由调用程序来平衡栈
程序执行过程:
- 调用者将函数执行完毕时应返回的地址、参数压入栈
- 函数使用
ebp
指针+偏移量对栈中的参数进行寻址并取出,完成操作 - 子程序使用
ret
或retf
指令返回,eip
置为栈中保存的地址,并继续执行
栈的建立过程(两个参数时):
- 先将
arg2
压栈,esp=K-04h
- 将
arg1
压栈,esp=K-08h
- 执行
call
,把返回地址压栈,esp=K-0Ch
- 为了程序能够恢复,将
ebp
压栈,esp=K-10h
move ebp, esp
,将当前的栈顶设置为栈底sub esp, 8
,定义局部变量,两个变量分别为[esp-4]
和[esp-8]
- 函数结束时,
add esp, 8
释放局部变量占用,或者使用ret 8
来释放
还可以用enter和leave指令维护
enter
指令:push ebp
,mov ebp, esp
,sub esp, xxx
leave
指令:add esp, xxx
,pop ebp
寄存器
通常遵循 fastcall
规范
- VC++:左边两个参数分别存入
ecx
,edx
中,其余压栈 - Borland Delphi/C++:左边三个参数分别存入
eax
,edx
,ecx
中,其余按PASCAL方式压栈
C++非静态类成员默认调用 thiscall
,对象的每个函数隐含接受 this
参数,使用 eax
存放,其余参数从右到左压栈
名称修饰约定
为了操作符和函数重载,C++编译器会按照规则修改入口点的符号名,从而允许同一个名字有多个用法。
C的规则如下:
- stdcall调用约定在输出函数名前加下划线,在后面加@,格式为
_functionname@number
- __cdecl调用约定格式为
_functionname
- Fastcall调用约定格式位
@functionname@number
均不改变大小写
C++规则如下:
- stdcall调用约定以
?
开头,函数名后以@@YG
标识参数表开始,后跟参数表,参数表第一项位返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前,参数表后,以@Z
标识整个名字的结束,若无参数,则以Z
结束。格式为?functionname@@YG******@Z
或?functionname@@YG*XZ
- __cdecl调用将
@@YG
替换为@@YA
- Fastcall调用将
@@YG
替换为@@YI
返回值
return操作返回
存放在 eax
寄存器中,高32位存放在 edx
中
传引用方式返回
传引用调用方式将变量的地址传递给函数,可以在子函数中修改该内存单元中变量的值,因此允许修改原始变量
数据结构
局部变量
函数内部定义的一个变量,作用域和生命周期仅局限于该函数内
栈存放
先将参数压入栈中,再修改 ebp
,最后减小 esp
。因此 [ebp+**h]
表示参数, [ebp-**h]
表示局部变量
寄存器存放
有 6 个通用寄存器尽可能有效地存放局部变量, 因此需要注意确定当前的寄存器中存储的变量是哪个变量
全局变量
局部变量存放在栈中,而全局变量存放在内存区中
版本标记等常数通常为全局变量
全局变量通常存放在数据区块 .data
的一个固定地址处,程序使用固定的硬编码地址进行寻址
如果在只读区块,说明是一个常量
数组
一般通过基址+变址实现寻址,如:
mov eax, [407030h + eax]
间接寻址一般用于给数组和结构赋值,[base+n]
根据n的不同对结构中的相应单元赋值。
0040101D lea esi, dword ptr [esp+8]
00401021 mov edi, 3
00401026 mov eas, dword ptr [esi]
...
00401036 add esi, 4
00401039 dec edi
0040103A jnz short 00401026
虚函数
C++面向对象中,最重要的概念就是虚函数
虚函数是程序运行时定义的函数,其地址不能在编译时确定,只能在调用即将进行时确定。虚函数的引用存放在专用数组——虚函数表(Virtual Table,VTBL)中。
调用时首先通过虚函数指针找到虚函数表的地址,然后在虚函数表中找到该函数的入口地址,最后进行调用。
控制语句
if-else
汇编形式通常为
cmp a, bjz 0040xxxxh ; (jnz)
可以用 test eax, eax
替代 cmp
,该语句(相当于逻辑与运算)表示当 eax
为 0 时,设置 ZF
为 1,jz
则表示 ZF
位为 1 时跳转
switch-case
无优化版本
0040101D cmp [ebp-08], 01 ; case 100401021 je 0040103100401023 cmp [ebp-08], 02 ; case 200401027 je 0040104000401029 cmp [ebp-08], 0A ; case 100040102D je 0040104F0040102F jmp 0040105E ; default
使用 dec
指令替代 cmp
mov eax, [esp+08]dec eax ; case 1je 0040xxxxdec eax ; case 2je 0040xxxxsub eax, 00000008 ; case 10je 0040xxxx
跳转表实现(case的取值为算术级数时)
jmp dword ptr [4*eax+004010B0] ; 跳转表
转移指令机器码计算
位移量=目的地址-起始地址-跳转指令长度
转移指令机器码=转移类别机器码+位移量
转移指令可以分为短转移,长转移和子程序调用(call)
短转移2字节
长转移无条件5字节,条件转移6字节
call指令5字节
条件设置指令
对于语句
c = (a < b) ? c1 : c2;
条件分支语句为
cmp a, b mov eax, c1 jl L1 mov eax, c2L1:
使用条件设置语句可以不包含条件分支
xor eax, eaxcmp a, bsetge al ; if a >= b, al = 1, else al = 0dec eaxand eax, (c1 - c2)add eax, c2
或者使用条件传输指令
mov eax, c2cmp a, bcmovl eax, cl
循环语句
通常使用 ecx
寄存器作为计数器,例如
xor ecx, ecx ; 计数器清空:L1 inc ecx ... cmp ecx, 05 ; 循环退出条件 jbe L1
优化后的循环实例
xor ecx, ecx xor eax, eax:L1 add ecx, eax inc eax cmp eax, 64h jle L1 xor eax, eax
对应的源码
sum = 0;for (i = 0; i <= 100; i++) sum += i;
数学运算
加减法
add
和 sub
指令,有时候可以使用 lea
进行优化
lea 允许一个时钟内计算 lea edx, [eax+ecx+78h]
级别的运算
乘法
乘法使用 mul
或 imul
指令
对于2的幂,使用 shl
指令可以加快运算
由于 lea
指令可以实现乘 2, 4, 8 的运算,因此可以用来加快 3, 5, 6, 7, 9 等数字的乘法运算,如 lea eax, [eax+eax*4]
除法
div
或 idiv
指令
对于2的幂,使用 shr
指令加速,有符号时使用 sar
此外可以利用乘法进行加速
常见的优化公式为 $$ \dfrac{a}{b}=a\times \dfrac{1}{b} $$ 因此,$\div 11$ 可以优化为 $(\times 2E8BA2E9)»(32+1)$
mov eax, 2E8BA2E9imul ecxsar edx, 1 ; edx中存放了乘法的高位双字节mov ecx, edx
字符串
字符串存储
分为两种,一种使用结束符作为标识,一种记录长度
- C语言:
String\0
- DOS字符串:
String$
- PASCAL:
\x05String
- Delphi:
\x05\x00String
Go语言使用一个64位整型记录长度
字符寻址指令
mov将当前指令所在的内存复制并放到目的寄存器中,可操作常量或指针
lea是装入有效地址,操作数是地址
以下两条指令是等价的
lea eax, [401000h]mov eax, 401000h
都是将401000h写入eax寄存器中
因此,以下两个指令也是等价的
lea eax, [eax+8]add eax, 8
常被编译器用来计算加法
ASCII大小写转换
区别在于二进制的第五位,大写字母为0,小写字母为1
因此有如下方法
- 大小写转换:$\pm\ \mathrm{0x}20$,$\oplus\ \mathrm{0x}20$
- 转大写:$&\ \mathrm{0b}11011111$
- 转小写:$|\ \mathrm{0b}00100000$
计算长度
mov ecx, FFFFFFFF ; 这一句是一个重要特征xor eax, eax ; 清零,原文为subrepnz ; 复制串操作,直到ecx为0scasb ; 串扫描指令,把al中的内容与edi指向的附加段中的数据逐一比较not ecx ; ecx=字符长度+1dec ecx ; ecx=字符长度je xxxxxx ; 如果ecx为0,说明长度为0
指令修改技巧
eax有优化,尽可能使用
替换字节:
指令 | 机器码 | 指令字节长度 |
---|---|---|
nop | 90 | 1 |
push eax + pop eax | 50 58 | 2 |
inc eax + dec eax | 40 48 | 2 |
mov edi, edi | 8B FF | 2 |
jmp xx | EB 00 | 2 |
用nop就行
寄存器清零:
指令 | 机器码 | 指令字节长度 |
---|---|---|
mov eax, 00000000h | B8 00 00 00 00 | 5 |
push 0 + pop eax | 6A 00 + 58 | 3 |
sub eax, eax | 2B C0 | 2 |
xor eax, eax | 33 C0 | 2 |
测试寄存器是否为0:
指令 | 机器码 | 指令字节长度 |
---|---|---|
cmp eax, 00000000h | 83 F8 00 | 3 |
or eax, eax / test eax, eax | 0B C0 / 85 C0 | 2 |
后接 je label
字节码为 74 xx
或 0F 84 xxxxxxxx
,长度为2或6,取决于近跳转还是远跳转
寄存器置 0FFFFFFFFh
指令 | 机器码 | 指令字节长度 |
---|---|---|
mov eax, 0FFFFFFFFh | B8 FF FF FF FF | 5 |
(清零后)dec eax | 48 | 1 |
std + sbb eax, eax | F9 + 2B C0 | 3 |
转移指令
指令 | 机器码 | 指令字节长度 |
---|---|---|
jmp label | EB xx / E9 xxxxxxxx | 2 / 6 |
push label + ret | 68 xxxxxxxx + C3 | 6 |
64位
与32位有很多重叠
寄存器
64位通用寄存器 R 开头
8个128位 XMM 寄存器,通常用来优化(SIMD指令)
此外, AX
低16位,AL
低8位, AH
第 $8\sim 15$ 位
R8
则有 R8D
低32位,R8W
低16位,R8B
低8位
函数
栈
x64中有如下区别
-
一个栈空间8字节(64位)
-
汇编指令对栈顶需要对齐16(被16整除)
根据start寻找main
start函数结束前会有如下指令
jmp __tmainCRTStartup
进入该函数后可以看到 main
函数
如果符号表被去除,可根据前后特征进行定位
在调用完成main后,通常会调用exit来退出进程,所以exit前的一个call就是main函数(也可能在该函数内部的call中)。
调用约定
使用寄存器快速调用约定
前几个参数使用的寄存器是固定的,后续的参数从右往左栈,非1, 2, 4, 8字节大小的参数必须用引用(地址)传参
传递顺序为 RCX
, RDX
, R8
, R9
,所有浮点参数由XMM传参,顺序依次为 XMM0
~ XMM3
为了使得寄存器仍然能够使用,会预留栈空间,将寄存器的值存入栈空间中,该空间由调用者申请并平衡
函数返回值
使用 RAX
返回参数,返回值过大可以使用栈空间作为参数间接访问
数据结构
局部变量
使用栈区进行存放
Release版会更多地使用寄存器
全局变量
地址通常在编译期固定
mov eax, cs:140009150h
数组
$地址=首地址+类型大小\times 下标$
IDA中使用Y快捷键来修改数据类型,可以反编译成下标模式,更好看一些
汇编通常为 [地址+寄存器*n]
(或者循环中每次循环 地址+=n
?)
控制语句
通常虚线箭头表示有条件跳转,实线箭头表示无条件跳转
if 语句:jxx跳转,且目的地址后没有jxx(说明不是循环)
if else语句:jxx跳转,且目的地之前有一个jmp实跳转,目的地址后无跳转
if elif else语句:多个jxx跳转,每个Block以jmp结尾,最后一个Block不含跳转
switch case语句:分支数 $\geqslant 6$ 使用case表,$<6$ 使用else if
无法使用case表的情况下,为减少if的判断次数,可能使用二叉平衡树来减少if判断次数
循环语句
do while
先执行,后判断
do_while_start:{ ; 代码}cmp a, bjxx do_while_start
通常有一个向上跳转
while循环
先判断,后执行
while_start:cmp a, bjxx while_end{ ; 代码}jmp while_startwhile_end:; 后续代码
通常为一个向下的条件跳转,该目的地之前有一个向上的实跳转,跳转到向下跳转前
for 循环
jmp for_iffor_step: 步长for_if: 循环条件jxx for_endfor 代码jmp for_stepfor_end:
很常见的代码,比while循环多一个向下跳转
数学运算符
加减法
add和sub指令,可用lea指令进行优化
此外还有常量折叠的优化方法,即编译时提前完成常量间的计算,节省运行消耗的时间
乘法
imul为有符号乘法,mul为无符号乘法
通常使用lea比例因子寻址优化
lea edx, ds:0[rcx*4] ; *4imul edx, 7 ; *7lea edx, [rbc+rbc*8] ; *9
除法
有符号
除数为 $2^n$ 时,使用位移进行优化
除数为 $-2^n$ 时,使用位移,同时增加求补(x为负数时,计算 $-((x+(2^n-1))\gg n)$ )
取模
软件保护技术
序列号
序列号(注册码)的方式是目前最常见的一种保护
过程通常为:用户提交个人信息,公司计算得到序列号并返回给用户,用户通过序列号进行注册。软件从磁盘文件或注册表中获取注册信息
保护机制
本地计算用户信息并与序列号比较
即:$序列号=F(用户名)$
对于这种方式,可以直接使用调试的手段,在内存中直接找到计算后的序列号,同时,将F函数复制出即可生成注册机
通过注册码求逆并与用户信息比较
即:用公式 $序列号=F(用户名)$ 生成,用公式 $用户名=F^{-1}(序列号)$ 验证
破解方法有
- 通过 $F^{-1}$ 求出 $F$
- 给定用户名,穷举序列号
- 给定序列号,用 $F^{-1}$ 计算出用户名(通常包含不可见字符)
对等函数检查
即:$F_1(用户名)=F_2(序列号)$
通常 $F_2$ 可逆,借鉴上两种破解思路即可
二元函数
即:$特定值=F(用户名,序列号)$
缺陷在于可能缺少用户名与序列号的一一对应关系,开发者不易写出注册机
攻击方法
法一:通过跟踪输入,找到判断逻辑
软件通常调用api将用户输入复制到缓冲区,常用api有:
GetWindowsTextA(W)
,GetDiaItemTextA(W)
,GetDlgItemInt
,hmemcpy
,或者查找输出函数(对话框或标准输出等)
法二:跟踪程序启动时对注册码的判断过程
注册表中的序列号会调用
RegQueryValueExA(W)
,INI文件中GetPrivateProfileStringA(W)
等等
根据数据约束性
对于采用明文比较的程序,正确注册码通常在输入注册码的前后 90h
字节的地方
hmemcpy
这个函数是 Windows 9x
系统的内部函数,是万能断点
现在同样可以使用系统的API下断点
消息断点
按下和释放鼠标时,会发送 WM_LBUTTONDOWN (0201h)
和 WM_LBUTTONUP (0202h)
消息
可以利用消息断点断在按钮的事件代码处
人机交互信息
软件大多数采用了人机对话的方式进行,因此可以直接通过搜索字符串和交叉引用找到关键函数
应该是目前最常用的方法了
字符串比较
- 寄存器直接比较
- 函数比较
- 串比较
串比较有些少见,记录一下:
lea edi []
lea esi []
repz cmpsd
jz (jnz)
制作注册机
明码泄露的攻击
序列号在内存中曾以明码出现过即可
可以使用keymake编写内存注册机,或利用 Int 3
等 Debug API
手写内存注册机
无明码
进行加密算法的逆向解密
或者直接将汇编嵌入注册机中
警告窗口
软件不时提醒用户购买正式版本
可以通过设置窗口为不可见来去除,或者在窗口的创建函数处将其跳过
利用 Resource Hacker
软件可以找到窗口的 id,然后再汇编中搜索即可找到窗口的程序
程序中,是否注册的 flag
标记可能是全局变量,找到这个变量并 patch 程序使其为 1 即可
时间限制
限制单次运行时长,或者限制软件的使用时间
计时器
setTimer() 函数
程序运行时会申请一个计时器,并指定间隔,并获得一个处理计时器超时的回调函数
UINT SetTimer(
HWND hWnd, // 窗口句柄,计时器到时后,将向这个窗口发送WM_TIMER消息
UINT nIDEvent, // 计时器标识
UINT uElapse, // 指定计时器时间间隔(单位为毫秒)
TIMERPROC lpTimerFunc // 回调函数,超时后将调用
);
高精度计时器
通过调用 timeSecEvent()
函数启动
GetTickCount() 函数及 timeGetTime() 函数
该函数返回系统自成功启动以来所经过的时间,将两次返回值相减,即可得到当前运行时间
这种方法也可以使用 time()
等函数
时间限制
软件通常将第一次运行时的系统时间,存放在注册表或文件或某扇区中,每次运行时获取该时间,并与当前时间进行比较
为了避免用户修改系统时间,软件会在保存安装时间(并存放于多个地方)之外,再保存最近一次运行的时间,每次运行时用当前时间替换
拆解时间限制
- 直接跳过
SetTimer()
函数 - 利用
WM_TIMER
消息,查找到时间比较的位置,对二进制文件进行patch(去掉退出跳转等等)
动态分析时,可以配合变速齿轮使用,这样就可以很快到达软件的限制时间,进行调试
菜单功能限制
当注册版和正式版文件相同,只是部分功能被限制无法使用时,可以恢复正式版的功能
相关函数
EnableMenuItem()
BOOL EnableMenuItem(
HMENU hMenu, // 菜单句柄
UINT uIDEnableItem, // 欲允许或禁止的一个菜单条目的标识符
UINT uEnable, // 控制标志,包括允许、灰化、禁止等
)
EnableWindow()
BOOL EnableWindow(
HWND hWnd, // 窗口句柄
BOOL bEnable // True为允许,False为禁止
)
拆解菜单限制保护
找到关键函数,把函数的参数patch一下即可
KeyFile保护
KeyFile通常是一个小文件,可能是可见字符,也可能是二进制文件,由软件开发者定义
软件启动后,会从KeyFile文件中读取数据,根据处理结果判断是否正确注册
相关API
与文件操作有关的API都可以下断点
API | 作用 |
---|---|
FindFirstFileA | 确定注册文件是否存在 |
CreateFileA, _lopen | 确定文件是否存在,打开文件以获得句柄 |
GetFileSize, GetFileSizeEx | 获得文件的大小 |
GetFileAttributesA, GetFileAttributesExA | 获得文件的属性 |
SetFilePointer, SetFilePointerEx | 移动文件指针 |
ReadFile | 读取文件内容 |
拆解保护
- 利用文件监视软件找到KeyFile文件名
- 利用十六进制编辑器伪造KeyFile
- 动态调试,跟踪文件内容
具体的破解方法与输入序列号类似
网络验证
软件必须从服务器中取得一些关键数据才能正确运行
破解的思路是拦截服务器的数据包,并分析程序对该数据包的处理
相关API
int send(
SOCKET s, // 套接字描述符
const char FAR *buf, // 缓冲区
int len, // 发送数据的字节数
int flags // 附加标志,一般为0
);
int recv(
SOCKET s, // 套接字描述符
char FAR *buf, // 缓冲区
int len, // 缓冲区buf的长度
int flags // 附加标志,一般为0
);
此外,还有微软扩展函数 WSASend
和 WSARecv
破解思路
当服务器发送的数据包固定时,可以搭建本地服务器,发送该数据包
数据包不固定时,需要分析算法
加密算法——常见加密库接口及其识别
可以使用 Flair
工具制作 IDA sig
Miracl 大数运算库
大数库,支持 RSA、DH 密钥交换、DSA 以及椭圆曲线等
存储方式:以 $2^{32}$ 进制表示,低位在前,高位在后
识别方式:MR_IN()
是错误处理方式,几乎每个函数中都有
mov dword ptr [eax+ecx*4+20], yy
其中,yy 就是 MR_IN()
的参数,一次可以从 miracl.h
中找到对应的函数
FGInt
用于 Delphi,可实现常见公钥加密系统
存储方式:以 $2^{31}$ 进制表示
识别方式:参数个数以及调用前后的数据变化(黑盒)或者使用 PEiD 的 Krypto ANALyzer 插件
freeLIP
最初用于用于进行 RSA-129 挑战,$2^{30}$ 进制,速度不如 Miracl
Crypto++
实现了大量的加密算法,常用识别方法为 IDA sig,需要熟练掌握加密算法
LibTomCrypto
包括常见的散列算法、对称算法以及公钥加密算法
GMP
核心采用了汇编语言实现,速度非常快,通常用于实现大整数分解
OpenSSL
用于网安领域,包括一些加密算法的实现,例如 BlowFish、IDEA、DES、CAST,RSA、DSA,MD5、RIPEMD、SHA 等
可以到 OpenSSL 的 crypto 目录下的加密算法源码中寻找符合条件的函数
Microsoft Crypto API
可参考 MSDN,IDA、OllyDbg 等软件均可识别
NTL
数论相关,实现有符号的、算术整数的运算,以及向量、矩阵、基于有限域和整数的多项式运算
DCP 和 DEC
Delphi 的加密算法库
Windows内核基础
内核理论基础
权限级别
CPU将权限分为 R0~R3
其中 R0
权限最高,运行内核,R1
和 R2
运行驱动程序,R3
权限最低,运行应用程序
操作系统(Windows, Linux)为方便,将内核和驱动程序(内核态)运行在了 R0
层,将应用程序(用户态)运行在 R3
层,而没有使用 R1
和 R2
,因此 AMD64
取消了 R1
和 R2
层
内存空间布局
32位系统虚拟内存:
2GB内核空间,64KB的NULL空间和非法空间,其余为进程空间
64位系统虚拟内存:
Windows实际为44位(16TB),Linux实际为48位(256TB)
存在大量空洞
+-+-+-+-+-+-+-+-+-+-+-+-+ 0x000000
| |
+-+-+-+-+-+-+-+-+-+-+-+-+ 0x400000
| text |
+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+
| BSS |
+-+-+-+-+-+-+-+-+-+-+-+-+
| heap |
+-+-+-+-+-+-+-+-+-+-+-+-+ 向下增长
| |
| hole |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+ 0x00002AAAAAAAA000
| 内存映射区域 |
+-+-+-+-+-+-+-+-+-+-+-+-+ 向下增长
| |
| hole |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+ 向上增长
| stack |
+-+-+-+-+-+-+-+-+-+-+-+-+ 0x00007FFFFFFFF000=TASK_SIZE
| 未定义区域 |
+-+-+-+-+-+-+-+-+-+-+-+-+ 0xFFFF800000000000
| 内核空间 |
+-+-+-+-+-+-+-+-+-+-+-+-+
Windows内核启动过程
BIOS+MBR+Windows
启动自检
从BIOS中载入必要指令,进行硬件初始化检查,并显示信息
初始化启动
根据CMOS设置,BIOS加载启动盘,将引导代码载入内存,由MBR执行启动过程。启动代码搜索MBR的分区表,找到活动分区,将第一个扇区的引导代码载入内存,检测系统并查找启动管理器。过去为 ntldr
,Windows7开始使用 Bootmgr
作为启动管理。
Boot加载
对启动管理器进行设置
- 设置内存模式:32位系统+32位CPU,设置为32位内存模式;64位系统+64位CPU,设置为64位内存模式
- 启动一个简单的文件系统:定位
boot.ini
,ntoskrnl
,Hal
等启动文件 - 读取
boot.ini
文件
检测和配置硬件
检查和配置硬件设备,如系统固件、总线、适配器、键盘磁盘等等
内核加载
启动管理器先加载内核 Ntoskrnl.exe
和硬件抽象层 HAL
。HAL
会对硬件底层进行隔离,为操作系统提供统一的API。
随后根据注册表 HKEY_LOCAL_MACHINE\System\CurrentControlSet
来加载驱动程序
注册表中的 Start
键表示了启动顺序
SERVICE_BOOT_START
, 内核初始化时,与系统核心相关的重要驱动程序SERVICE_SYSTEM_START
SERVICE_AUTO_START
,登陆界面开始SERVICE_DEMAND_START
,需要时手动加载SERVICE_DISABLED
,禁止加载
Windows会话管理启动
smss.exe
是Windows中第一个创建的用户模式进程,主要用于
- 创建系统环境变量
- 加载
win32k.sys
,Windows子系统的内核模式部分 - 启动
csrss.exe
,Windows子系统的用户模式部分 - 启动
winlogon.exe
- 创建虚拟内存页面文件
- 执行重启前未完成的重命名工作