初次接触脱壳
程序壳
(也称为 “程序包装器” 或 “加壳程序”)是一种软件工具,它可以将一个软件程序的可执行文件包装在另一个可执行文件中。
借用PandaOS师傅的图片,简单了解一下压缩壳的原理:、
栈
栈(stack)是内存中分配的一段空间。
向一个栈插入新元素又称作入(push)放到栈顶元素的上面,使之成为新的栈顶元素;
从一个栈删除元素又称作出栈(pop),它把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
ESP定律
esp也就是栈顶,ESP定律的核心是堆栈平衡,就是在程序执行压缩壳的时候,会把壳的OEP压入栈中,然后当加载完壳之后,又会释放出壳的入口点然后重新把源程序的OEP压入栈中,在脱壳的时候就可以通过查看栈的变化而定位到源程序的入口点然后jump出来源程序实现脱壳,具体的汇编操作是:pushad:是将所有32位通用寄存器压入堆栈
IAT
( lmportAddress Table),在可执行文件中使用其他DLL可执行文件的代码或数据,称为导入或者输入。
在脱壳的过程中,导入表是很重要的东西,程序加壳的同时也改变了IAT表,所以在脱壳的时候也一定要修复IAT。如果找到了源程序的OEP,但是无法正常运行,那就是要修复程序的IAT了。
OEP
OEP,即Original Entry Point,是程序的入口点,一般在程序中就是刚开始执行的位置,在脱壳的过程中,就要通过动调去跳过壳的程序然后找到源程序的OEP入口,然后再把程序给dump出来(类似于把程序和壳给分开,只取壳),就实现了脱壳
断点
硬件断点:硬断点需要硬件寄存器提供支持,断点的数目受Embedded ICE中的Watchpoint数目的限制,但是可以在任何地方设置断点。
软件断点:软件断点通过在运行起来的程序中设置特征值实现,其数目不受限制,但是一般情况下软件断点只能在可写的存储器的地址中设置
通俗来说,断点分为软件断点和硬件断点,软件断点就是平常在ida中设置的断点,他的本质是给原来的程序中额外增加了一段程序,由于加壳的时候会对程序进行压缩,所以在ida中添加到软件断点的程序也会被压缩变成一些不可识别的数据,所以就无法实现间断。而硬件断点就没有这样的问题,他是基于硬件寄存器的基础上设置断点,就不会因为壳而受影响,可以理解成运行到那个寄存器的地址就停止的断点
一些常见的汇编指令:
Pushad
Pushad 是一种汇编指令,用于将当前 CPU 上下文中的所有通用寄存器(eax、ebx、ecx、edx、esi、edi、ebp、esp)依次入栈。该指令常用于保存当前进程的上下文并开辟新的堆栈帧,以便调用子程序。
具体地说,当执行 Pushad 指令时,CPU 首先将 eax 寄存器的值压入堆栈中,然后是 ebx、ecx、edx、esi、edi、ebp 和 esp 寄存器的值,每个寄存器占据 4 个字节的空间。在最后一个寄存器被压入堆栈之后,esp 寄存器的值减去了 32,指向当前堆栈顶部。这意味着堆栈上已经为每个寄存器分配了 4 个字节的空间,以便稍后可以使用 Popad 指令来还原它们的值。
总体来说,Pushad 和 Popad 指令通常是成对出现的,而且只用于 32 位模式下的 x86 架构。通过使用这些指令,汇编程序能够更加方便地管理进程的上下文和堆栈,从而实现复杂的运算和逻辑处理。
Pushfd
Pushfd 是一种汇编指令,用于将当前 CPU 的标志寄存器(flags register)的值入栈。标志寄存器中包含了一些特殊的位,它们的状态可以反映出 CPU 正处于何种工作状态,或者最近所发生的某些事件。例如,ZF(零标志位)和 SF(符号标志位)分别用于表示上次 ALU 操作的结果是否为 0 或负数。
当执行 Pushfd 指令时,CPU 首先将标志寄存器的值压入堆栈中,然后将 esp 寄存器的值减去 4,使其指向新的栈顶。在这个过程中,标志寄存器的值会被压缩成一个 32 位的无符号整数,并存储到堆栈中,以便稍后使用 Popfd 指令来还原它。
Pushfd 指令通常用于保存 CPU 运行环境中一些重要的状态信息,例如中断允许标志、虚拟内存开关状态等。它可以与 Popfd 指令一起使用,用于保存和还原程序执行之前的 CPU 状态信息。需要注意的是,Pushfd 和 Popfd 指令只能在特权级别为 0 或 1 的代码段中使用,在用户模式下会引发异常。
ret
Ret 是一种汇编指令,用于从子程序中返回到主程序。它的作用是将栈顶的数据弹出并赋值给程序计数器 PC(program counter),以便程序跳转回调用该子程序的位置继续执行。
具体来说,Ret 指令会先从堆栈中弹出一个 16/32 位的地址值,该值通常用于存储主程序返回时应该跳转到哪里继续执行。然后,这个地址值会被赋值给程序计数器 PC,使得 CPU 能够执行主程序中接下来的指令。
在使用 Ret 指令时需要注意栈的维护问题。在调用子程序时,主程序通常需要将一些参数通过入栈传递给子程序,并且还需要保存一些寄存器状态等运行环境信息。当子程序执行完毕后,它所使用的这些资源都需要及时释放,以免造成内存泄漏或其他问题。在一些编译器中,Ret 指令会默认自动生成 Epilogue 代码片段,用于恢复子程序执行前的运行环境和释放它所使用的堆栈空间。
需要注意的是,在使用 Ret 指令时必须确保返回地址存在于堆栈中,否则会发生意料之外的结果。如果主程序预期从一个没有调用过的子程序中返回,可以通过调用类似于 Nop 指令的占位符代码来确保堆栈上存在一个有效的返回地址。
Call
Call 是一种汇编指令,用于将程序执行流转移到一个子程序中执行,并记录下一个返回地址。具体来说,Call 指令首先会将当前 PC 的值(即 Call 指令的下一条指令地址)压入堆栈顶部,然后将跳转至目标子程序的入口地址更新到 PC 中,从而实现程序流程的转移。
当子程序执行完毕后,可以使用 Ret 指令将运行流程返回到 Call 指令下方对应的那条指令继续执行。Ret 指令会自动从堆栈顶部弹出保存在其中的返回地址,接着将其写入 PC 中,实现程序执行流程的“回溯”。
Call 指令常被用于程序结构化设计中,可以将复杂的任务分解成多个较小且功能单一的子过程,提高程序控制的模块性和可读性。此外,Call 指令还适用于各种其他场景,例如函数调用、异常处理、代码重用等等。需要注意的是,在使用 Call 指令时需要合理维护堆栈,避免堆栈溢出、栈内数据损坏等问题的发生。
Jump
Jump指令是一种汇编指令,用于实现程序代码的跳转。当CPU执行到Jump指令时,它会根据该指令中给出的目标地址,直接改变当前正在执行的程序指令的地址,从而将控制权转移到新的位置。这种操作既可以用于让程序跳转到不同的程序分支上执行,也可以用于实现循环、函数调用等复杂的程序控制结构。
具体来说,Jump指令通常是直接跳转到另外一个已知的程序地址或者是相对于当前PC寄存器的偏移量地址。在跳转过程中,CPU和内存需要处理好跳转前后内存状态以及保持堆栈的正确性,以免发生未知错误。因此,在使用Jump指令时,需要小心谨慎地编写代码,并且需要对其效果进行充分地测试和验证。
Je
“je”是汇编指令中的一种条件跳转指令,可以实现根据特定的条件来进行程序跳转的功能。 “je”全称为”Jump if Equal”(如果相等就跳转),它的作用是判断前面的运算结果是否等于零,如果等于则跳转到指定地址执行后面的指令,否则就顺序执行后面的指令。
具体来说,在使用”je”指令时,需要先进行一些运算或者比较操作,然后对比结果进行判断,并根据判断的结果来决定是否跳转到指定的目标地址。例如,当我们要判断两个数字是否相等时,可以通过使用”cmp”指令比较它们的大小关系,然后用”je”指令来判断它们是否相等,并根据”je”指令的跳转结果来选择接下来执行的代码块。
需要注意的是,因为每个指令都会占用CPU的执行时间和计算资源,所以在实际应用中,应该尽可能地把程序设计成简单明了的结构,避免过多的分支结构和复杂的判断逻辑。同时,在使用条件跳转指令时,也需要确保程序的正确性和安全性,避免出现意外的逻辑错误和内存访问异常。
汇编代码比较通俗易懂的形式:
1 | cmp a,b // 比较a与b |
helloupx11[三叶草](单步步过)
ESP定律
esp也就是栈顶,ESP定律的核心是堆栈平衡,就是在程序执行压缩壳的时候,会把壳的OEP压入栈中,然后当加载完壳之后,又会释放出壳的入口点然后重新把源程序的OEP压入栈中,在脱壳的时候就可以通过查看栈的变化而定位到源程序的入口点然后jump出来源程序实现脱壳,具体的汇编操作是:pushad:是将所有32位通用寄存器压入堆栈
IAT
( lmportAddress Table),在可执行文件中使用其他DLL可执行文件的代码或数据,称为导入或者输入。
在脱壳的过程中,导入表是很重要的东西,程序加壳的同时也改变了IAT表,所以在脱壳的时候也一定要修复IAT。如果找到了源程序的OEP,但是无法正常运行,那就是要修复程序的IAT了。
OEP
OEP,即Original Entry Point,是程序的入口点,一般在程序中就是刚开始执行的位置,在脱壳的过程中,就要通过动调去跳过壳的程序然后找到源程序的OEP入口,然后再把程序给dump出来(类似于把程序和壳给分开,只取壳),就实现了脱壳
断点
硬件断点:硬断点需要硬件寄存器提供支持,断点的数目受Embedded ICE中的Watchpoint数目的限制,但是可以在任何地方设置断点。
软件断点:软件断点通过在运行起来的程序中设置特征值实现,其数目不受限制,但是一般情况下软件断点只能在可写的存储器的地址中设置
通俗来说,断点分为软件断点和硬件断点,软件断点就是平常在ida中设置的断点,他的本质是给原来的程序中额外增加了一段程序,由于加壳的时候会对程序进行压缩,所以在ida中添加到软件断点的程序也会被压缩变成一些不可识别的数据,所以就无法实现间断。而硬件断点就没有这样的问题,他是基于硬件寄存器的基础上设置断点,就不会因为壳而受影响,可以理解成运行到那个寄存器的地址就停止的断点
一些常见的汇编指令:
Pushad
Pushad 是一种汇编指令,用于将当前 CPU 上下文中的所有通用寄存器(eax、ebx、ecx、edx、esi、edi、ebp、esp)依次入栈。该指令常用于保存当前进程的上下文并开辟新的堆栈帧,以便调用子程序。
具体地说,当执行 Pushad 指令时,CPU 首先将 eax 寄存器的值压入堆栈中,然后是 ebx、ecx、edx、esi、edi、ebp 和 esp 寄存器的值,每个寄存器占据 4 个字节的空间。在最后一个寄存器被压入堆栈之后,esp 寄存器的值减去了 32,指向当前堆栈顶部。这意味着堆栈上已经为每个寄存器分配了 4 个字节的空间,以便稍后可以使用 Popad 指令来还原它们的值。
总体来说,Pushad 和 Popad 指令通常是成对出现的,而且只用于 32 位模式下的 x86 架构。通过使用这些指令,汇编程序能够更加方便地管理进程的上下文和堆栈,从而实现复杂的运算和逻辑处理。
Pushfd
Pushfd 是一种汇编指令,用于将当前 CPU 的标志寄存器(flags register)的值入栈。标志寄存器中包含了一些特殊的位,它们的状态可以反映出 CPU 正处于何种工作状态,或者最近所发生的某些事件。例如,ZF(零标志位)和 SF(符号标志位)分别用于表示上次 ALU 操作的结果是否为 0 或负数。
当执行 Pushfd 指令时,CPU 首先将标志寄存器的值压入堆栈中,然后将 esp 寄存器的值减去 4,使其指向新的栈顶。在这个过程中,标志寄存器的值会被压缩成一个 32 位的无符号整数,并存储到堆栈中,以便稍后使用 Popfd 指令来还原它。
Pushfd 指令通常用于保存 CPU 运行环境中一些重要的状态信息,例如中断允许标志、虚拟内存开关状态等。它可以与 Popfd 指令一起使用,用于保存和还原程序执行之前的 CPU 状态信息。需要注意的是,Pushfd 和 Popfd 指令只能在特权级别为 0 或 1 的代码段中使用,在用户模式下会引发异常。
ret
Ret 是一种汇编指令,用于从子程序中返回到主程序。它的作用是将栈顶的数据弹出并赋值给程序计数器 PC(program counter),以便程序跳转回调用该子程序的位置继续执行。
具体来说,Ret 指令会先从堆栈中弹出一个 16/32 位的地址值,该值通常用于存储主程序返回时应该跳转到哪里继续执行。然后,这个地址值会被赋值给程序计数器 PC,使得 CPU 能够执行主程序中接下来的指令。
在使用 Ret 指令时需要注意栈的维护问题。在调用子程序时,主程序通常需要将一些参数通过入栈传递给子程序,并且还需要保存一些寄存器状态等运行环境信息。当子程序执行完毕后,它所使用的这些资源都需要及时释放,以免造成内存泄漏或其他问题。在一些编译器中,Ret 指令会默认自动生成 Epilogue 代码片段,用于恢复子程序执行前的运行环境和释放它所使用的堆栈空间。
需要注意的是,在使用 Ret 指令时必须确保返回地址存在于堆栈中,否则会发生意料之外的结果。如果主程序预期从一个没有调用过的子程序中返回,可以通过调用类似于 Nop 指令的占位符代码来确保堆栈上存在一个有效的返回地址。
Call
Call 是一种汇编指令,用于将程序执行流转移到一个子程序中执行,并记录下一个返回地址。具体来说,Call 指令首先会将当前 PC 的值(即 Call 指令的下一条指令地址)压入堆栈顶部,然后将跳转至目标子程序的入口地址更新到 PC 中,从而实现程序流程的转移。
当子程序执行完毕后,可以使用 Ret 指令将运行流程返回到 Call 指令下方对应的那条指令继续执行。Ret 指令会自动从堆栈顶部弹出保存在其中的返回地址,接着将其写入 PC 中,实现程序执行流程的“回溯”。
Call 指令常被用于程序结构化设计中,可以将复杂的任务分解成多个较小且功能单一的子过程,提高程序控制的模块性和可读性。此外,Call 指令还适用于各种其他场景,例如函数调用、异常处理、代码重用等等。需要注意的是,在使用 Call 指令时需要合理维护堆栈,避免堆栈溢出、栈内数据损坏等问题的发生。
Jump
Jump指令是一种汇编指令,用于实现程序代码的跳转。当CPU执行到Jump指令时,它会根据该指令中给出的目标地址,直接改变当前正在执行的程序指令的地址,从而将控制权转移到新的位置。这种操作既可以用于让程序跳转到不同的程序分支上执行,也可以用于实现循环、函数调用等复杂的程序控制结构。
具体来说,Jump指令通常是直接跳转到另外一个已知的程序地址或者是相对于当前PC寄存器的偏移量地址。在跳转过程中,CPU和内存需要处理好跳转前后内存状态以及保持堆栈的正确性,以免发生未知错误。因此,在使用Jump指令时,需要小心谨慎地编写代码,并且需要对其效果进行充分地测试和验证。
Je
“je”是汇编指令中的一种条件跳转指令,可以实现根据特定的条件来进行程序跳转的功能。 “je”全称为”Jump if Equal”(如果相等就跳转),它的作用是判断前面的运算结果是否等于零,如果等于则跳转到指定地址执行后面的指令,否则就顺序执行后面的指令。
具体来说,在使用”je”指令时,需要先进行一些运算或者比较操作,然后对比结果进行判断,并根据判断的结果来决定是否跳转到指定的目标地址。例如,当我们要判断两个数字是否相等时,可以通过使用”cmp”指令比较它们的大小关系,然后用”je”指令来判断它们是否相等,并根据”je”指令的跳转结果来选择接下来执行的代码块。
需要注意的是,因为每个指令都会占用CPU的执行时间和计算资源,所以在实际应用中,应该尽可能地把程序设计成简单明了的结构,避免过多的分支结构和复杂的判断逻辑。同时,在使用条件跳转指令时,也需要确保程序的正确性和安全性,避免出现意外的逻辑错误和内存访问异常。
汇编代码比较通俗易懂的形式:
1 | cmp a,b // 比较a与b |
helloupx11[三叶草](单步步过)
找OEP特征的函数
先查壳,发现是64位,并且是elf文件,无法加载进入dbg调试,所以只能用ida了,ida远程动调需要在虚拟机开放端口
和在dbg一样的操作,找到一个自带的入口断点,然后手动设置一下断点,开始动调
简单描述一下单步步过的具体操作流程
单步步过就是频繁使用F4、F7、F8这三个快捷键实现逐步调试程序,找到函数之前的跳转关系一步一步一个一个函数地往下走,知道找到有函数OEP程序的特征代码的时候再开始扒代码,当然如果是在ida中的话是可以直接
看代码进行操作的
先贴一个各位代码入口点大佬写的博客:
然后就开始按f8单步步过去执行函数或者也可以直接跳过,就去找retn函数,按f4直接执行到这个函数然后f7单步步入(因为retn函数可以理解成return,就是返回主函数的意思,单步步入是进入上一级函数,然后实现一级一级的跳转最终回到主函数然后走出壳的代码找到真正的函数,这个操作就类似于在ida中按x返回上一级函数类似)
注意!,是retn,ret retn是不可以的,他俩不是一个东西
然后找到retn之后,就选中这个函数然后f4,程序就跳转到这一步了,之后就是f7单步步入了(执行并进入)
进去之后口到了这个函数界面,可以看到这里是没有retn函数的,也没关系,一直步过就好,可能就是这个函数内部也有跳转函数,遇到jump函数就单步步过就可以了,继续往下运行
如果步入的话就跳转了,就可能会跳出我们的控制范围
至于下面为什么会有一大堆的数据,这里也解释一下:
因为压缩壳的缘故,所以程序被压缩成了ida无法识别的数据,所以ida就会认为这些不是代码是数据,因此就是数据的形式,下面也不会有retn函数,所以就跟着jump步过就好
这里也是一样,遇到call指令,可以和jump指令一样就步过就好,不然也是会转跳到call里面的子程序,也会导致程序跳出可控范围
然后又是一个没有retn的函数,是一个jump,步过一下
到这里基本上就是很接近最终函数了,然后retn步入一下就找到了程序的一个包含程序入口特征的代码
程序入口点特征:https://www.52pojie.cn/thread-1640646-1-1.html
另外这里其实就是很明显的linux程序的很正入口,可以随便找一个linux系统的程序然后拖到ida中看看
我这里随便找了个国赛pwn的shaokao题程序进去随便看了看,基本上就可以认定那个程序就是入口点了
然后在入口点跳转的程序是 unk_401D55
双击到程序中:
此时程序还是数据的状态,但是我们知道,通过一步一步的步入,我们已经走出了壳程序的压缩程序了,所以我们就可以自己进行编译了,快捷键c或者右击code,然后就反编译回了正常的代码段
后选中函数按p键重新编译一下函数,这时候函数就变成了我们平时真正的正常的函数,就可以f5反编译了
终于变成了最正常的函数了!
终于变成了最正常的函数了!
分析代码(略)
由于这个题主要的还是脱壳,所以关键代码就是一眼就你看出来的,结束!
攻防世界:crackme(esp定律)
寻找OEP
首先查壳,nsapck壳,利用esp脱壳定律手脱一下
拖动到32位的dbg中,有四块比较重要的面板
①是反汇编串口,里面是程序的汇编代码
②是寄存器窗口,用来存储寄存器的状态,一般是用来看堆栈的数值变化比较方便
③是数据窗,就类似于010的二进制形式
④是堆栈窗口
ida快捷键
1 | a:将数据转换为字符串 |
另外还有一些dbg的快捷键:
1 | F2:下断点,指定断点的地址。 |
单步步过(F7):一行一行执行程序
单步步入(F8):它与单步步过几乎一样,除了遇到函数时:遇到函数时,F7的作用是将函数作为一个整体跨过,而F8是会进入到函数内部(如果有源码的话)
然后单机断点,去查看程序最初始的断点,是一个pushfd的指令,去转跳到这个断点(双击)
单步步过之前看一下ESP栈顶的情况,看看数值:0019f7f4
然后看到了pushad和pushfd等压入栈的操作,然后选择断点右击,选择设为新的原点,让程序在此处开始运行,然后f8单步步过一下
然后单步步过之后,ESP的数值发生变化
这里涉及到压缩壳的运行过程:
压缩壳就是一段程序,他把程序压缩,然后再运行程序的时候,先运行壳的解压程序,把源程序解压成正常的代码形式,然后再开始运行。所以正常来说,会把壳的OEP压入栈顶ESP,然后当壳解压完成之后,就会释放壳的加密程序,所以就会导致栈顶的数据发生改变,一般情况下,这个改变之后的数值,就是源程序的oep,所以在这之后就可以看到正常的程序了,这点也类似于smc
此时就找到了改变之后的ESP数值:0017F7F0
此时就找到了源程序的OEP
脱壳
右击所找到的OEP,右击在转储中跟随
随后在左下角的数据窗口就转到了OEP的位置,下一个硬件断点
右击‘46’ –>断点 –>硬件,存取 –>字节
然后按F9一直运行到刚刚设置的硬件断点处停止,此时是壳的程序已经运行完了,然后到了源程序的OEP处停止,可以看这个jump指令上面的指令,是两个pop指令,也对应了所说的壳运行结束时把程序移出栈
然后就是脱壳了,使用dbg自带的插件csylla,把jump转跳到地址修改到OEP,修改程序的入口
这里就是把401336填到OEP的位置,这里crack.401336是因为我的程序名字叫crack,401336才是入口
然后点击转储,也就是所谓的dump程序,把程序给转出来,这样就实现了脱壳
修复IAT
程序脱壳之后,但是原来的IAT还是修改之后的样子,所以就要修复一下IAT,点击IAT自动搜索,然后点击获取导入,修改IAT表
然后再选择修复转储,选择刚刚dump出的程序,修复IAT表,这时就得到了一个又syc的文件,就是脱壳之后的程序,可以拖到die重新查壳
此时脱壳结束
syclla中操作总结:
分析代码
可以对比一下脱壳前后在ida中的变化:
脱壳前
脱壳后
看一下主函数:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
然后可以在ida中查看到key和flag的数值:
解题脚本:
1 | key1="this_is_not_flag" |
另外贴一个大佬写的esp定律比较全面的帖子: