欢迎来到我的博客系列,“威胁检测与搜寻建模”,我打算在这里探索和扩展我对我们试图检测的内容的理解。长期以来,我们都在战术、技术和程序范式中运作,以至于我觉得我们讨论复杂技术主题的能力往往受到我们表达想法的能力的阻碍。我最近的观察是,三层分类法(如TTP)的局限性太大,无法促进必要的对话来改善我们对检测的思考。我相信存在三个以上的层,这意味着我们的三层分类必然导致在分类的顶部或底部对不同的事物进行分组。出于这个原因,在我看来,“程序”一词使用得太宽泛,无法描述太多的事情,并限制了我们真正了解技术细节的能力。战术、技术和程序都是抽象的概念,作为将具体事物组合在一起的类别。我想从混凝土开始,然后逐步探索两者之间的所有层次。
出于这个原因,我们将从分析 API 函数开始,从某种意义上说,API 函数是操作系统中功能的基本组件。第一篇文章将深入探讨一个著名的攻击工具 Mimikatz,并确定它使用哪些 API 函数来完成特定任务。我希望你喜欢它!
在我的理解函数调用堆栈一文中,我介绍了 Windows API 函数的嵌套特性。几乎总是有一个肤浅/记录的 API 版本,然后通过一系列调用对更深层次、更基本的函数的调用,这些函数不太可能被记录下来,但仍然能够被应用程序直接调用。然后我解释说,恶意软件开发人员可以利用这种嵌套的知识来调用不太预期/记录的函数版本,以逃避某些传感器,使其行为“不可见”。在那篇博文中,我们专门探讨了 CreateFileW 并深入研究了它,但我们没有解释如何确定我们应该对哪个函数感兴趣的过程。
相关视频教程
恶意软件开发(更新到了130节)
这篇文章将介绍一个过程,即源代码审查,用于确定给定恶意软件样本使用了哪些功能。在本演示和本系列接下来的几篇文章中,我们将使用每个人最喜欢的工具 Mimikatz,并探索它依赖于哪些 API 函数来执行其最流行的命令。请记住,一个工具通常只是在一系列 API 函数之上充当一个更肤浅的包装器,完成艰苦的工作。在这里,我们将通过分析 Mimikatz 的源代码来了解它调用了哪些函数,然后我们可以利用我上一篇文章中演示的过程来了解这些函数是如何嵌套的。sekurlsa::logonPasswords
因此,这篇文章详细介绍了我如何分析 Mimikatz 源代码以将命令与其各自的函数调用联系起来。logonPasswords
第一个障碍是确定在代码库中从哪里开始分析。Mimikatz Github 项目提供了许多相同的版本 首先,我们计划深入研究 Mimikatz 的主要组件,因此我们将跳过 、 和其他文件夹并导航到该文件夹。mimidrv
mimilib
mimikatz
在 Mimikatz 文件夹中,我们看到一些与 Visual Studio Code 项目和主文件相关的文件。让我们考虑一下 Mimikatz 命令是如何工作的。每个 Mimikatz 命令都遵循语法。这意味着我们有兴趣深入研究模块文件夹。mimikatz.c
module::command
进入模块文件夹后,我们可以考虑使用的确切命令。这是考虑告诉 Mimikatz 从 LSASS 转储凭据的命令的绝佳时机。如果我们不熟悉该工具的用法,那么我们可能不知道。在这种情况下,我建议首先重新访问引起我们注意该工具的来源,以了解它在那里的确切使用情况。也许这将是一份威胁报告,或者它可能是一篇博客文章,但无论哪种方式,一个好的源文档都应该包括所使用的特定命令。
用于从 LSASS 进程内存转储凭据的最常见或规范的 Mimikatz 命令是 。这意味着我们感兴趣的代码位于 sekurlsa 文件夹中。让我们来看看。sekurlsa::logonPasswords
在 中,我们找到几个文件。主文件是 ,因此我们可以单击它并深入研究。sekurlsa
kuhl_m_sekurlsa.c
Mimikatz 中的每个模块都有一个中心文件(以模块命名),在主文件的开头,我们找到一个函数表。此表用于将命令与发出命令时执行的内部函数相关联。在这里,我们看到命令指向 。logonPasswords
kuhl_m_sekurlsa_all
此时,我们开始跟踪对内部函数的一系列调用。该函数使用两个参数调用该函数。第一个参数称为 ,第二个参数用于测量数组的大小。kuhl_m_sekurlsa_all
kuhl_m_sekurlsa_getLogonData
lsassPackages
lsassPackages
让我们花点时间看看数组包含什么。此常量包含一个类型实例数组,这些实例表示 Windows 上默认提供的不同安全支持提供程序/身份验证包。我们可以查看结构定义,以更好地理解每个结构中包含的内容。lsassPackages
PKUHL_M_SEKURLSA_PACKAGE
PKUHL_M_SEKURLSA_PACKAGE
该结构被定义为总共有 5 个字段。第一个是 ,大概是身份验证包本身。第二个字段称为 ,它似乎是某种回调函数,可能用于从包中检索凭据。第三个只是一个布尔值,称为 。但是,这里没有足够的信息来确切地了解该字段的预期用途是什么。第四个字段是实现身份验证包的模块名称 (DLL)。第五个也是最后一个字段被调用,其类型为 。KUHL_M_SEKURLSA_PACKAGE
Name
CredsForLUIDFunc
isValid
Module
KUHL_M_SEKURLSA_LIB
现在我们可以看一下实例的定义,我们看到 name 被设置为 ,一个调用的函数被设置为 ,被设置为 ,被设置为 ,最后,最后一个字段似乎被初始化为 的实例。kuhl_m_sekurlsa_kerberos_package
kerberos
kuhl_m_sekurlsa_enum_logon_callback_kerberos
CredsForLUIDFunc
isValid
TRUE
ModuleName
kerberos.dll
NULL
KUHL_M_SEKURLSA_LIB
KUHL_M_SEKURLSA_PACKAGE kuhl_m_sekurlsa_kerberos_package = {L"kerberos", kuhl_m_sekurlsa_enum_logon_callback_kerberos, TRUE, L"kerberos.dll", {{{NULL, NULL}, 0, 0, NULL}, FALSE, FALSE}};
了解了这一点后,让我们看看该函数的实现。事实证明,这里没有什么值得兴奋的。我们看到数组与参数形式的包数量一起传入。这些参数将添加到 OptionalData 变量中,然后作为第二个参数传递给名为 的新函数。此外,我们看到第一个参数似乎是我们应该调查的某种回调函数。kuhl_m_sekurlsa_getLogonData
lsassPackages
nbPackages
kuhl_m_sekurlsa_enum
kuhl_m_sekurlsa_enum_callback_logondata
以下函数是事情真正开始变得有趣和有点复杂的地方。该函数具有一些必须做出的决策和一些必须调用的函数。我们将探讨我们所看到的内容,并解释如何确定路径,例如,代码将选择哪条路径。我们注意到的第一件有趣的事情是对 的调用。让我们来看看它的实现。kuhl_m_sekurlsa_enum
kuhl_m_sekurlsa_acquireLSA
该函数需要做出几个决定。第一个决定是检查是否已经存在句柄(可能是 LSASS 进程的句柄)。如果没有,它会跟进检查是否已设置变量。我们可以检查代码以查看是否满足这些条件中的任何一个。kuhl_m_sekurlsa_acquireLSA
pMinidumpName
第一个 if 子句检查是否设置为 。如果我们找到变量首次声明的位置,我们就会找到这一段代码。它看起来好像是一个具有两个字段的结构。第一个显式设置为 ,而第二个(不同类型的结构)设置为三个零。我们应该检查类型定义,看看其中一个或每个的重要性。cLsass.hLsassMem
NULL
cLsass
cLsass = {NULL, {0,0,0}};
cLsass
NULL
KUHL_M_SEKURLSA_CONTEXT
根据结构的定义,第一个字段是 ,签入的值的名称。这意味着我们可以重新访问变量的实例化,并看到 is ,这意味着条件语句的计算结果为 true,这意味着我们将执行 if 语句中包含的代码。KUHL_M_SEKYRLSA_CONTEXT
hLsassMem
kuhl_m_sekurlsa_acquireLSA
cLsass
hLsassMem
NULL
(!cLsass.hLsassMem)
下一个检查与该值相关。您可能已经注意到,当我们看到要声明的值时,在前面的屏幕截图中设置了此值。pMinidumpName
cLsass
我重新分享了图像,我们可以看到变量设置为 .pMinidumpName
NULL
这意味着条件语句的计算结果为 false,这会导致执行被传递给 else 块。(pMinidumpName)
让我们看一下 else 块中的第一行,我们看到一个名为的变量被设置为该值。请注意第 159 行,其中变量被声明为 类型 。Type
KULL_M_MEMORY_TYPE_PROCESS
Type
KULL_M_MEMORY_TYPE
接下来,我们看到被调用的子例程。请注意,它有两个参数,第一个是字符串,第二个是指向变量的指针。变量定义(第 161 行)显示 pid 是 .根据函数的名称,我们可能希望它检索名为 的进程的进程信息,特别是进程标识符。让我们来看看。kull_m_process_getProcessIdForName
lsass.exe
pid
DWORD
lsass.exe
我们通过注意到一些正在初始化的变量来开始对函数的分析。重要的是,我们观察到变量被实例化为结构的实例。此结构使用三个字段进行实例化,第一个是指向先前实例化变量的指针,第二个是指向函数的第二个参数,此处调用,第三个是布尔值。请记住,调用函数传入了一个指向名为 的变量的指针,现在在此函数中调用该变量。接下来,我们看到对名为 的 API 函数的调用,该函数传入指向我们刚才讨论的变量和第一个参数的指针,该参数现在称为 。我们知道调用函数将字符串作为第一个参数传递,因此这似乎是将该值应用于稍后预期的特定数据结构类型。最后,我们看到一个指向变量的指针作为第二个参数传递给一个名为 的新函数。让我们来看看。kull_m_process_getProcessIdForName
mySearch
KULL_M_PROCESS_PID_FOR_NAME
uName
processId
FALSE
pid
processId
RtlInitUnicodeString
uName
name
lsass.exe
mySearch
kull_m_process_getProcessInformation
该功能非常简单明了。我们看到对函数的调用,其中第一个参数是一个枚举值(可以在此处找到此枚举的源),第二个参数是指向缓冲区的指针,第三个参数是 0。kull_m_process_getProcessInfroamtion
kull_m_process_NtQuerySystemInformation
SystemProcessInformation
当我们调查该函数时,我们发现它只是为了调用 API 函数而构建的。是一个函数,旨在促进许多不同信息位的枚举,其中一个与正在运行的进程相关(这由前面看到的枚举值指示给函数)。问题在于,由于可以返回不可预测的类型和大小的结果,因此作者创建了一种机制来枚举预期的输出大小 ()。此功能之所以有效,是因为该函数将请求的信息的实际长度写入参数(此处用 表示),如果该值大于大小(由 here 表示),则函数将失败。然后,程序可以使用保留的值来分配适当大小的缓冲区,并再次传递它。kull_m_process_NtQuerySystemInformation
NtQuerySystemInformation
NtQuerySystemInformation
SystemProcessInformation
NtQuerySystemInformation
buffer
returnedLen
buffer
informationLength
returnedLen
但是,如果我们看一下第 19 行,此代码似乎没有利用该内置过程来确定输出数据所需的大小。相反,似乎 被初始化为字节的长度,如果函数失败,则该值将移到左侧一位 (),重新分配缓冲区,然后再次调用该函数。重复此过程,直到调用成功。buffer
0x1000
sizeOfBuffer
sizeOfBuffer <≤ 1
有了这种理解,我们就可以查看 API 文档以更好地了解它是如何工作的。一般来说,此代码部分旨在枚举所有进程并遍历它们,直到找到 .一旦找到它要查找的进程,它就会记录进程标识符并返回它以在下一步中使用。NtQuerySystemInformation
lsass.exe
经过短暂的绕行,我们回到了 ,其中以下兴趣行调用 Windows API 函数。请注意,传递了三个参数。第一个是在第 163 行设置的变量,该变量是 或取决于系统的主要版本。kuhl_m_sekurlsa_acquireLSA
OpenProcess
processRights
PROCESS_VM_READ | PROCESS_QUERY_INFORMATION
PROCESS_VM_READ | PROCESS_QUERY_LIMITED_INFORMATION
值得注意的是,这是将生成的句柄用于下一个函数调用所需的最低必要访问权限(我们将在后面的帖子中遇到)。由于该值是位字段,因此即使不需要其他工具,也可以添加访问权限以逃避静态检测。
PROCESS_VM_READ | PROCESS_QUERY(_LIMITED)_INFORMATION
processRights
第二个参数只是一个布尔值,第三个参数是从调用 派生的变量。换句话说,它是 LSASS 进程的进程标识符。FALSE
pid
kull_m_process_getProcessIdForName
如果我们不熟悉它的用途和参数,我们可以参考文档来更好地了解如何使用它。此函数用于打开进程的句柄。这是必需的,因为进程驻留在内核中,这意味着用户模式代码无法直接访问它们。相反,操作系统提供了一个接口 ,用于请求访问进程。然后,它根据任意访问控制列表 (DACL) 授予访问权限。查看文档页面,了解有关如何使用以及参数的含义的更多信息。OpenProcess
OpenProcess
OpenProcess
现在我们已经完成了对 的分析,我们可以返回并继续我们的调查。打开LSASS的进程句柄后,我们可以继续浏览代码,下面函数调用是。它似乎需要三个参数,两个指向结构的指针(数据和变量在第 295 行实例化)和一个 .让我们看一下 的代码。kuhl_m_sekurlsa_acquireLSA
kuhl_m_sekurlsa_enum
kull_m_memory_copy
KULL_M_MEMORY_ADDRESS
securityStruct
DWORD
kull_m_memory_copy
我们看到 的代码实现由一系列嵌套的 switch 语句组成,这意味着它的行为会有所不同,这取决于我们必须探索的一些变量,以了解命令的正确流程。kull_m_memory_copy
logonPasswords
第一个 switch 语句调查变量,该变量作为第一个参数传递给 。Destination
kull_m_memory_copy
如果我们看一下对(第 323 行)的调用,我们可以看到第一个参数是指向名为 的变量的指针。我们可以滚动到函数的顶部以查找变量的声明位置(第 295 行),并看到它的类型为 ,并且第一个字段 () 设置为指向 a 的指针,该指针设置为 1,第二个字段是指向称为 的常量值的指针。kull_m_memory_copy
&data
data
kuhl_m_sekurlsa_enum
data
KULL_M_MEMORY_ADDRESS
nbListes
ULONG
KULL_M_MEMORY_GLOBAL_OWN_HANDLE
请记住,中的 switch 语句查看变量,但准确地查看值。要准确确定该值是什么,我们必须检查分配给变量的类型的定义,即 。kull_m_memory_copy
Destination
Destination->hMemory->type
Destination
KULL_M_MEMORY_ADDRESS
从这个定义中,我们可以看到第一个字段表示某种地址。请记住,指向值为 1 的 a 的指针已分配给此字段。第二个字段是指向称为 的不同结构的指针。请注意,第二个字段的名称是 。这意味着我们正在理解嵌入的值,正如我们已经确定的那样,它被设置为一个名为 .现在我们需要弄清楚这个常数代表什么。ULONG
PKULL_M_MEMORY_HANDLE
hMemory
Destination->hMemory->type
hMemory
KULL_M_MEMORY_GLOBAL_OWN_HANDLE
我们可以在代码库中搜索结构的定义,发现它是 类型 .结构的第一个字段是该类型的实例(这看起来像我们感兴趣的),第二个字段设置为 。KULL_M_MEMORY_GLOBAL_OWN_HANDLE
KULL_M_MEMORY_HANDLE
KULL_M_MEMORY_TYPE_OWN
NULL
让我们看看类型的结构定义,看看我们正在处理什么。第一个字段是一个类型,被称为(这是拼图中缺失的部分)。我们可以回顾全局变量定义,并看到类型设置为 。这为我们提供了确定将在第一个 switch 语句中做出的选择所需的信息。KULL_M_MEMORY_HANDLE
KULL_M_MEMORY_TYPE
type
KULL_M_MEMORY_GLOBAL_OWN_HANDLE
KULL_M_MEMORY_TYPE_OWN
返回 时,我们可以看到 because 设置为 that switch 语句将选择第一种情况。kull_m_memory_copy
Destination->hMemory->type
KULL_M_MEMORY_TYPE_OWN
不要庆祝太多,因为一旦我们开始遵循所选案例条件中的代码,我们就会发现另一个 switch 语句。这一次,它正在调查参数,特别是字段。这与前面提到的 switch 语句相同,但重点放在而不是这次。该参数是传入的第二个参数,因此让我们回到调用函数并检查一下。Source
Source->hMemory->type
Source
Destination
Source
kull_m_memory_copy
我们可以看到(第 323 行)函数调用的第二个参数是指向一个名为 的变量的指针,该变量在(第 295 行)的开头实例化为 类型的 null 值。我们还看到(第 321 行)被设置为变量的值。kull_m_memory_copy
securityStruct
kuhl_m_sekurlsa_enum
KULL_M_MEMORY_ADDRESS
securityStruct.hMemory
cLsass.hLsassMem
现在是重新访问结构定义的好时机,我们可以看到该字段的类型为 。KUHL_M_MEMORY_ADDRESS
hMemory
KULL_M_MEMORY_HANDLE
然后,我们可以查看结构定义,看看它有两个字段。第一个字段称为 ,我们感兴趣的值,用于确定我们将在 switch 语句中遵循哪种情况。第二个字段可以是四种可能的句柄类型(、、或)中的任何一种。KULL_M_MEMORY_HANDLE
type
PKULL_M_MEMORY_HANDLE_PROCESS
PKULL_M_MEMORY_HANDLE_FILE
PKULL_M_MEMORY_HANDLE_PROCESS_DMP
PKULL_M_MEMORY_HANDLE_KERNEL
因此,现在我们需要弄清楚变量的设置位置,以了解变量的内容。回想一下,我们之前发现该变量被指定为全局变量并使用值进行实例化。cLsass
securityStruct
cLsass
NULL
这意味着它必须在我们已经调查过的代码中的某个位置进行设置。请记住,它首先检查是否包含有效的句柄。如果没有,它会检查是否设置了。如果没有,它将执行下图中突出显示的代码部分。kuhl_m_sekurlsa_acquireLSA
cLsass.hLsassMem
pMinidumpName
在这里,我们看到一个名为 的变量被设置为 。我们可以看到它在函数的开头声明为类型的实例。我们对这个值很感兴趣,但我们必须首先了解这个值是如何分配给变量的。Type
KULL_MEMORY_TYPE_PROCESS
Type
kuhl_m_sekurlsa_acquireLSA
KULL_M_MEMORY_TYPE
cLsass
如果我们在调用后继续遵循代码,我们会找到另一个 if 语句,它在其中验证该语句是否有效。假设调用成功,那么它将是。这意味着后续行将在调用函数的位置执行。请注意传递给它的三个参数。是变量集(第 178 行)到 ,是 的输出,它是 LSASS 的进程句柄,并且是用于确定我们在正在探索的 switch 语句中选择哪种情况的值。OpenProcess
hData
OpenProcess
kull_m_memory_open
Type
KULL_M_MEMORY_TYPE_PROCESS
hData
OpenProcess
cLsass.hLsassMem
现在,我们可以调查该函数,看看它如何处理其参数。在函数中,我们看到传入的第三个参数现在称为 。我们首先看到(第 17 行),也称为调用函数,设置为参数。kull_m_memory_open
cLsass
*hMemory
(*hMemory)->type
cLsass->type
Type
接下来,我们会遇到一个基于第一个参数 switch 语句。我们从设置为 的调用函数中知道,因此我们可以看到代码将选择第二种情况。Type
Type
KULL_M_MEMORY_TYPE_PROCESS
在开始分析代码之前,必须了解从调用函数传入的参数名称不一定与被调用函数中的参数名称相对应。例如,在调用函数中,调用第二个参数,但在调用函数中,调用第二个参数。类似地,该函数的第三个参数是指向 的指针,但在 中,它被称为 。kuhl_m_sekurlsa_acquireLSA
hData
kull_m_memory_open
hAny
kuhl_m_sekurlsa_acquireLSA
cLsass
kull_m_memory_open
*hMemory
在代码中,我们看到(第 26 行)分配给 。这意味着这是 API 调用生成的句柄。hAny
(*hMemory)->pHandleProcess->hProcess
cLsass->Type
KULL_M_MEMORY_TYPE_PROCESS
cLsass->pHandleProcess-hProcess
OpenProcess
最后,我们确定了 is 的值,这意味着我们可以看到代码将遵循下图中突出显示的第二种情况。从这里开始,代码相对简单,因为我们看到了对 Windows API函数 ,该函数用于从源地址读取LSASS进程的内存(您可能可以从函数的名称中找出)。Source->hMemory->type
KULL_M_MEMORY_TYPE_PROCESS
ReadProcessMemory
与前两个函数调用一样,我们可以查看 Microsoft 文档中的 ReadProcessMemory,以了解它的使用方式以及调用时的作用。这代表了我们今天分析的结束,我们发现 Mimikatz 的命令使用 Windows API 函数来访问 LSASS 进程内存的内容并访问目标凭据。sekurlsa::logonPasswords
ReadProcessMemory
在分析了 Mimikatz 的命令后,我们发现它通常调用三个 Windows API 函数:获取 LSASS 进程的进程标识符 (PID)、打开 LSASS 的读取句柄,以及读取 LSASS 内存的内容,其中可能存储了要转储的凭据。为了显示这些调用之间的关系,我在下面创建了一个图表:sekurlsa::logonPasswords
NtQuerySystemInformation
OpenProcess
ReadProcessMemory
现在,我们可以按照我的 了解函数调用堆栈 一文中描述的过程来深入研究这些函数调用中的每一个。一个有趣的副作用是,我们可以看到这些函数调用是交织在一起的,因为第一次调用的输出需要作为第二次调用的输入,而第二次调用的结果需要作为第三次调用的输入。这意味着函数序列可能与单个函数本身一样有趣。
当我刚开始从事信息安全工作时(在我真正了解 API 函数的工作原理之前),经常听到人们描述一种表示“进程注入”的模式。该模式是 VirtualAllocEx、WriteProcessMemory 和 CreateRemoteThread(有趣的是,它们没有提到 OpenProcess,因为这三者都需要作为输入),我们经常被告知,如果你看到这个模式,那么你就看到了进程注入:动态链接库。我们至少建立了一种可能在 OS 凭据转储中常见的模式 - Lsass 内存类型行为 (NtQuerySystemInformation、OpenProcess、ReadProcessMemory) 。不过,我们不应假设此模式的每个实例都是凭据转储,或者这是唯一有效的凭据转储模式。您能想到任何其他可能指示凭据转储的函数组合吗?我们将在下一篇文章中探讨这个问题的答案!
其它课程
二进制漏洞课程(更新中)
windows网络安全防火墙与虚拟网卡(更新完成)
windows文件过滤(更新完成)
USB过滤(更新完成)
游戏安全(更新中)
ios逆向
windbg
还有很多免费教程(限学员)
更多详细内容添加作者微信