本博客面向NJUPT的逆向应试,仅供参考,大佬飘过,不喜勿喷。
今天参考了一下逆向界圣经《加密与解密》,对内容做了详细修改与完善。
1.windows平台的各种典型调试工具及应用的场景;
(1)静态分析与PE文件结构查看
winhex:文件查看工具,二进制编辑器,十六进制编辑器
ultraedit:万能的文本编辑器,十六进制和二进制编辑器
eXeScope:PE文件编辑器,PE文件查看器,查看PE文件的结构用。
IDA:静态分析
- 下面这个是如何在ida中查看API函数(Imports窗口)
(2)动态分析
windbg:用户态与内核态调试工具(调试内核与分析应用程序),主要是以命令方式工作
x32dbg/ollydbg:windows平台下的动态分析工具(软件应用层调试)
gdb:linux平台下动态分析工具
此外,还有
- (应用级调试)R3 debugging: Ollydbg/gdb
- (内核级调试)R0 debugging: windbg/kgdb, softice
(3)固件调试
- 工具的话有Binwalk,firmware-mod-kit,这些主要都是应用在linux平台下的。
- 固件调试的环境大多是用Qemu虚机。ida可与Qemu 联动,调试固件
2.各种典型的查壳工具名称及对应的用途;
PEiD:最经典的查壳工具。
DIE:linux平台下开源查壳工具,其实不只是linux平台,windows和mac都可
LordPE:PE文件编辑器,也是常见的查壳工具(下图右边的几个按钮是实现的功能,字面上也能看出是干嘛的了)
3.虚拟化环境的搭建过程中涉及的步骤、原理、工具名称;
虚机:VMware/virtualBox/Qemu
(固件调试)
4.crackme分析的入手方法;
首先找到提示用户输入的API函数,如GetDlgItemTextA,GetWindowText,,GetDlgItemInt、MessageBoxA
等,还有关于输入的结果的提示信息,这样可以基本定位程序判别输入的算法位置,然后就是精确定位,分析关键代码,尤其注意几个条件跳转语句,cmp
,test
,运算操作等等。。。
实例请参考我的另外两篇关于crackme的博客
5.压缩壳的脱壳步骤及对应的原理;
upx:压缩壳
zprotect:加密壳
- 查找真正的程序入口点(对于upx压缩壳采用ESP定律查找)
- 抓取内存镜像文件(工具:LordPE、OD plugins)
- 重建PE文件修复输入表(工具:ImportREC)【这里涉及到PE文件结构,参考另一篇博客】
ESP定律(栈平衡原理)
upx满足ESP定律。
先给出esp寄存器的功能:存放栈顶的偏移地址有关汇编代码中call指令实质上是
push ip ;把下一条指令的地址(存放在ip寄存器中)入栈 jmp near ptr 地址 ;跳转到该函数的地址处
还有ret和retf(一个函数结束的标志):
pop ip ;(ret)返回到call指令的下一条指令
pop IP ;(retf)返回到call指令的下一条指令 pop CS
在编写加壳软件时,我们必须要保证外壳初始化的现场环境(即各个寄存器的值)与源程序的现场环境是相同的,所以,在程序自解密或者自解压过程中,对于多数壳,它们会先将当前寄存器状态压栈,如使用pushad/pushfd。
而在解压结束后, 会将之前的寄存器值出栈,如使用popad/popfd,一般标志寄存器不是很重要,一般不做处理。
所以编写加壳软件必须遵守的是栈平衡原理。因此在寄存器出栈时, 往往程序代码被恢复, 此时硬件断点触发(pop IP),这就是我们要下硬件断点的原因,然后在程序当前位置,只需要一些单步操作,就会到达正确的OEP位置。
也就是说,我们可以这么认为,如果载入程序后只有esp寄存器(eip除外)内容发生变化,那么这个程序多半可以用ESP定律。
6.花指令的原理及意义;
花指令是程序中的无用指令或者垃圾指令,故意干扰各种反汇编静态分析工具,但是程序不受任何影响,缺少了也能正常运行加花指令后, IDA 等 分析工具对程序静态反汇编时,往往会出现错误或者遭到破坏,加大逆向静态分析的难度,从而隐藏自身的程序结构和算法,从而较好的保护自己花指令有可能利用各种指令: jmp , call, ret 的一些堆栈技巧,位置运算等。
花指令分成可执行式花指令(无用指令)和不可执行式花指令(垃圾指令)
- 可执行式花指令:①可以正常运行;②不改变任何寄存器的值;③反汇编器可以正确反汇编该指令。主要式用于病毒变形
- 不 可 执 行 式 花 指 令 \color{red}{不可执行式花指令} 不可执行式花指令:①不可以正常运行;②不改变任何寄存器的值;③反汇编器可能会错误反汇编这些字节。主要是用于干扰反汇编引擎如IDA进行反汇编
冯诺依曼体系架构下指令跟数据放在同一存储器,一些线性扫描的反汇编引擎无法区分夹杂在代码中的数据,导致反汇编错误。
下面给出沙老师的8个花指令原理,详细的参考之前我写过的花指令博客:
花指令1
__asm {
xor eax, eax //eax寄存器置0
test eax, eax //相当于test 0,0;从而产生确定标志位ZF
je LABEL1
jne LABEL2 //je和jnz指令形成互补跳转
LABEL2 :
_emit 0x5e //与pop si机器码相同
and eax, ebx
_emit 0x50 //与push ax机器码相同
xor eax, ebx
_emit 0x74 //与汇编助记符 jz 机器码相同
add eax, edx
LABEL1 :
}
程序运行这个花指令的时候产生确定标志位(ZF
),进而出现确定跳转(必定执行je LABEL1
跳转指令 ),所以肯定是会绕过干扰代码
的,而反汇编引擎是会将数据0x5e, 0x50 ,0x74 当作指令反汇编
花指令2
__asm {
push eax;
xor eax, eax;
test eax, eax;
jnz LABEL1;
jz LABEL2;
LABEL1:
_emit 0xE8; //与call助记符的机器码相同
LABEL2:
mov byte ptr[a], 0;
call LABEL3;
_emit 0xFF; //与adc助记符的字节码相同
LABEL3:
add dword ptr ss : [esp], 8;
ret;
__emit 0x11;
mov byte ptr[a], 2;
pop eax;
}
LABEL1标号的是干扰项,跟1号花指令效果一样,不再赘述。
直接从LABEL2开始分析,调用了函数LABEL3,把_emit 0xFF;的地址入栈(push IP)转到label3,此时栈顶指针sp指向的是call指令的下一条指令,即0xFF这个干扰项的地址,但执行add dword ptr ss : [esp], 8;给这个地址值加了8,指向了mov byte ptr[a], 2的地址,巧妙地修改了这个地址,所以接下来的ret指令将该指令的地址出栈到IP寄存器(pop IP),以至于在调用完LABEL3函数之后,下一个指令变成了mov byte ptr[a], 2,这样程序运行不会受到干扰项的影响。
花指令3
_asm {
push ebx;
xor ebx, ebx;
test ebx, ebx;
jnz LABEL5;
jz LABEL6;
LABEL5:
_emit 0x21; //与and助记符的机器码相同
LABEL6:
pop ebx;
}
同花指令1
花指令4
void example4()
{
__asm {
push ebx;
xor ebx, ebx;
test ebx, ebx;
jnz LABEL7;
jz LABEL8;
LABEL7:
_emit 0xC7;
LABEL8:
pop ebx;
}
a = 4;
}
同花指令1
花指令5
__asm {
call LABEL9;
_emit 0x83;
LABEL9:
add dword ptr ss : [esp], 8;
ret;
__emit 0xF3;
}
同花指令2
花指令6
LoadLibrary("./hhhh"); //函数返回值存储于eax中
_asm {
cmp eax, 0;
jc LABEL6_1; //cf=1触发跳转
jnc LABEL6_2; //cf=0触发跳转
LABEL6_1:
_emit 0xE8; //与call汇编指令对应
LABEL6_2:
}
程序运行过程中用API函数LoadLibrary加载了一个实际不存在的名为hhhh
的库,而由于不存在这个库所以必定返回0
,且存入eax
,也就是说cmp指令
使进位标志寄存器cf=0
,产生了确定值,即确定跳转
花指令7
if (a > 0)
return 1;
else
return 0;
_asm {
cmp eax, 0;
jc LABEL7_1;
jz LABEL7_2;
LABEL7_1:
_emit 0xE8;
LABEL7_2:
}
同样,确定标志位产生确定跳转,配合干扰项,不再赘述。
花指令8(配合裸函数)
void __declspec(naked)__cdecl cnuF(int* a)//裸函数,开辟和释放堆栈由我们自己写。
{
//55 8b ec 83
__asm
{
/*保留栈底*/
push ebp
/*开辟栈空间*/
mov ebp, esp
sub esp, 0x40//0x40是缓冲区大小
/*保留现场(寄存器状态)*/
push ebx
push esi
push edi
/*缓冲区写入数据*/
mov eax, 0xCCCCCCCC //0xCCCC在gb2312中是'烫'字
mov ecx, 0x10 //cx为下面填'烫'操作计数
lea edi, dword ptr ds : [ebp - 0x40]
rep stos dword ptr es : [edi]
}
/*执行的操作*/
*a = 1;
//花指令
_asm
{
call LABEL9;
_emit 0xE8;
_emit 0x01;
_emit 0x00;
_emit 0x00;
_emit 0x00;
LABEL9:
push eax;
push ebx;
lea eax, dword ptr ds : [ebp - 0x0]
add dword ptr ss : [eax - 0x50], 26;
pop eax;
pop ebx;
pop eax;//运行到这里eax存的是mov eax, dword ptr ss : [esp - 8];的地址
jmp eax;
__emit 0xE8;
_emit 0x03;
_emit 0x00;
_emit 0x00;
_emit 0x00;
mov eax, dword ptr ss : [esp - 8];
}
__asm
{
/*恢复现场*/
pop edi
pop esi
pop ebx
/*释放栈空间*/
mov esp, ebp
pop ebp
ret
}
}
同样是修改指令地址,是将修改后的指令地址出栈到eax,下面有个jmp无条件跳转语句,程序自动跳到mov eax, dword ptr ss : [esp - 8];
,绕过垃圾指令
7.ollydbg典型快捷键的名称、功能
[填空]
8.调试中不同断点的使用原理及使用条件
int 3断点(软中断)
也称软中断,cc断点。这是一种基于软中断机制断点,3为中断号。OD中,当你在代码区某行按F2即可实现,其机理是下断处的内容被用INT 3
修改了,当被调试进程执行INT 3指令导致一个异常时,调试器会捕捉这个异常,从而停在断点处,然后将断点处的指令恢复成恢复成原来的指令显示给调试者。
优点:可以无条件设置无数个断点
缺陷:改变了原程序机器码,易被软件检测
反调试技术:常用的一种反调试技术是搜索软件断点,来扫描调试器对它代码的INT3 修改。
内存断点
假如你用int 3断点对数据区下断,OD会提示你断点可能不会实现,其实也是必然,程序不可能执行数据区,然而我们却可以当数据被读取或写入时进行下断,这种原理主要基于内存属性,当下读写断点时,OD会修改断点处读写属性,如果程序对此数据读写的话,会产生读写异常,OD捕捉此异常并分析,其可以知道运行到何处,对代码段也可以下此断点,机理相似。
条件:大多在遇到代码校验且硬件断点失灵的情况下选择使用。
硬件断点(硬中断)
就是传说中的硬中断,与DRx调试寄存器有关,DRx调试寄存器共8个,每个有如下特性:
- DR0~DR3:调试地址寄存器、用于保存需要监视的地址,例如设置硬件断点。
- DR4~DR5:保留、未公开具体作用。
- DR6:调试寄存器组状态寄存器。
- DR7:调试寄存器组控制寄存器。
原理是用DR0、DR1、DR2、DR3
设定地址,并用DR7
设定状态(控制位),所以只可以设置4个断点,不会将首字节修改成CC
,当程序运行到设定的地址时CPU会向调试器发送异常信息,对其初步处理后,中断程序。
相比于软中断,硬中断更难被检测。
OD的快捷键F4的原理就是如此,只不过在中断之后自动删除该硬件断点。
要在触发硬件断点的下一条指令处下断。
优点:速度快,在检测INT 3
的反调试程序中可以使用硬件断点避免检测。
缺陷:只能设置4个。
9.简单了解一下壳偷代码的基本原理。
-
基本原理:
某些壳在处理OEP代码时候,把OEP处固定的代码NOP掉。然后把这些代码放到壳代码空间中去,而且常伴随花指令,使原程序的起始代码从壳执行,然后JMP回到原程序空间如果脱掉壳,这一部分就会遗失,达达到反脱壳的目的 -
原因:前几行代码放到壳空间中,所以不会被转储和执行
-
解决方法:寻找真的OEP和缺失代码
10.一个简单的crackme实例计算题目;
先把几个常见汇编操作码理解了。。。
参考我的另外两篇关于crackme的博客
11.目标程序呈现出各种典型特点,给出对应的分析及破解思路;
一些反调试的破解之法已经写在下面反调试技术的分析中。
以下仅是我个人理解:
还有个考点就是字符串隐藏,这里可能是指反跟踪技术中的信息隐藏(猜的~~),所谓信息隐藏就是防止攻击者通过一些提示信息定位到你的条件判断语句,因为一旦定位成功就很大风险被破解(例如:提示你“注册码过期”的字符串,如果不加处理,攻击者会很容易反汇编后定位到你的判别算法然后爆破)。至于如何处理,简单点的方法直接字符串异或一下,在程序要显示这个字符串的地方异或回来,要是想更复杂点更难以调试的话就用特殊编码方式(比如base64等等),还有加密函数(明天考密码,不用我多说可以用啥)或者用一些打包及加壳软件(vmp是个很好的选择)等等,对于做这样处理后的程序,此时静态分析基本是没有用的,出来的只会是乱码,还是要结合动态分析(动态分析为主,静态分析为辅),动调的目的主要就是看你怎么找到那个加密的位置了。动调过程中尤其要留意一些奇怪的字符(这些可能是加密后的字符)和对这些字符进行的操作,因为不排除可能有一些隐藏算法只是简单的异或处理,这样就可以写个exp简单破解一下,或者配合LordPE的内存映像文件dump出来查找字符串也可。
然后翻了下以前做过的ctf题中关于这个的,找了个别人的博客,可以参考一下:[WMCTF2020]easy_re,这篇文章只要看那个以Reversing:easy_re
标题的就行,目的只是简单了解一下。除此之外还找到一个看雪论坛关于这个的博客
12.反调试技术的分析
想深入了解的,这里推荐一篇文章
(1) 调式器检测:各种方法查看调试器是否存在
windows api:
应用程序可以通过调用这些API,来检测自己是否正在被调试。
- IsDebuggerPresent()
IsDebuggerPresent查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值。
mov eax,dword ptr fs:[00000030h] //获取PEB结构基地址
movzx eax,byte ptr [eax+2] //根据PEB结构,我们知道这里是获取BeingDebugged的值
附msdn文档:
破解:将BeingDebugged值置1(改寄存器标志位)
-
CheckRemoteDebuggerPresent()
CheckRemoteDebuggerPresent同IsDebuggerPresent几乎一致。它不仅可以探测系统其他进程是否被调试,通过传递自身进程句柄还可以探测自身是否被调试。 -
NtQueryInformationProcess()
这个函数是Ntdll.dll中一个原生态API,它用来提取一个给定进程的信息。
它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。
为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。
第二个参数设置的值可以是下面的三选一:- ProcessDebugPort获取调试端口——调试的时候系统分配进程一个调试端口FFFFFFFF,非调试状态为0
- ProcessDebugObjectHandle获取调试对象句柄——如果进程处于调试状态,调试句柄值就存在,非调试状态为NULL
- ProcessDebugFlags调试标志——调试状态,为0;非调试状态,为1。
例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。
-
NtQuerySystemInformation()
基于调试环境检测的反调试技术(检测当前OS是否在调试模式下运行)
第一个参数是SYSTEM_INFORMATION_CLASS SystemInformationClass
SYSTEM_INFORMATION_CLASS是枚举类型,值为SystemKernelDebuggerInformation(0x23)时,第二个参数PVOID SystemInformation被填入
SYSTEM_KERNEL_DEBUGGER_INFORMATION结构体地址
当SYSTEM_KERNEL_DEBUGGER_INFORMATION.DebuggerEnabled=1,系统处于调试状态
破解:以正常模式启动windows
手动检测数据结构:
恶意代码编写者经常手动执行与上面API功能相同的操作。
系统痕迹检测:
我们使用调试工具来分析恶意代码,但这些工具会在系统中驻留一些痕迹。恶意代码通过搜索这种系统痕迹,来确定你是否试图分析它。
(2) 识别调试器:识别是否在调试中
-
检测软件、硬件断点:
在代码中查找机器码0xCC,来扫描调试器对它代码的INT 3修改。例如:为了防范API被下断点,一些软件会检测API的首地址是否是0xCC。破解:将断点设置在函数内部或者函数末尾,例如函数入口下一行。
在检测软件断点的程序中尝试设置硬件断点。硬件断点失灵就用内存断点。 -
时钟检测:
被调试时,进程的运行速度大大降低,例如,单步调试大幅降低恶意代码的运行速度,所以时钟检测是恶意代码探测调试器存在的最常用方式之一。 -
父进程判断一般双击运行的进程的父进程都是explorer.exe,但是如果进程被调试父进程则是调试器进程。也就是说如果父进程不是explorer.exe则可以认为程序正在被调试。
(3) 干扰调试器:令调试失败
-
TLS回调 :
在C++开发中,会经常遇到TLS(线程局部存储)回调函数。该函数有一个非常大的特点就是在线程运行前要先运行该函数,那么只需要在TLS回调函数中,写上反调试功能的代码,就可以有反调试功能,而不需要修改OEP,来执行反调试代码。使用PEView打开某程序,在PE扩展头中,如果TLS Table项中两个字段值都为0, 则该程序没使用TLS回调函数,否则就用了。破解:将OD断点设置为System breakpoint即可
-
利用中断:
因为调试器使用INT 3来设置软件断点,所以一种反调试技术就是在合法代码段中插入0xCC(INT 3)欺骗调试器,使其认为这些0xCC机器码是自己设置的断点。 -
陷阱标志位:
EFLAGS寄存器的第八个比特位是陷阱标志位。如果设置了,就会产生一个单步异常。在该模式下,CPU执行一条指令就会触发EXCEPTION_SINGLE_STEP异常,之后TF会自动清零破解:调试器忽略EXCEPTION_SINGLE_STEP异常,在注册SHE地址设置断点。