Cool-Y.github.io/source/_posts/逆向工程实验.md
2019-05-07 19:34:30 +08:00

16 KiB
Raw Permalink Blame History

title date tags categories
逆向工程与软件破解 2019-03-28 15:25:04
逆向
破解
二进制

软件保护方式

  1. 功能限制
  2. 时间限制
  • 运行时长限制
  • 使用日期限制
  • 使用次数限制
  1. 警告窗口

分析工具

  1. 静态分析工具
  • IDA
  • W32Dasm
  • lordPE
  • Resource Hacker
  1. 动态分析工具
  • OllyDbg
  • WinDbg

对抗分析技术

  1. 反静态分析技术
  • 花指令
  • 自修改代码技术
  • 多态技术
  • 变形技术
  • 虚拟机保护技术
  1. 反动态分析技术
  • 检测调试状态
  • 检测用户态调试器
  • 检测内核态调试器
  • 其他方法父进程检测StartupInfo 结构时间差通过Trap Flag检测
  1. 发现调试器后的处理
  • 程序自身退出
  • 向调试器窗口发送消息使调试器退出
  • 使调试器窗口不可用
  • 终止调试器进程

PE文件格式基础


加壳脱壳


反调试技术

反调试技术,程序用它来识别是否被调试,或者让调试器失效。为了阻止调试器的分析,当程序意识到自己被调试时,它们可能改变正常的执行路径或者修改自身程序让自己崩溃,从而增加调试时间和复杂度。

探测windows调试器

  1. 使用windows API 使用Windows API函数探测调试器是否存在是最简单的反调试技术。 通常防止使用API进行反调试的方法有在程序运行期间修改恶意代码使其不能调用API函数或修改返回值确保执行合适的路径还有挂钩这些函数。 常用来探测调试器的API函数有IsDebuggerPresent CheckRemoteDebuggerPresent NtQueryInformationProcess OutputDebuggString
  2. 手动检测数据结构 程序编写者经常手动执行与这些API功能相同的操作
  • 检查BeingDebugged属性
  • 检测ProcessHeap属性
  • 检测NTGlobalFlag
  1. 系统痕迹检测 通常我们使用调试工具来分析程序但这些工具会在系统中驻留一些痕迹。程序通过搜索这种系统的痕迹来确定你是否试图分析它。例如查找调试器引用的注册表项。同时程序也可以查找系统的文件和目录查找当前内存的痕迹或者查看当前进程列表更普遍的做法是通过FindWindows来查找调试器。

识别调试器的行为

在逆向工程中可以使用断点或单步调试来帮助分析但执行这些操作时会修改进程中的代码。因此可以使用几种反调试技术探测INT扫描、完整性校验以及时钟检测等几种类型的调试器行为。

  1. INT扫描 调试器设置断点的基本机制是用软件中断INT 3机器码为0xCC临时替换程序中的一条指令。因此可以通过扫描INT 3修改来检测。
  2. 执行代码校验和检查 与INT扫描目的相同但仅执行机器码的CRC或MD5校验和检查。
  3. 时钟检测 被调试时,进程的运行速度大大降低,常用指令有:rdstc QueryPerformanceCounter GetTickCount,有如下两种方式探测时钟:
  • 记录执行一段操作前后的时间戳
  • 记录触发一个异常前后的时间戳

干扰调试器的功能

本地存储(TLS)回调TLS回调被用来在程序入口点执行之前运行代码这发生在程序刚被加载到调试器时 使用异常使用SEH链可以实现异常程序可以使用异常来破坏或探测调试器调试器捕获异常后并不会将处理权立即返回给被调试进程。 插入中断插入INT 3、INT 2D、ICE

调试器漏洞

PE头漏洞、OutputDebugString漏洞

实验一:软件破解

对象

crack.exe28.0 KB

  • 无保护措施:无壳、未加密、无反调试措施
  • 用户名至少要5个字节
  • 输入错误验证码时输出“Bad Boy!”

爆破

查找显示注册结果相关代码

当输入错误验证码时程序会输出“Bad Boy”因此我们将程序拖入IDA以流程图显示函数内部的跳转。查找“Bad Boy”字符串我们可以定位到显示注册结果的相关代码

查找注册码验证相关代码

用鼠标选中程序分支点,按空格切换回汇编指令界面

可以看到这条指令位于PE文件的.text节并且IDA已经自动将地址转换为运行时的内存地址VA:004010F9

修改程序跳转

  • 现在关闭IDA换用OllyDbg进行动态调试来看看程序时如何分支跳转的Ctrl+G直接跳到由IDA得到的VA:004010F9处查看那条引起程序分支的关键指令
  • 选中这条指令按F2设置断点再按F9运行程序这时候控制权会回到程序OllyDbg暂时挂起。到程序提示输入名字和序列号随意输入名字大于五个字节点击ok后OllyDbg会重新中断程序收回控制权如图
  • 验证函数的返回值存于EAX寄存器中if语句通过以下两条指令执行
cmp eax,ecx
jnz xxxxxxx
  • 也就是说当序列号输入错误时EAX中的值为0跳转将被执行。 如果我们把jnz这条指令修改为jz,那么整个程序的逻辑就会反过来。 双击jnz这条指令,将其改为jz,单击"汇编"将其写入内存 可以看到此时程序执行了相反的路径

  • 上面只是在内存中修改程序我们还需要在二进制文件中也修改相应的字节这里考察VA与文件地址之间的关系

  • 用LordPE打开.exe文件查看PE文件的节信息 根据VA与文件地址的换算公式

文件偏移地址 = VA - Image Base - 节偏移
            = 0x004010F9 - 0x00400000 - 0
            = 0x10F9

也就是说这条指令在PE文件中位于10F9字节处使用010Editer打开crack.exe将这一字节的75(JNZ)``改为74(JZ)``,保存后重新执行,破解成功!

编写注册机

查找显示注册结果相关代码

通过查找字符串“good boy”等我们可以找到显示注册结果的相关代码

查找注册码验证相关代码

因为检测密钥是否正确时会将结果返回到EAX寄存器中因此在检测密钥前必然会对EAX寄存器清空由此我们可以找到注册码验证的相关代码。

根据注册码验证代码编写注册机

分析上图算法按tab键转换为高级语言

for ( i = 0; i < v6; v12 = v10 )
  v10 = (v6 + v12) * lpStringa[i++];
if ( (v12 ^ 0xA9F9FA) == atoi(v15) )
  MessageBoxA(hDlg, aTerimaKasihKer, aGoodBoy, 0);

可以看出生成注册码主要在for循环中完成之后将生成的注册码与输入相比较判断是否正确。 所以,只要能弄明白v6v12v10v15的含义,我们就可以轻松的编写注册机。 打开ollybdg在进入循环之前设下断点动态调试程序

004010CC  |> /8B4D 10       |mov ecx,[arg.3]  //此时ecx为name
004010CF  |.  8B55 0C       |mov edx,[arg.2]  //edx为0x1908
004010D2  |.  03D3          |add edx,ebx      //edx加上name的长度ebx
004010D4  |.  0FBE0C08      |movsx ecx,byte ptr ds:[eax+ecx]  //ecx=61h
004010D8  |.  0FAFCA        |imul ecx,edx     //61h(a) * edx
004010DB  |.  40            |inc eax          //eax加1初始为0
004010DC  |.  894D 0C       |mov [arg.2],ecx
004010DF  |.  3BC3          |cmp eax,ebx      //循环是否结束

arg.3为输入的namearg.2初始为0x1908ebxname的长度,eax每次循环加1直到等于长度 因此,我们可以对参数的含义进行解释如下

v12 = 6408;   //0x1908
v10 = 6408;   //0x1908
v6 = len(name);
v12 = input_serial;
for ( i = 0; i < v6; i++ ){
  v12 = v10
  v10 = (v6 + v12) * lpStringa[i];
}
if ((v12 ^ 0xA9F9FA) == atoi(v15)){
  MessageBoxA(hDlg, aTerimaKasihKer, aGoodBoy, 0);
}

可见,v12^0xA9F9FA的结果即是正确的注册码,我们编写一个简单的程序帮助我们生成注册码:

#include <iostream>
#include<stdio.h>

using namespace::std;
int main(){
	int v12;
	int v10 = 6408;   //0x1908
	string name;
	cout << "请输入name:  ";
	cin >> name;
	int len = name.size();
	for(int i = 0; i < len+1; i++ ){
  		v12 = v10;
  		v10 = (len + v12) * name[i];
	}
 	cout<<"\n"<<"注册码为:  "<<(v12 ^ 0xA9F9FA)<<endl;
 	return 0;
}

计算出"testname"的对应注册码 注册成功!

实验二:软件反动态调试技术分析

对象

CrackMe1.exe 1641.0 KB 无保护措施:无壳、未加密、无反调试措施 使用OllyDbg对该程序进行调试时程序会自动退出

要求

  1. 分析CrackMe1.exe是如何通过父进程检测实现反OllyDbg调试的
  2. 分析除父进程检测外,该程序用到的反动态调试技术

父进程检测

一般双击运行的进程的父进程都是explorer.exe但是如果进程被调试父进程则是调试器进程。也就是说如果父进程不是explorer.exe则可以认为程序正在被调试。

BOOL IsInDebugger(){
  HANDLE     hProcessSnap = NULL;
  char Expchar[] ="\\EXPLORER.EXE";
  char szBuffer[MAX_PATH]={0};
  char FileName[MAX_PATH]={0};
  PROCESSENTRY32 pe32   = {0};

  hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); //得到所有进程的列表快照
  if (hProcessSnap == INVALID_HANDLE_VALUE)
      return FALSE;          

  pe32.dwSize = sizeof(PROCESSENTRY32);

  if (!Process32First(hProcessSnap, &pe32))  // 查找进程
  {
      CloseHandle (hProcessSnap);
      return FALSE;
  }

  do // 遍历所有进程
  {
      if(pe32.th32ProcessID==GetCurrentProcessId() )//判断是否是自己的进程?
        {
          HANDLE hProcess = OpenProcess (PROCESS_ALL_ACCESS, FALSE, pe32.th32ParentProcessID); //打开父进程
          if (hProcess)
            {
              if (GetModuleFileNameEx(hProcess, NULL, FileName,  MAX_PATH) ) // 得到父进程名
                  {
                    GetWindowsDirectory(szBuffer,MAX_PATH); //得到系统所在目录
                    strcat(szBuffer,Expchar);            //组合成类似的字串D:\Winnt\Explorer.EXE
                    if(strcmpi (FileName,szBuffer))  // 比较当前是否为Explorer.EXE进程
                      {
                        return TRUE;   // 父进程若不是Explorer.EXE则是调试器
                      }
                    else
                      {
                        return FALSE; // 无法获得进程名
                      }
                    CloseHandle (hProcess);
                  }
              else
                {
                  return FALSE;//无权访问该进程
                }
            }
        }
        while (Process32Next(hProcessSnap, &pe32));
          CloseHandle (hProcessSnap);
          return FALSE;
  }

由上述示例代码我们可以看到父进程检测中调用了GetCurrentProcessId函数来判断。 因此在Ollydbg中首先找到GetCurrentProcessId模块Ctrl+N然后设置断点 查看断点是否设置成功 运行该程序,在断点00401932停下打开任务管理器CrackMe1的pid为4020=0xFB4 程序在调用完GetCurrentProcessId后pid被放入EAX寄存器中值为0xFB4 然后调用Openprocess函数其参数processId为0xFB4返回进程CrackMe1的句柄 通过ntdll.dll中的LoadLibraryA和GetProcAddress函数找到NtQueryInformationProcess:

PNTQUERYINFORMATIONPROCESS  NtQueryInformationProcess = (PNTQUERYINFORMATIONPROCESS)GetProcAddress(GetModuleHandleA("ntdll"),"NtQueryInformationProcess");  

用OpenProcess获得的句柄设置NtQueryInformationProcess的对应参数然后调用NtQueryInformationProcess从其返回值中可以获取到CrackMe1.exe的父进程PID=0xDB4=3508,在任务管理器中查看进程名确实是ollydbg 然后再次调用openprocess获得父进程的句柄 最后调用GetModuleFileNameExA通过OpenProcess返回的句柄获取父进程的文件名 至此成功获取到父进程的文件名接下来将进行父进程文件名与“c:\windows\explore.exe”的字符串比较。 EDX中保存explorer字符串ESI中保存ollydbg字符串 然后进入循环逐位比较比较流程是首先取esi中第一个字符到eax将EAX的值减去41然后存入exc中并与19比较大小判断是否大写若是则eax加上20转化为小写转化为小写之后对edx中的字符做同样操作然后test eax eax判断是否比较完毕若没有则逐个比较直到遇到不相等的字符。

其他检测

用EnumWindows枚举所有屏幕上的顶层窗口并将窗口句柄传送给应用程序定义的回调函数此处的回调函数调用了GetWindowTextA将指定窗口的标题栏如果有的话的文字拷贝到缓冲区内 将得到的窗口标题与”ollydbg”等进行比较看是否为调试器。


实验三:加花加密反调试技术分析

对象

CrackMe2.exe 9.00 KB 保护措施:部分加花、部分加密、简单反调试 根据提示

内容

  1. 加壳脱壳深入理解
  2. 尝试手动脱壳
  3. 分析CrackMe2.exe中花指令
  4. 分析CrackMe2.exe中的被加密的函数的功能
  5. 分析CrackMe2.exe中的反调试手段
  6. 分析CrackMe2.exe中混合的64位代码的功能