用Ghidra打破简单的“裂缝”-第1部分

许多人可能已经第一手知道这是哪种野兽-Ghidra (“九头蛇”),以及它是如何用第一手吃掉该程序的,尽管该工具仅在今年3月才公开提供。 我不会对Hydra,其功能等的描述打扰读者。 我敢肯定,那些已经成为主题的人已经自己研究了这些,而那些还没有成为主题的人可以随时进行研究,因为现在可以很容易地在Internet上找到详细的信息。 顺便说一下,Habra(它的插件开发)的一个方面已经在Habré(出色的文章!)中进行了介绍。我将仅给出主要链接:


因此,Hydra是一款具有模块化结构免费跨平台交互式反汇编程序和反编译器,它支持几乎所有主要的CPU架构,并且具有灵活的图形界面,可用于处理反汇编代码,内存,恢复的(反编译)代码,调试符号等

让我们尝试用这款九头蛇打破一些东西!

步骤1.找到并研究裂纹


作为“受害者”,我们找到了一个简单的“破解”程序。 我只是去了cramess.one ,在搜索中指出了难度级别= 2-3(“简单”和“中等”),程序的源语言=“ C / C ++”,平台=“ Multiplatform”,如下面的屏幕截图所示:



搜索返回2个结果(下面的绿色)。 第一次破解是16位的,没有在我的Win10 64位上启动,但是第二次破解seveb的level_2 )出现了。 您可以从此链接下载它。

下载并解压破解包; 如站点上所示,存档的密码为cramess.de 。 在档案中,我们找到了与Linux和Windows对应的两个目录。 在我的机器上,我进入Windows目录,在其中遇到了唯一的“可执行文件” -level_2.exe 。 让我们跑步,看看她想要什么:



看起来真是个无赖! 在启动时,该程序不显示任何内容。 我们尝试再次运行它,将一个任意字符串作为参数传递给它(突然间,它在等待键吗?)-再也没有什么……但是不要绝望。 假设我们还必须找出启动参数作为任务! 是时候揭开我们的“瑞士刀”了-九头蛇。

步骤2.在Hydra中创建一个项目并进行初步分析


假设您已经安装了Hydra。 如果还没有,那么一切都很简单。

安装Ghidra
1)安装JDK版本11或更高版本(我有12

2)下载Hydra(例如, 从此处 )并安装(在撰写本文时,Hydra的最新版本是9.0.2,我有9.0.1)

我们启动Hydra,并在打开的项目管理器中立即创建一个新项目。 我给它起名为crackme3 (即已经为我创建了crackme和crackme2项目)。 该项目实际上是文件目录,您可以向其中添加任何文件以进行研究(exe,dll等)。 我们将立即添加我们的level_2.exe( 文件|导入或只是I键):



我们看到,在导入之前,Hydra将我们的实验用程序确定为Win32 OS和x86平台的32位PE(便携式可执行文件)。 导入后,我们正在等待更多信息:



在这里,除了前面提到的位深度之外,我们可能仍然对字节顺序感兴趣,在我们的例子中, 字节顺序Little (从低字节到高字节),这是英特尔第86平台所期望的。

通过初步分析,我们完成了。

步骤3.执行自动分析


是时候在Hydra中开始对该程序进行全自动分析了。 通过双击相应的文件(level_2.exe)来完成此操作。 Hydra具有模块化结构,可通过可独立添加/禁用或开发的插件系统提供其所有基本功能。 分析也是如此-每个插件都负责其分析类型。 因此,首先,我们面对这个窗口,您可以在其中选择感兴趣的分析类型:

分析设置窗口

就我们的目的而言,保留默认设置并运行分析是有意义的。 尽管论坛上的用户抱怨说,对于大型项目,Hydra失去了IDA Pro的速度,但分析本身的执行速度非常快(花了我大约7秒钟)。 这可能是正确的,但对于小文件,此差异并不明显。

至此,分析完成。 其结果显示在“代码浏览器”窗口中:



该窗口是在Hydra中工作的主要窗口,因此您应仔细研究。

代码浏览器界面概述
默认界面设置将窗口分为三部分。

中央部分是主窗口-反汇编程序的列表,它或多或少类似于IDA,OllyDbg等中的“兄弟”。 默认情况下,该列表中的列是(从左到右):内存地址,命令的操作码,ASM命令,ASM命令的参数,交叉引用(如果适用)。 当然,可以通过单击此窗口工具栏中的砖墙形式的按钮来更改显示。 老实说,我从未在任何地方看到过如此灵活的反汇编程序输出配置,这非常方便。

在3面板的左侧

  1. 程序的各个部分(单击鼠标可在各个部分中移动)
  2. 字符树(导入,导出,函数,标题等)
  3. 使用变量的类型树

对于我们来说,这里最有用的窗口是符号树,它使您可以快速找到例如功能名称的函数并转到相应的地址。

右侧是反编译代码的列表(在我们的示例中为C)。

除了默认窗口外,您还可以在“ 窗口”菜单中选择并放置许多其他窗口,并在浏览器中的任何位置显示。 为了方便起见,我在中间添加了一个字节窗口和一个带有函数图的窗口,在右边添加了字符串变量(Strings)和函数表(Functions)。 这些窗口现在在单独的选项卡中可用。 另外,任何窗户都可以拆卸并“浮动”,并根据自己的意愿放置和调整它们的尺寸-我认为这也是一个非常周到的解决方案。

步骤4.学习程序算法-main()函数


好吧,让我们继续直接分析我们的破解程序。 在大多数情况下,您应该先搜索程序的入口点,即 启动时调用的主要功能。 知道我们的破解是用C / C ++编写的,我们猜测main函数的名称将是main()或类似的名称:)说完了。 在符号树的过滤器中(在左侧面板中)输入“ main”,然后在“ 功能”部分中查看函数_main() 。 单击鼠标转到它。

main()函数概述和重命名晦涩的函数


在反汇编程序列表中,将立即显示相应的代码部分,然后在右侧我们看到此函数的反编译C代码。 Hydra的另一个便利功能是同步选择:当鼠标选择一系列ASM命令时,反编译器中的相应代码部分将突出显示,反之亦然。 另外,如果打开了内存查看窗口,则分配与内存同步。 正如他们所说,所有的创意都很简单!

我马上注意到在Hydra工作的一个重要特征(与IDA相对)。 Hydra的工作主要集中在分析反编译的代码 。 因此,Hydra的创建者(我们记得-我们所说的是来自NSA的间谍:)非常注重反编译的质量和使用代码的便利性。 特别是,只需双击代码即可简单地继续定义函数,变量和内存部分。 另外,任何变量和函数都可以立即重命名,这非常方便,因为默认名称没有含义并且可能会造成混淆。 正如您稍后将看到的,我们将经常使用这种机制。

因此,这里是main()函数,Hydra对其进行了“解剖”,如下所示:

列出主要()
int __cdecl _main(int _Argc,char **_Argv,char **_Env) { bool bVar1; int iVar2; char *_Dest; size_t sVar3; FILE *_File; char **ppcVar4; int local_18; ___main(); if (_Argc == 3) { bVar1 = false; _Dest = (char *)_text(0x100,1); local_18 = 0; while (local_18 < 3) { if (bVar1) { _text(_Dest,0,0x100); _text(_Dest,_Argv[local_18],0x100); break; } sVar3 = _text(_Argv[local_18]); if (((sVar3 == 2) && (((int)*_Argv[local_18] & 0x7fffffffU) == 0x2d)) && (((int)_Argv[local_18][1] & 0x7fffffffU) == 0x66)) { bVar1 = true; } local_18 = local_18 + 1; } if ((bVar1) && (*_Dest != 0)) { _File = _text(_Dest,"rb"); if (_File == (FILE *)0x0) { _text("Failed to open file"); return 1; } ppcVar4 = _construct_key(_File); if (ppcVar4 == (char **)0x0) { _text("Nope."); _free_key((void **)0x0); } else { _text("%s%s%s%s\n",*ppcVar4 + 0x10d,*ppcVar4 + 0x219,*ppcVar4 + 0x325,*ppcVar4 + 0x431); _free_key(ppcVar4); } _text(_File); } _text(_Dest); iVar2 = 0; } else { iVar2 = 1; } return iVar2; } 


似乎一切似乎都很正常-变量的定义,标准C类型,条件,循环,函数调用。 但是仔细看一下代码,我们注意到由于某些原因,某些函数的名称未定义,而是由伪函数_text()代替(在反编译器窗口中为.text() )。 让我们从定义这些功能开始。

双击第一个通话的正文

  _Dest = (char *)_text(0x100,1); 

我们看到这只是标准calloc()函数的包装函数,该函数用于为数据分配内存。 因此,让我们将该函数重命名为calloc2() 。 将光标放在函数标题上,调用上下文菜单,然后选择“ 重命名函数” (热键-L ),然后在打开的字段中输入新名称:



我们看到该函数被立即重命名。 我们回到主体()主体(工具栏上的“ 后退”按钮或Alt + <- ),我们看到这里已经代替了神秘的_text()calloc2() 。 太好了!

我们对所有其他包装器函数都执行相同的操作:我们逐一检查它们的定义,看一下它们的作用,将它们重命名(我在C函数的标准名称中添加了索引2)并返回到主函数。

我们理解了main()函数代码


好的,我们发现了一些奇怪的功能。 我们开始研究主要功能的代码。 跳过变量声明,我们看到仅当满足字符串指定的条件时,函数才返回变量iVar2的值,该值是零(函数成功的标志)。

 if (_Argc == 3) { ... } 

_Argc是传递给main()的命令行参数(参数)的数量。 也就是说,我们的程序“吃”了2个参数(我们记得,第一个参数始终是可执行文件的路径)。

好的,让我们继续。 在这里,我们创建一个由256个字符组成的C字符串( char数组):

 char *_Dest; _Dest = (char *)calloc2(0x100,1); //  new char[256]  C++ 

接下来,我们有一个3次迭代的循环。 在其中,我们首先检查是否设置bVar1标志如果已设置,则将以下命令行参数(字符串)复制到_Dest

 while (i < 3) { /*    .  */ if (bVar1) { /*   */ memset2(_Dest,0,0x100); /*    _Dest    */ strncpy2(_Dest,_Argv[i],0x100); break; } ... } 

解析以下参数时设置此标志:

 n_strlen = strlen2(_Argv[i]); if (((n_strlen == 2) && (((int)*_Argv[i] & 0x7fffffffU) == 0x2d)) && (((int)_Argv[i][1] & 0x7fffffffU) == 0x66)) { bVar1 = true; } 

第一行计算此参数的长度。 此外,条件检查参数的长度必须为2,倒数第二个字符==“-”,最后一个字符==“ f”。 请注意,反编译器如何使用字节掩码“转换”从字符串中提取字符。
可以通过将光标放在相应的十六进制文字上来监视数字的十进制值以及相应的ASCII字符。 ASCII映射并不总是有效(?),因此我建议查看Internet上的ASCII表。 您也可以直接在Hydra中将标量从任何数字系统转换为任何其他数字系统(通过上下文菜单-> Convert ),在这种情况下,该数字将显示在所选数字系统的任何位置(在反汇编程序和反编译器中); 但就我个人而言,我更喜欢在代码中保留十六进制以保证工作的和谐,因为 内存地址,偏移量等 十六进制无处不在。
循环后出现以下代码:

 if ((bVar1) && (*_Dest != 0)) { /*    1) "-f"  2)  -         */ _File = fopen2(_Dest,"rb"); if (_File == (FILE *)0x0) { /*  1    */ perror2("Failed to open file"); return 1; } ... } 

在这里,我立即添加了评论。 我们检查参数(“ -f path_to_file”)的有效性,然后打开相应的文件(传递的第二个参数,我们将其复制到_Dest)。 该文件将以二进制格式读取,如fopen()函数的“ rb”参数所示。 如果读取失败(例如,文件不可用),则在stderror流中显示一条错误消息,并且程序退出并显示代码1。

接下来是最有趣的:

  /* !!!     !!! */ ppcVar3 = _construct_key(_File); if (ppcVar3 == (char **)0x0) { /*    ,  "Nope" */ puts2("Nope."); _free_key((void **)0x0); } else { /*    -      */ printf2("%s%s%s%s\n",*ppcVar3 + 0x10d,*ppcVar3 + 0x219,*ppcVar3 + 0x325,*ppcVar3 + 0x431); _free_key(ppcVar3); } fclose2(_File); 

打开的文件描述符( _File )被传递给_construct_key()函数,该函数显然对所寻找的密钥进行验证。 该函数返回一个二维字节数组( char ** ),该数组存储在ppcVar3变量中。 如果阵列为空,则控制台上会显示简洁的“ Nope”(即,我们认为是“ Nope!”),然后释放内存。 否则(如果数组不为空),将显示看似正确的键,并释放内存。 函数结束时,文件描述符关闭,内存释放,并返回iVar2

因此,现在我们意识到我们需要:

1)用正确的密钥创建一个二进制文件;
2)在参数“ -f”之后在裂纹中传递其路径

在本文的第二部分,我们将分析函数_construct_key() ,我们发现该函数负责检查文件中的密钥。

Source: https://habr.com/ru/post/zh-CN447450/


All Articles