Back
Featured image of post 加密与解密学习笔记(持续更新ing)

加密与解密学习笔记(持续更新ing)

suibiankankan

基础知识

分析的一般途径和策略

  1. 学会软件的操作和使用方法 $\to$ 推测出软件的设计思想和编程思路
  2. 静态分析:阅读反汇编的程序清单,利用人机交互的提示信息了解片段所完成的功能,宏观了解软件的编程思路
  3. 动态跟踪:首先完成反反调试,并解密加密程序,了解初始化工作,获得各个模块之间的中间结果
    1. 粗跟踪:不跟踪调用等指令,仅根据执行结果分析程序的功能
    2. 细跟踪:针对性跟踪分析关键模块

文本字符

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 即可

函数调用

编译器通常使用 callret 指令来调用函数

call 指令将其之后的指令地址压入栈顶,ret 指令则返回到调用位置

有时可能利用寄存器进行间接调用,如: call eax

参数传递

调用函数时,将参数压入栈中

对于不同语言,有不同的调用约定

类型 C/C++(__cdecl) pascal stdcall fastcall
参数传递顺序 从右向左 从左向右 从右向左 使用寄存器和栈
平衡栈 调用者 子程序 子程序 子程序
VARARG 允许*

VARARG表示参数个数可以不确定

stdcall中,如果参数个数不确定,需要由调用程序来平衡栈

程序执行过程:

  • 调用者将函数执行完毕时应返回的地址、参数压入栈
  • 函数使用 ebp 指针+偏移量对栈中的参数进行寻址并取出,完成操作
  • 子程序使用 retretf 指令返回, 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 ebpmov ebp, espsub esp, xxx

leave 指令:add esp, xxxpop 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;

数学运算

加减法

addsub 指令,有时候可以使用 lea 进行优化

lea 允许一个时钟内计算 lea edx, [eax+ecx+78h] 级别的运算

乘法

乘法使用 mulimul 指令

对于2的幂,使用 shl 指令可以加快运算

由于 lea 指令可以实现乘 2, 4, 8 的运算,因此可以用来加快 3, 5, 6, 7, 9 等数字的乘法运算,如 lea eax, [eax+eax*4]

除法

dividiv 指令

对于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 xx0F 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字节大小的参数必须用引用(地址)传参

传递顺序为 RCXRDXR8R9,所有浮点参数由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)GetDlgItemInthmemcpy,或者查找输出函数(对话框或标准输出等)

法二:跟踪程序启动时对注册码的判断过程

注册表中的序列号会调用 RegQueryValueExA(W),INI文件中 GetPrivateProfileStringA(W)等等

根据数据约束性

对于采用明文比较的程序,正确注册码通常在输入注册码的前后 90h 字节的地方

hmemcpy

这个函数是 Windows 9x 系统的内部函数,是万能断点

现在同样可以使用系统的API下断点

消息断点

按下和释放鼠标时,会发送 WM_LBUTTONDOWN (0201h)WM_LBUTTONUP (0202h) 消息

可以利用消息断点断在按钮的事件代码处

人机交互信息

软件大多数采用了人机对话的方式进行,因此可以直接通过搜索字符串和交叉引用找到关键函数

应该是目前最常用的方法了

字符串比较

  1. 寄存器直接比较
  2. 函数比较
  3. 串比较

串比较有些少见,记录一下:

lea edi []
lea esi []
repz cmpsd
jz (jnz)

制作注册机

明码泄露的攻击

序列号在内存中曾以明码出现过即可

可以使用keymake编写内存注册机,或利用 Int 3Debug API 手写内存注册机

无明码

进行加密算法的逆向解密

或者直接将汇编嵌入注册机中

警告窗口

软件不时提醒用户购买正式版本

可以通过设置窗口为不可见来去除,或者在窗口的创建函数处将其跳过

利用 Resource Hacker 软件可以找到窗口的 id,然后再汇编中搜索即可找到窗口的程序

程序中,是否注册的 flag 标记可能是全局变量,找到这个变量并 patch 程序使其为 1 即可

时间限制

限制单次运行时长,或者限制软件的使用时间

计时器

setTimer() 函数

程序运行时会申请一个计时器,并指定间隔,并获得一个处理计时器超时的回调函数

UINT SetTimer(
    HWND hWnd,             // 窗口句柄,计时器到时后,将向这个窗口发送WM_TIMER消息
    UINT nIDEvent,         // 计时器标识
    UINT uElapse,          // 指定计时器时间间隔(单位为毫秒)
    TIMERPROC lpTimerFunc  // 回调函数,超时后将调用
);
高精度计时器

通过调用 timeSecEvent() 函数启动

GetTickCount() 函数及 timeGetTime() 函数

该函数返回系统自成功启动以来所经过的时间,将两次返回值相减,即可得到当前运行时间

这种方法也可以使用 time() 等函数

时间限制

软件通常将第一次运行时的系统时间,存放在注册表或文件或某扇区中,每次运行时获取该时间,并与当前时间进行比较

为了避免用户修改系统时间,软件会在保存安装时间(并存放于多个地方)之外,再保存最近一次运行的时间,每次运行时用当前时间替换

拆解时间限制

  1. 直接跳过 SetTimer() 函数
  2. 利用 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 读取文件内容

拆解保护

  1. 利用文件监视软件找到KeyFile文件名
  2. 利用十六进制编辑器伪造KeyFile
  3. 动态调试,跟踪文件内容

具体的破解方法与输入序列号类似

网络验证

软件必须从服务器中取得一些关键数据才能正确运行

破解的思路是拦截服务器的数据包,并分析程序对该数据包的处理

相关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
);

此外,还有微软扩展函数 WSASendWSARecv

破解思路

当服务器发送的数据包固定时,可以搭建本地服务器,发送该数据包

数据包不固定时,需要分析算法

加密算法——常见加密库接口及其识别

可以使用 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 权限最高,运行内核,R1R2 运行驱动程序,R3 权限最低,运行应用程序

操作系统(Windows, Linux)为方便,将内核和驱动程序(内核态)运行在了 R0 层,将应用程序(用户态)运行在 R3 层,而没有使用 R1R2 ,因此 AMD64 取消了 R1R2

内存空间布局

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.inintoskrnlHal 等启动文件
  • 读取 boot.ini 文件
检测和配置硬件

检查和配置硬件设备,如系统固件、总线、适配器、键盘磁盘等等

内核加载

启动管理器先加载内核 Ntoskrnl.exe 和硬件抽象层 HALHAL 会对硬件底层进行隔离,为操作系统提供统一的API。

随后根据注册表 HKEY_LOCAL_MACHINE\System\CurrentControlSet 来加载驱动程序

注册表中的 Start 键表示了启动顺序

  1. SERVICE_BOOT_START, 内核初始化时,与系统核心相关的重要驱动程序
  2. SERVICE_SYSTEM_START
  3. SERVICE_AUTO_START,登陆界面开始
  4. SERVICE_DEMAND_START,需要时手动加载
  5. SERVICE_DISABLED,禁止加载
Windows会话管理启动

smss.exe 是Windows中第一个创建的用户模式进程,主要用于

  • 创建系统环境变量
  • 加载 win32k.sys,Windows子系统的内核模式部分
  • 启动 csrss.exe,Windows子系统的用户模式部分
  • 启动 winlogon.exe
  • 创建虚拟内存页面文件
  • 执行重启前未完成的重命名工作
Built with Hugo
Theme Stack designed by Jimmy