解密在Emotet、Qbot和Dridex中使用的加密器

Change 技术 2019年3月21日发布
Favorite收藏

导语:加密器是一类软件,可以加密、混淆和操纵恶意软件,使其更难以被安全程序检测到。

加密器是一类软件,可以加密、混淆和操纵恶意软件,使其更难以被安全程序检测到。Zscaler ThreatLabZ研究小组发现,Emotet、Qbot和Dridex在近期活动中都使用了一种常见的加密器,该加密器也同样能在一些Ursnif和BitPaymer的活动中检测到。Emotet和Dridex能够存活如此之久的原因之一,就是它们会通过使用各式各样的加密器来逃避检测,加密器会将原始二进制文件封装在内部,使对其的检测和分析变得更为复杂。

Emotet是一种模块化恶意软件,主要功能是作为银行木马的下载器或dropper。在过去的四年里Emotet一直很活跃,它也是去年最流行的恶意软件系列之一。Dridex是一种源自Zeus Trojan家族的银行木马,即使在2015年被FBI端掉之后也至今能看到它在野外活跃的踪迹。Qbot则能远程访问受害者的系统,窃取信息,并将之上传到攻击者的远程服务器。最近检测结果显示,Emotet的有效负载url指向了Qbot恶意软件,Emotet近期使用的加密器我们将在下文阐述。

此加密器为恶意软件的核心二进制文件提供了多层保护。在本文中,我们将详述这些加密二进制文件的属性是如何使其适用于各种变化的。这些属性可以在不执行二进制文件的情况下被静态验证,并用于编写解密器。下图中展示了Emotet的核心二进制文件在加密器的混淆和加密包装层中的封装过程。

0.核心二进制文件

1.代码通过变换指令和替换跳转指令的方式进行混淆

2.混淆的二进制文件被加密,并附加在加载器二进制文件的末尾

3.加载器二进制文件的文件对齐方式被打乱

4.加密加载器的二进制文件

5.最后对加密后的加载器二进制文件的散列块进行二进制封装

1.png

图1.加密的各个阶段

我们的目标是通过逆向上述的各阶段从而获得恶意软件的核心二进制文件。此外,核心二进制文件应是独立可加载/可执行的,并且IOC应易于提取。我们将从阶段5开始,描述二进制文件的某些启发式属性,通过这些属性解密该阶段,并持续溯源到阶段0。在我们的分析中,我们发现这些启发式属性适用于所有二进制文件的变化情况。

阶段5

阶段5的二进制文件是Emotet可执行文件,它通过MalSpams中的恶意链接或MS Office文档中的恶意宏进行下载。我们在阶段5的目标是进入阶段4以获得加密的自定义加载器的二进制文件。正如图1中所展示的那样,此阶段的二进制文件是加密的加载器二进制文件的散列块,我们需要发现这些块并按正确的顺序组装它们。在讨论如何做到这一点之前,我们先看看几个示例——这些块是如何分布在二进制文件中的。红色标出部分为文件块。

2.png

2.2.png

图2.文件块模式示例

在上面的例子中,我们可以看到这些文件块并不总是在固定的位置,因为它们的大小不一致,而且块的顺序也不同。因此,第一个挑战是找到这些块并按正确的顺序排列它们。好消息是,我们知道加密器还需要对数据块进行排序,并将数据块地址和大小存储在一个表中,这个表称为“块描述符表”。坏消息是,这个表不能在二进制文件中可预测的位置找到,而且表的结构在二进制文件的变化中也不是恒定的。下面是这个表结构的一些变体。块描述符表基本上是块描述符条目的组合。

struct ChunkDescriptorEntry[n] ChunkDescriptorTable; // n == number of chunks

3.png

图3.数据块描述符表结构示例

在上述结构中,“chunkAddressDword”包含块的虚拟地址。块的大小可以通过“firstDword”和“secondDword”上的下列操作之一获得,该操作在所有块描述符条目中都是不变的。

1. unsigned int chunkSize = firstDword + secondDword
2. unsigned int chunkSize = firstDword ^ secondDword
3. unsigned int chunkSize = secondDword - firstDword

块描述符表的启发式属性:

1. 0 <= x <= ?,      0 <= y <= ?,      0 <= z <= ?
2. offset(firstDword) < offset(secondDword) < offset(chunkAddressDword)
3. offset(firstDword) < offset(chunkAddressDword) < offset(secondDword)
4. offset(chunkAddressDword) < offset(firstDword) < offset(secondDword)
5. entropy(chunk) > 5 out of 8.
6. 块不包含连续的4个0。

下面是查找块模式的伪代码。函数“FindChunkEntry”返回块的偏移量和从块偏移量开始的firstDword、chunkAddressDword的距离。如果对函数的三个连续调用的返回值和三个返回偏移量之间的长度相等,那么可以解析整个数组来生成块地址和块大小的关联数组。

(offset1, m1, n1) = FindChunkEntry(filedata, fileSize)
(offset2, m2, n2) = FindChunkEntry(filedata + offset1, fileSize)
(offset3, m3, n3) = FindChunkEntry(filedata + offset2, fileSize)
If (offset2 - offset1) == (offset3 – offset2)
    // found the FindChunkEntry array
 
FindChunkEntry(filedata, fileSize)
        p = 0
        while p > fileSize
                firstDword = filedata[p]
                q = p
                while q < p + T
                secondDword = filedata[p]
                        chunkSize = firstDword (+) secondDword
                        r = p - T
                        while r < q + T
                                chunkAddress = filedata[r]
                                if ValidateChunk(chunkAddress, chunkSize) == TRUE // Heuristics 5, 6
                                        if p < q < r
                                                x = q - p
                                                y = r - q
                                                z = ?
                                                return (p, x, y)
                                        elif p < r < q
                                                x = r - p
                                                y = q - r
                                                z = ?
                                                return (p, x, y)
                                        elif r < p < q
                                                x = p - r
                                                y = q - p
                                                z = ?
                                                return (r, x, y)
                                r += 4
                        q += 4
                p += 4

现在有了块地址和块大小的关联数组,我们可以组合这些块来获得加密的加载器二进制文件,于是我们可以进入第四阶段。

阶段4

在我们的分析中,我们观察到该加载器二进制文件(PE exe)的加密方式,是通过在循环中使用密钥数组进行简单的字节到字节(byte-to-byte)的添加进行的。在此加密数据中,二进制文件不需要以零偏移量出现。在第四阶段,我们的目标是找到PE文件在加密数据和解密密钥中的偏移量。首先,我们将找到解密密钥,它可以强制通过加密数据来查找PE文件的起始偏移量。解密过程出现在第5阶段二进制文件中,但不是在可预测的位置。我们将从加密的数据本身推导出解密密钥。

关于加密数据需要注意的是,启发式属性是一种重复的字节序列的模式。重复序列的这种模式是由PE文件的加密性质和属性引起的。在加密的数据中,此模式将出现在未加密时本应填充0的位置,以下是可能的填充位置:

1、在加密数据的开头,因为PE文件不一定从零偏移量开始。

2、两段之间的空隙,或是PE头与第一段之间。

4.png

图4.加密数据显示了一个重复的字节序列

5.png

图5.解密数据,显示对应PE文件的出现

重复序列就是我们的密钥,序列的长度就是密钥长度。接下来我们需要做的是在这个重复的序列中找到键的开头和PE文件的偏移量,可以通过在检查MZ报头时对该序列和从其起点开始的加密数据应用加密的逆操作来实现。

伪代码如下:

FindPeFileAndKeyStart (keySequence, keySequenceLen, encryptedData, encryptedDataLen)
        k = 0
        while (k < keySequenceLen)
                i = 0
                while (i < encryptedDataLen)
                        if (    // inverse encryption
                            encryptedData[i + 0] - keySequence[k + 0] == 0x4D &&
                            encryptedData[i + 1] - keySequence[k + 2] == 0x5A &&
                            encryptedData[i + 3] = keySequence[k + 3]
                            )
                                peFileOffset = i
                                keyStart = k
                                return (peFileOffset, keyStart)

现在我们有了PE文件偏移量和密钥,可以使用循环中的密钥进行字节到字节的减法来解密数据。之后就能得到加载器二进制文件,但是这个加载器二进制文件有一个奇怪的文件对齐方式,我们需要对其进行规范化。因此,我们来到了第三阶段。

1、在能访问密钥的第5阶段,搜索二进制文件中的指令。下面是我们在分析中发现的例子:

1. 8A 0C 1D FF 31 40 00                          MOV CL, BYTE PTR DS : [EBX + 4031FF]
2. 2A 0C 1D 30 42 40 00                         SUB CL, BYTE PTR DS:[EBX+404230]
3. 8D 84 01 0B 32 36 01                          LEA EAX,DWORD PTR DS:[ECX+EAX+136320B]
4. 8B 1D E4 DD 46 00                              MOV EBX,DWORD PTR DS : [46DDE4]
5. 8D 0D 81 A1 3D 01                              LEA ECX,DWORD PTR DS:[13DA181]
6. 8A 86 77 41 E8 00                                MOV AL, BYTE PTR DS : [ESI + E84177]
7. 8A 98 77 51 C0 00                               MOV BL, BYTE PTR DS : [EAX + C05177]
8. A1 3C FB 46 00                                     MOV EAX,DWORD PTR DS : [46FB3C]

其中,DWORD指向一个密钥数组,不过这个地址是假的。因此,我们需要通过将密钥应用于每个此类指令的加密数据来验证密钥。

2. 在大多数情况下,顺着pdb路径可以找到密钥。

阶段3

在此阶段,我们有了一个加载器二进制文件,但它的文件对齐方式混乱。我们将为这个PE映像分配一个新的文件对齐方式,也就是0x200。更改文件对齐非常简单,只需要我们根据计算的地址小心地移动新文件对齐的部分,并更新节头表中的节地址和大小。

下面是伪代码:

dwNewFileAllignment = 0x200
dwCurrentRawAddress = GetAllignedDwrod(PEHeader.OptionalHeader.SizeOfHeaders, dwNewFileAllignment);
        while (i < NumberOfSections)
                NewSecionHeaders[i].PointerToRawData = dwCurrentRawAddress;
                NewSecionHeaders[i].SizeOfRawData = GetAllignedDwrod(OldSecionHeaders[i].SizeOfRawData, dwNewFileAllignment);
                Memcpy(pbyNewFileBuffer + dwCurrentRawAddress,
                                pbyOldFileData + OldSecionHeaders[i].PointerToRawData,
                                OldSecionHeaders[i].SizeOfRawData);
                dwCurrentRawAddress += GetAllignedDwrod(OldSecionHeaders[i].SizeOfRawData, dwNewFileAllignment);
                dwCurrentRawAddress = GetAllignedDwrod(dwCurrentRawAddress, dwNewFileAllignment);
                i = i + 1

阶段2

在加载器的附加数据中有加密数据,它只是核心恶意软件二进制文件的混淆版本。加密方法与步骤4相同。这里我们需要做的就是计算附加数据的偏移量,并应用第四阶段提到的解密技术。由此,我们得到了核心PE文件,但它需要在代码中进行一些修复,因为二进制文件中缺少一些指令。

阶段1

在此阶段,我们有了一个PE文件,但它有些不完整,因为加载器二进制文件已经占用了代码部分的一些指令。这些被占用的指令将被放入内存中,并在核心二进制代码中插入一条跳转指令,该代码指向相应的被占用指令,然后控制权会被传递给核心二进制文件。

下图是此混淆出现的示例。

 6.png

图6.带有跳转指令的混淆代码

最后的目标是消除核心二进制代码的混淆。这意味着我们需要将被占用的指令返回到它们的实际位置并删除跳转。此时,上面的代码将显示如下。

7.png

图7:去混淆代码

即使在将指令放在不同位置之后,装载程序也能顺利执行核心恶意软件。加载程序需要计算移动指令的跳转地址,并将跳转指令放在这些指令的位置。为此,移动的指令及其元信息存储在加载程序的二进制文件本身中。在我们的分析中,我们发现包含此信息的表存在于“.rdata”部分中。

struct DeobfuscationTable
{
        unsigned int dwOrgInstrVAdddress; // Address of eaten instruction in loader’s binary
        unsigned int dwPatchRVAddress;    // Offset where Jump need to insert
        unsigned int dwOrgInstrLength;    // length of moved instructions in bytes
};

一旦我们得到了去混淆表,我们只需要从虚拟地址“dwOrgInstrVAdddress”加载器的二进制文件中读取“dworginstrvlength”字节,并将其写入到核心恶意软件二进制文件中的相对虚拟地址“dwPatchRVAddress”中。

下面是伪代码:

while (pDeObfuscationTable-> dwOrgInstrVAdddress !=  0x00)
{
        patchOffset = GetFileOffsetFromRVA(
        pCorePEHeader,
        pCoreSectionHeaders,
        pDeObfuscationTable-> dwPatchRVAddress);
        orgInsOffset = GetFileOffsetFromRVA(
        pLoaderPEHeader,
        pLoaderSectionHeaders,
        pDeObfuscationTable-> dwOrgInstrVAdddress - pLoaderPEHeader-OptionalHeader.ImageBase);
        memcpy (
                pbyCoreFileData + dwPatchOffset,
                pbyLoaderFileData + orgInstrOffset,
                pDeObfuscationTable->dwOrgInstrLength);
        pDeObfuscationTable += 1;
}

在这个阶段,我们将获得简单的、独立可执行的核心Emotet二进制文件,它可以由IDA进行反编译,也可以与此解码器提取的其他二进制文件进行二进制区别。

本文翻译自:https://www.zscaler.com/blogs/research/demystifying-crypter-used-emotet-qbot-and-dridex如若转载,请注明原文地址: https://www.4hou.com/technology/16219.html
点赞 0
  • 分享至
取消

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

扫码支持

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

发表评论