逆向分析ISFB银行木马的第一阶段的加载程序(下) - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

逆向分析ISFB银行木马的第一阶段的加载程序(下)

lucywang 逆向破解 2019-03-28 09:16:04
258108
收藏

导语:Ursnif是目前活动最为频繁的银行木马,它也被称为GOZI。在这篇文章中,我将详细介绍如何解压并分析第一阶段加载程序的可执行文件,然后使用这些信息提取第二阶段加载程序DLL,即rpcrt4.dll,我将在后面的文章中对其进行分析。

(接上文)

29.png

看一下该函数,arg_0(XOR密钥)用于Rotate Left指令。下图中,可以看到BL被移动到CL中,它增加1并用于将XOR密钥向左旋转1,这会产生用于解密BSS字符串的最终XOR密钥。

30.png

一旦执行了这些计算,就会得出这个密钥:0x249d730c。虽然可以从调试程序获取字符串并将其复制到IDA示例,但我更喜欢使用Python复制自定义例程。简单地说,该算法混合使用了XOR和rotate right (ROR)指令来解密DWORD中的数据。首先,我要复制BSS部分中的数据。在十六进制编辑程序模式下查看它,选择整个部分(有数据的地方),然后点击编辑 - >导出数据。接下来,我需要解析这些数据,进而进行解密。

31.1.png

31.2.png

要解析它,我将使用python。我们所需要做的就是将十六进制字节除以4,并将结果存储在一个列表中。这意味着每个值都是一个DWORD。按照以下命令,你应该能够得到如图所示的输出。

32.png

接下来,我将使用CyberChef(一款开源网络工具)进行更多分析,由于我替换了字符,因此在每个DWORD之前都有一个0x,因此Python不会将列表视为字符串列表。

33.png

如果一切顺利,此时现在你应该有一个合适的十六进制DWORD列表。为了解密该列表,你只需将其复制到脚本中并确保密钥正确,然后它将被解密并输出原始数据以及整数列表。在下图中,你可以看到脚本的解密过程,不是太复杂。

34.png

一旦解密完成,就会有如下所示的输出。

35.png

现在我可以将解密的字符串复制到IDA,下图中有一个IDA Python脚本,它用数据中的数据字节覆盖dest中的字节。列表数据将包含我的脚本打印到终端的整数值,因此你只需从中复制它并将其粘贴到文档中即可。

36.png

此时,我需要将其作为IDA中的脚本或模块导入,为此你可以通过导入文件或复制文本并将其粘贴到框中来执行此操作。

37.png

在将PatchArr()输入控制台并按下回车键后,脚本应该用解密的字符串覆盖BSS部分的所有字节,此时,你应该看到类似于下图中的内容。

38.png

然后,我可以通过选项->通用->分析->重新分析程序重新分析有效载荷,它应该识别大多数字符串,尽管偶尔会出现错误。

39.png

使用新解密的BSS部分,我们应该能够分析出其余的有效载荷。

一旦字符串被解密,ISFB就会调用一个函数,该函数使用其中一个解密的字符串——IsWow64Process,它将检查系统是否为64位,然后测试结果(如果是x64,则存储在EAX - 1中,如果不是x64,则存储在0中)。如果系统不是64位的,那么下图中的变量var_4将用于EAX的AND操作,此时,它将等于0,这意味着var_4中的值将为0。如果系统是64位的,则自动跳过。不管系统体系结构如何,var_4中的值都会被移动到EAX中,即返回值。

40.png

如果退出函数,我可以看到EAX再次被测试,如果结果不为零,则值1将移动到DWORD dword_405478。由于我运行的系统是64位,dword_405478将包含值1。如果我搜索对此DWORD的交叉引用,可以找到使用它的测试指令,因此我可以确定这是系统架构的一个指示器。

41.png

调用的下一个函数非常复杂,简而言之,此函数负责将地址发送给少数几个API调用。为此,它使用一个预定义值列表,一个“密钥”和一个哈希例程。首先,找出密钥。查看下图,就在hModule中的值移入ECX之前,EAX与二进制文件中的DWORD进行了异或运输,但是在查看此DWORD时,它是空的,这意味着它是动态解析的。

42.png

要找出这个DWORD将包含的内容,我需要找到对它的交叉引用,具体来说,就是寻找一个引用它作为MOV指令中的目标。幸运的是,我在BSS解密函数中发现了它。从下图中,我可以看到来自字符串的DWORD被移动到ECX中,然后在减法和加法指令中进行使用。现在,就可使用反编译器中查看具体的过程。

43.1.png

43.2.png

如下图所示,从它的外观来看,我可以通过执行简单操作来获取所使用的密钥。

43.jpg

我可以通过查看调试程序中的二进制文件来仔细检查这一点,如下图所示。

44.png

在找到密钥后,就来看看哪个API的值要被导入。这很容易找到,因为它已经存在于可执行文件中了。另外,我还可以看到EDI被用作计数器,因为每个循环都将其递增4,直到值达到20,这意味着总共有5个API使用此方法来查找。嵌入列表中的值全部由密钥进行异或,这会产生用于比较的哈希查找值。我可以从中找到对应的哈希算法,并确定查找和存储哪些API。

45.png

45.2.png

由于函数非常大,我将重点介绍调用哈希函数的部分。传递给这个函数的三个参数是; DLL的地址,值0和用于比较的正确哈希。该函数使用基址执行一些计算,以便得到DLL内的导出表。此时,它会遍历每个导出并哈希导出的名称,然后将其与预定义的哈希进行比较。如果哈希值匹配,则该函数将检索导出的API的地址,并使用该地址覆盖预定义的哈希以供以后使用。如果它们不匹配,则该函数将继续进行下一次导出,直到最终发现匹配的哈希值为止。

46.png

46.2.png

查看实际的哈希函数,在调试器中运行该函数之前,很难得出该函数负责哈希的结论,对于初学者尤其如此。从静态分析的角度来看,这个函数中肯定发生了一些事情,因为有多个逻辑指令,但是在进一步分析之前,我们也不能确定这个猜测。

47.png

查看调试器,很明显,[edx*4+404108]和[ecx*4+404108]是查找表中的值,因为使用的XOR值不断变化,但是这些值是重复的,因此我可以确定它们不是随机的。当我查看查找表所在的存储区域时,很容易看到它的开始和结束位置。那么,知道有一个查找表后,我如何找出正在使用的哈希机制呢?

48.png

通常,自定义加密和哈希实现很难确定,除非你知道自己在寻找什么。只要该算法是公开可用的(例如AES或Serpent),而不是开发者自定义开发的算法,那么几乎总会有特定的值或指令作为常数出现。本文的示例中,常数就是0xA00AE278。如下图所示,这个值肯定与CRC-32有关。

49.png

在这个特定的示例中,程序循环遍历NTDLL.DLL导出函数并将它们全部哈希,直到找到匹配的哈希。更糟糕的是,哈希是不可逆转的,这意味着你可以找到匹配哈希的唯一方法是通过强制执行。这样做,你就可以哈希NTDLL导出的每个API,直到你找到匹配的哈希值,但这太费时了。简单的办法就是,在找到匹配项的命令上放置一个断点,然后执行程序。一旦断点被触发,你将能够找到正确的API。从下图中,你可以看到匹配的第一个API调用是ZwGetContextThread,我现在可以使用它来获得使用的CRC-32的变体。

50.png

在此,我推荐使用https://crccalc.com/转换网站,它允许我将调试程序的值与不同变量的输出进行比较。使用API名称ZwGetContextThread和所需的输出0x5A3D66E4,我可以找到使用的变体:CRC-32 / JAMCRC。

51.png

匹配的API被找到后,就不需要编写强制执行脚本了。但对于像GootKit这样的恶意家族来说,强制执行脚本还是有必要编写的。因此,导入的5个API是:ZwGetContextThread,ZwSetContextThread,ZwReadVirtualMemory,ZwWriteVirtualMemory和ZwAllocateVirtualMemory。

52.1.png

52.2.png

下一个函数只是检索要存储在存储区域中的文件名,它可能会在稍后的示例中使用。现在我想跳过它,分析它后面的更有趣的函数。

53.png

由于ISFB被嵌入FJ,F1或JJ结构,此结构位于节表之后,包含指向附加数据的指针,例如配置信息,RSA公钥,甚至是另一个可执行文件。 虽然FJ,F1和JJ之间存在差异,但它们的变化很小。本文的样本使用的是嵌入式JJ结构,可从魔术值0x4A4A(“JJ”)中被识别出来。

54.png

该结构的格式如下所示:

55.jpg

该二进制文件的解析结构如下:

56.jpg

我正在查看的函数负责解析此结构并查找所需的数据,这个函数的主要参数是第三个,它是“pnls”字符串和嵌入的十六进制值之间的XOR的结果。第一个和第二个参数只是代表一个空的存储区域的开始和结束,因此它们现在并不那么重要。

57.png

解析函数的前半部分时要遍历MZ和PE标头,直到进入位于节表之后的资源结构为止。魔术值实际上是“JJ”(0x4A4A),否则它将把0x14添加到当前地址并重试。

58.png

一旦结构确定了,该函数就会将第三个参数(0x9E154A0C)与嵌入的CRC-32哈希值进行比较,后者也是0x9E154A0C。如果不匹配,则该函数将再次返回或循环。如果它们匹配,则函数对结构标志和0x2执行bitwise AND,直到返回0,否则函数将一直循环或返回。如果返回0,则根据存储在结构中的大小值分配堆。一切都结束后,该函数将通过将结构中的地址添加到可执行文件的基址来获得联接数据的完整存储地址,然后对结构标志和0x1执行bitwise AND。如果结果为0,则联接数据( joined data)不会被压缩并被编码,如果结果不为0(在本文的示例中为1),则会压缩联接数据。由于在这个二进制文件中压缩了联接数据,我们将重点介绍压缩方法。

59.png

推送到函数的两个参数分别是压缩数据的位置,以及新分配的堆的地址。

60.png

你可以看到该函数非常复杂且很难理解,还记得我之前所说的常数值吗?它们也存在于一些压缩例程中,尽管与加密算法相比,识别算法要困难得多。

61.png

由于我会在上图中寻找一个没有存储在存储区域中的硬编码,所以我可以看到正在使用的一条CMP指令,将esi中的值与值0x7D00进行比较。使用此功能,我可以在Google上搜索“0x7d00压缩”。

62.jpg

63.png

根据明显的相似之处,我可以推断出这里使用的解压算法是APLib。根据这些信息,我就可以编写一个简单的提取脚本来从二进制文件中提取任何联接数据,并使用由Mak创建的优秀Python库Mlib对其进行解压。在十六进制编辑器中打开二进制文件,并找到联接数据的位置,识别字符串。

64.png

从解压缩函数中退出后,恶意程序将确保解压缩的可执行文件的大小与JJ结构中存储的值大小相同。如果大小不匹配,则使用HeapFree()从内存中清除已解压缩的可执行文件。然后,恶意软件用存储在结构中的XOR密钥对解压数据的第一个DWORD进行异或。看一下解压后的数据,我可以看到可执行文件的第一个DWORD无效,它应该看起来像00009000,而不是54205BD4。在XOR之后,可以看到有效值。该函数在自行清理后,然后返回调用函数。如果你在此阶段转储可执行文件并在开头添加“MZ”,则无法在PE Bear中打开它。

65.1.png

65.2.png

在调用函数中,恶意软件会使用滚动XOR算法覆盖已解压的可执行文件。我不确定它为什么这样做,可能是因为可执行文件稍后会被解密。无论如何,一旦可执行文件被加密,函数就会这样做。

66.png

首先,它创建一个新的文件映射,并调用MapViewOfFile,然后在调用UnmapViewOfFile之前将其文件名复制到新映射的区域。不过我不太清楚为什么要这么做,因为它似乎没有任何作用。

67.png

现在,调用此示例中的最后一个函数。由于这是一个相当大的函数,所以我只挑重点来说。首先,该函数会移动不同的指针,然后将加密的可执行文件的前2个字节与值0x5A4D (MZ)进行比较,以检查数据是否加密。由于它是加密的,结果不会是0,因此它将根据在嵌入式JJ结构中看到的大小分配堆。接下来,它将调用另一个函数,该函数负责执行另一个滚动XOR算法,该算法将解密数据。虽然与上一个函数有点不同,但是返回的可执行文件应该相同的。然后,它将定位PE头中的值,该值会向你透露可执行文件的系统架构,在本文的示例中是0x14C,这意味着它是基于x86的。

68.1.png

68.2.png

接下来,恶意软件将使用NtCreateSection()分配一块全新的存储部分,并将其设置为Read-Write-eXecute。此时,已解压的可执行文件就包含在其中。

69.png

此时,将形成一个位于新创建的存储区域内的地址,指向0x0022EC50,稍后将使用该地址。创建的存储部分首先会被复制到地址0x00240000。接下来,恶意程序开始将可执行文件复制到新地址,但是它会跳过整个MZ标头并简单地复制PE标头中的所有内容。

70.png

然后,调用一个从NTDLL导入更多API调用的函数,但是这次没有使用CRC哈希,并且名称只是作为参数传递。导入的API调用是LdrLoadDll,LdrGetProcedureAddress和ZwProtectVirtualMemory。指向地址的指针存储在相同的存储区域中,并被分为4个空字节。然后将这些地址复制到0x00220000处的可执行文件,以便在执行期间使用。接下来,将代码区域复制到存储中的可执行文件中,在加载的API之后只需要40个字节,以下是在完成下一个阶段之前调用的内容。

71.png

接下来,下一个函数负责将执行命令传递给可执行文件。有两个函数将执行过程传递给可执行文件,但它们依赖于系统架构。我用的是x64版本,很简单,因为它所做的只是在地址0x0022E040处调用函数,该函数将为下一阶段做好准备。因此,你可以将此阶段视为另一个“解压过程”,因为它所做的只是解压可执行文件,但是有许多函数可以延续到下一个阶段,从而使分析变得容易得多。

72.png

当我进入该函数时,会看到一个int3,它会作为一个断点来引发异常,阻止我跳过它。要解决这个问题,我可以在前一个调用上放置一个断点,重新启动调试程序,然后运行它,直到遇到断点。

73.png

因为在本文的示例中,我没有删除int3。所以,我将使用Immunity Debugger软件。Immunity Debugger软件专门用于加速漏洞利用程序的开发,辅助漏洞挖掘以及恶意软件分析。它具备一个完整的图形用户界面,同时还配备了迄今为止最为强的Python安全工具库。它巧妙的将动态调试功能与一个强大的静态分析引擎融合于一体,它还附带了一套高度可定制的纯pythont图形算法,可用于帮助我们绘制出直观的函数体控制流以及函数中的各个基本块。它负责使用以前导入的API导入DLL,首先,有一个循环将DLL名称存储在存储区域中,该名称将使用LdrLoadDll加载。然后,循环使用一组硬编码的API,每个API都传递给LdrGetProcedureAddress,加载的DLL是 NTDLL.DLL,KERNEL32.DLL和OLEAUT32.DLL。

74.1.png

74.2.png

74.3.png

导入API后,ZwProtectVirtualMemory就会在一个循环中被调用多次,以更改几个存储区域的保护。

75.png

为了跳转到有效载荷执行发生的位置,我希望在调用寄存器(在本文的示例中为EBX)时设置断点。一旦断点被触发,就表示你已经进入了ISFB攻击中的下一个阶段。

76.1.png

76.2.png

我现在需要做的就是将其转储出来,将“MZ”和“PE”添加到标头中,删除映射并使用PE Bear重新绑定它,然后我们就可以开始分析下一个阶段了!

77.1.png

77.2.png

77.3.png

转储DLL的MD5: 52b4480de6f4d4f32fba2b535941c284,至此,逆向分析 ISFB 银行木马的第一阶段的加载程序的过程已经全部完成,并解压了下一个阶段的一些内容,我将在下一篇文章中对其进行分析。

  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务