深入分析KSL0T(Turla组织的Keylogger)(二)

 

上篇链接:https://www.anquanke.com/post/id/181793

再阅读了本文的上篇之后,让我们继续来分析KSL0T。首先,你可以从VirusBay下载Keylogger。到目前为止,我们已经使用一个简单的XOR方法解密了大量文本,该方法揭示了如何记录不同的密钥、记录数据的文件名以及keylogger可能的名称(KSL0T)。如果你不了解我所说的内容,请先阅读本文的上篇。接下来,让我们开始更加深入的分析。

样本的MD5值为59b57bdabee2ce1fb566de51dd92ec94

如果你正在进行此分析,请确保已重命名解密函数。继续分析解密函数,使用最近解密出的值(kernel32.dll和GetProcAddress)调用GetModuleHandleW和GetProcAddress函数,这将得到要解密的最后两个字符串。

 

0x01 继续分析解密函数

图1

GetProcAddress函数的返回值将存储在rax寄存器,然后被mov到[rsp+48h+var_28],为了简化问题,我们可以重命名var_28为GetProcessAddress。所以无论何时[rsp+48h+var_28]被mov到另一个寄存器(只要它的值没有改变),如果寄存器在程序中被调用,我们就可以确定发生了什么。果然,在kernel32.dll的句柄被mov到rcx寄存器之前,[rsp+48h+GetProcessAddress]被mov到rdx寄存器中,然后0x1800039C0代码段上的函数被调用。

图2

我们可以很容易地确定传递给这个函数的参数,因为它再次使用了mov操作。我们已经知道rcx寄存器包含kernel32.dll的句柄,rdx寄存器包含GetProcAddress函数,r8寄存器似乎包含一个空内存的地址:0x1800105A0,该区域填满了0。

图3

如果你在图形模式下查看该函数,你将看到一个很长的“行”,如下图所示,直到最后才会出现if’s或for语句。还可以看到,在填充参数之前声明了许多变量,因为我们分析这个二进制数,使用的是静态分析方法,这个函数将需要大量的工作才能理解(因为它很可能是Turla集团为了防止简单分而使用的反静态分析的方法)。提示:除了这次之外,还有更多的数据需要解密,并且加密的数据是边运行边加载的,这就是为什么一行中有这么多mov操作的原因。因此,我们必须手动提取这些字节,弄清楚它们是如何解密的,并通过自动化或编写脚本解密。让我们投入其中吧!

图4

肯定有更好的方法去解密数据,于是我花费了很多工作去寻找它。首先,突出显示mov指令并将其复制到一个文件中。然后分离mov指令,使得它只包含指令的第二个参数(加密数据)。

图5

现在我们需要解析并正确格式化数据,这样我们就只需要将值mov到目标中。下面的脚本删除了除数字之外的所有内容,包括指定十六进制格式的符号’h’。对于奇异数,在值前面加上一个’0’。

def main():
f = open("data.txt", "r")
data = f.readlines()
f.close()

f = open("data_2.txt", "w")

for lines in data:
    lines = lines.split("], ")[1]

    if "h" in lines:
        lines = lines.split("h")[0]
        lines = lines + " "
    else:
        lines = "0" + lines
        lines = lines.split("n")[0]
        lines = lines + " "

    f.write(lines)

f.close()

if name == “main”:

main()

执行脚本之后,把输出保存为“data_2.txt”。这是提取的加密数据,为了理解它是用什么加密的,我们需要确定使用的解密方法。

图6

回到程序集,在将各个字节都mov到正确的位置之后,将重复调用0x180001000处的函数以类似于第一个解密函数的方式,只不过这次使用了两个参数。

图7

你可能已经猜到,这是另一个算法,这个算法比上一个简单得多,因为数据的每个部分与0x55进行XOR操作,这意味着我们不需要编写解密脚本,可以简单地在CyberChef软件上执行一个基本的XOR解密,并且转换十六进制格式。如果你以前没有使用过CyberChef,你应该了解并熟悉使用它,因为它在这种情况下非常有用。

图8

如图8所示,数据包含多个API调用和DLL调用,这些调用在运行时被加载到函数中。向下滚动图表,有几个对GetProcAddress的调用,以及对变量的调用,如’var_290’。我们可以通过两种方法来找出变量’var_290’中存储了什么,第一种方法是使用debugger,第二种方法是本文的静态分析(更复杂的方法)。为此,我们需要回溯分析的过程。我们可以看到,’rax’的值存储在’var_290’中,在’GetProcAddress’调用之后,其中一个参数是’kernel32.dll’,另一个是被调用的函数(存储在’var_58’中)。

图9

在’GetProcAddress’之前,解密函数用于在’var_58’处解密13字节的数据,因此让我们转到这个函数中’var_58’的’x-ref’,并数出13字节的数据:var_58 -> var_4C,如下图所示。

图10

复制这些字节并将它们粘贴在CyberChef中,和’0x55’执行XOR操作。这将会得到’LoadLibraryA’,如下图所示。

图11

之后,只有’GetProcAddress’和’LoadLibraryA’在函数中被调用——我们可以假设解密文本中的每个API函数都是导入的。显然,我们可以手工完成所有操作,但是如果能使用debugger,那么速度会快得多。

图12

由于所有导入问题都已解决,我们继续分析下一个函数,’GetUserNameExW’在程序中调用了两次。该函数的调用将返回域名和用户名’ReversingRE’。然后恶意软件使用’wcscat’将其mov到另一个位置,并检查其返回值中是否有’’。如果有,则返回指向它的指针,然后’’被’.’所代替,这样我们就得到了’Reversing.RE’。格式化的字符串’Reversing.RE’用于创建互斥变量。程序首先通过调用’OpenMutexW’检查互斥变量是否在该值下已创建,如果没有创建,则调用’CreateMutexW’创建互斥变量。通过再次检查,我们可知互斥变量是使用工具SysAnalyzer创建的,这个工具在动态分析时,对于分析恶意程序非常有用。

图13

一旦创建了互斥变量,在0x180003960处的函数被调用,它将创建一个指向0x180001B70的新线程。当创建的线程退出时,恶意软件也退出了。

图14

对于新创建的线程,似乎在线程执行之后,立即调用’0x180001B00’处的函数,其中包含keylogger的“肉”。根据keylogger使用的方法,我将其标记为’Set_Hooks’。

图15

用于执行密钥记录的恶意软件和的“合法”软件中最常用的两个Windows API调用是’GetAsyncKeyState’或’SetWindowsHookEx’。由于使用’GetAsyncKeyState’有很多问题,现在大多数keylogger都使用’SetWindowsHookEx’。在本例中,’SetWindowsHookEx’用于捕获键盘输入。虽然不能在IDA中使用伪代码函数,但是我们可以使用MSDN来了解当前调用的函数以及其是如何调用的。

HHOOK SetWindowsHookExA(int idHook, HOOKPROC lpfn, HINSTANCE hmod, DWORD dwThreadId);

在输入函数的所有参数之后,我们可以得到:

HHOOK SetWindowsHookExA(13, 0x1800022C0 , 0x180010720, 0);
HHOOK SetWindowsHookExA(WH_KEYBOARD_LL, LowLevelKeyboardProc, DLL_Handle, NULL);

安装一个hook——用于检测底层键盘输入,允许恶意软件捕获键盘输入。然后函数返回前面创建了’Get’,’Translate’和’DispatchMessage’循环的函数。当程序记录输入的密钥时,’GetMessage’将捕获键盘输入并将其传递给’TranslateMessage’,后者将虚拟密钥消息转换为字符消息,然后将其传递给’DispatchMessage’,并将其重定向到另一个窗口过程。如果你想了解更多关于捕获键盘输入的内部工作原理,请访问(https://securelist.com/keyloggers-how-they-work-and-how-to-detect-them-part-1/36138/) 。

图16

对于’setwindowshokexa’调用的函数,它位于’0x1800022C0’。如下图所示,这个函数非常混乱。图表底部的部分实际上是一个’switch’语句,因为我们看到有多个’case’值,以及一个’default’值。此外,IDA也告诉我们这是一个switch语句。C语言中switch语句是比较一个变量和几个不同变量的方法,而不是使用多个if语句。

图17

为了找到case变量的值,我们需要执行一些简单的加法。查看每个框,有一个lea rdx Encrypted_Keys,然后添加rdx…h,其中…表示某个十六进制值。在一个特殊的情况下,值13C被添加到加密密钥的内存地址,即0x18000F2F0。将它们相加后,得到0x18000F42C,它指向“<”。添加之后的下一条指令将一个值mov到r8d中。这表示字符串的大小,即4。因此,也包括0x18000F42C之后的3个字节,这意味着整个值。

图18

为了加快这个过程,我编写了一个简单的脚本来自动化实现这个过程,你所要做的就是输入加法值和字符串长度,终端将输出相应的key。脚本我已经上传到pastebin,你可以在这里查看(https://pastebin.com/7EyK8mHA) 。

图19

使用wcsncat将这个值连接到地址0x1800115B0。我们可以将其重命名为Captured_Char,因为它就是Captured_Char。如果捕获的键盘输入不等于任何硬编码值,则使用default,但是它们都将被记录在日记中。在研究这个函数的其余部分之前,让我们先看看数据是如何记录的。

图20

由图可知,这个函数相当长,因此我们只需要查看’WriteFile’部分(位于函数的底部),便可以查看数据在存储时是否加密。

图21

假设数据写入文件之前已进行加密。如你所见,这里有一个’for’循环,一方面用于使用’WriteFile’写入数据,另一方面用于使用原XOR密钥对数据进行XOR操作。首先,’var_34’的值与’var_20’的值进行比较。由于’var_34’是’WriteFile’调用中的第三个参数,所以我们可以推断出’var_34’是要进行XOR操作的数据的长度:

WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped)

因此,我们可以将’var_34’重命名为’NumberOfBytesToWrite’。在此过程中,我们还可以重命名调用中使用的其他变量,以便更容易理解函数。由于’var_20’在每个循环中递增,所以我们可以简单地将其重命名为i。接下来让我们看看XOR操作的部分,如下图所示。

图22

‘i’中的值被mov到’rcx’中,’Buffer’中的值(很可能是捕获的键盘输入值加上任何其他数据)被mov到’rax’中。同样,与这两个解密过程类似,第一个将被加密的字符是通过将’i’的值添加到’Buffer’的地址中找到的。然后mov到’edi’中,接着调用’div’操作。如果你还记得本文关于keylogger的上篇,’div’操作将rax的值与传递的操作数(即rcx的值)分开。’rcx’的值是100 (0x64),因此’rax’将除以100。但我们还不知道rax的值是多少,由下图知’dword_180010738’被移到了寄存器中,但它是空的。因此,我们必须找到将值mov到’dword’中的部分。

在搜索变量’xrefs’时,在加密过程(位于0x1800013F1)之前只提到一次’xrefs’变量。恶意软件获取了键盘输入记录的文件大小,然后执行另一个’div’操作,其余部分存储在dword中。假设文件大小为0,因为日志记录器刚刚启动。然后0除以100,显然是0。这意味着edx寄存器中的值是0,dword值也是0。因此,我们可以回到加密过程并完成其余工作。

图23

为了获得key的一个字节用于XOR运算,我们使用rdx和rax。第一个循环中rdx的值为零,这是使用’dword_180010738’的值进行’div’运算的结果。原始XOR运算的key的地址被mov到’rax’中,并使用’byte ptr [rax+rdx]’在’eax’中存储一个字节。’edi’(键盘输入数据)被mov到和’eax’进行XOR运算的’edx’中。基于i的值,加密的字符用于覆盖键盘输入数据的字符。接下来,’dword_180010738’的值增加1,这意味着XOR操作的key的缓冲区的第一个字符不同于其第二个字符。最后,i也增加1,循环继续,直到缓冲区被完全覆盖。

然后数据被写入文件,缓冲区被释放,文件句柄被关闭,函数返回。

图24

 

0x02 寻找键盘记录数据的位置

现在我们已经破解了算法,但是还需要找到记录数据的位置。已知包含文件句柄的变量,接下来让我们找到它被使用的第一个实例。果然,在调用’CreateFileW’之后,有一个’mov [rsp+928h+File]’和’rax’。当查看’CreateFile’的参数时,我们可以看到第一个参数是文件名:

HANDLE CreateFileA(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);

在本例中,第一个参数是一个包含msimm.dat (我们解密的原始字符串之一)的变量。由于没有找到该文件的文件路径,该文件应该是在当前目录中写的,因此无论何处keylogger一直在运行。

图25

现在已知关于文件如何记录和数据如何存储的所有信息,所以让我们看看是否可以获得加密数据的样本来分析它。打开一个VM并运行DLL。为了运行它,我使用x64Dbg,因为我无法获得’rundll32.exe’来运行它,这可能是因为缺少导出。最终,我想要的文件(‘msimm.dat’)在桌面上创建。打开它显示的似乎是另一种语言的文本,虽然这是记事本显示的加密文本。在类似’CFF Explorer’的文件中打开文件,以便查看文件的十六进制数据,这样我们就可以对其做XOR运算,使其回到纯文本的格式。将其复制到主机上的文本文件中,并使用你常用的文本格式工具。

图26

这样做的原因是我写的脚本非常简陋。为了让python将十六进制字节作为’hex bytes’读入序列,我尝试了几种不同的方法,但都失败了。如果你们对如何改进有什么想法,可以提出。目前来说,文本需要添加’0x’来进行格式化:

0x..., 0x..., 0x...

当CFF explorer将十六进制复制到一个长字符串中时,我们需要每隔一秒将其分割一次,并将空格转换为’, 0x’,我个人就是这样做的。现在,我的脚本并不是100%的时间都能工作,因此我主要使用它作为一个例子来展示如何在Python中复制这个算法。它似乎只对文本的一个部分有效,但我相信那些具有更高水平的python知识和恶意软件分析知识的人将能够重新利用它,使它完美地工作。不管怎样,它在这里(https://pastebin.com/Fwpz7PZf) 。当我们运行脚本时,它将使用密钥解密十六进制数据的部分并输出明文。正如我所提到的,有很多更好的方法可以做到这一点,因此它适用于不同的日志,但是我没有太多时间来处理它以及使它保持原样。

图27

到这里已经几乎完成了这个分析。没有从keylogger中提取日志文件的方法,所以我认为Turla集团只在远程访问时使用它,并通过远程访问工具或后门提取日志。

IOCs:

Keylogger: 59b57bdabee2ce1fb566de51dd92ec94

(完)