1.背景介绍

2015年1月2日,Dell Secureworks共享了一份关于利用专用域控制器(DC)恶意软件(名为“SkeletonKey”恶意软件)进行高级攻ji活动的报告,SkeletonKey恶意软件修改了DC的身份验证流程,域用户仍然可以使用其用户名和密码登录,攻ji者可以使用Skeleton Key密码作为任何域用户登录

(http://www.secureworks.com/cyber-threat-intelligence/threats/skeleton-key-malware-analysis/),2015 年 1 月 17 日Benjamin Delpy更新了Mimikatz使其也能进行Skeleton Key攻ji,我们今天分析的主要内容也就是mimikatz的misc::skeleton功能。

2.模块详细分析

misc::skeleton功能模块在kuhl_m_misc.c文件中,从该c文件开头可以看到作者定义了一个结构体数组用来存储当前misc模块中具体有哪些功能,该结构体有3个成员第一个是功能函数,第二个是启动该功能的参数,第三个是功能描述所以我们今天要分析的skeleton在这个kuhl_m_misc_skeleton函数中启动命令为misc::skeleton暂无描述,可以看到其他该misc模块是有很多功能是日常使用中也没有用上后续可以挖掘功能

所以我们在这个kuhl_m_misc_skeleton函数下个断点当我们执行misc::skeleton时就可以执行到这里开始调试,在该函数开始前还定义一个数组存储了一个字符串Kerberos-Newer-Keys,这是个很重要的字符串后续用到的时候详细解释

可以看到函数开始定义了很多变量,可以看到extensions结构体初始化了一些值,我们查看一下这个结构体可以发现该结构体成员有4个从名称大概可以猜出来分别为模块,功能,替换的值,指针,这样我们大概就可以这个结构体数组是什么了第一个结构体就是模块是kernel32.dll,localAlloc函数,替换的值为0X4a4a4a4a4a,指针为NULL,从现在来看这个结构体大部分内容是没意义的还有很多为NULL的

接下来又定义了一个结构体exforcb来存储extensions以及extensions的大小,判断当前Mimikatz的版本号是否小于vista系统的版本号这里我们的软件版本11是大于宏定义的visita系统5000的或者传入命令参数是否有letaes我们这里传入的是misc::skeleton所以两个判断都不为真onlyRC4Stuff为0 这样也才能进入下面的功能操作。接着利用RtlZeroMemory函数初始化orig结构体填充0

接下来用RtlInitUnicodeString函数计算出Kerberos-Newer-Keys字符串的长度并将该字符串以及长度保存在orig结构体中

接下来传入lsass.exe到kull_m_process_getProcessIdForName函数中用来获取lsass进程的pid,该函数主要通过NtQuerySystemInformationAPI函数来获取

【安全研究】从mimikatz学习万能密码_网络安全_07

接下来利用openprocess函数传入开始lsass进程的pid来获取lsass进程的句柄,然后将KULL_M_MEMORY_TYPE_PROCESS,lsass进程的句柄,&alsass.hmemory传入kull_m_memory_open函数中,alsass也是kuhl_m_misc_skeleton函数一开始定义的结构体里面有2个成员一个存储地址的指针,另一个成员又是一个结构体主要表示当前句柄的类型或者内存的属性

跟进这个函发现会根据传入的参数走KULL_M_MEMORY_TYPE_PROCESScase该函数主要就是给alsass.hmemory结构体赋值

接下来由于onlyRC4Stuff为0所以可以进入里面的流程我们看到kull_m_process_getVeryBasicModuleInformationsForName传入了3个参数开始的alsass.hmemory,kdscsvc.dll字符串,和另一个cryptinfos结构体的引用这个结构体主要是一个存储模块的基础信息成员有模块基地址,模块大小,时间戳,没有公开的函数

跟进函数其实该函数主要是通过kull_m_process_getVeryBasicModuleInformations函数获取kdscsvc.dll的详细信息,kull_m_process_callback_moduleForName是个回调函数主要是用结构体赋值的

进入函数主要是跟内存属性走不同case通过peb和ldr遍历进程的模块来获取kdscsvc.dll的详细信息

跟进kull_m_process_peb函数可以看到根据不同内存属性来走对应的case获取peb信息,这里是通过NtQueryInformationProcessAPI获取

由于VS是3环调试器无法读取到内核的信息,我们可以搭建双机调试使用windbg读取一下0x0000008f85514000PEB结构体信息

函数最开始有定义一个结构体来存储peb相关信息,使用kull_m_memory_copy函数根据不同的内存属性调用不同的内存拷贝函数这里调用的ReadProcessMemory函数

主要是利用PEB找到LDR以及双向链表InLoadOrderModuleList来遍历lsass进程模块找到kdcsvc.dll

可以看到确实获取到了kdcsvc.dll的模块信息并把信息存储在cryptInfos结构体中

接下来将Kerberos-Newer-Keys字符串指针传递给alocal结构体中,刚刚获取到kdcsvc.dll的基地址以及dll的大小传递到smemory结构体中这两个结构体的属性见下图也是函数开始定义的,将2个结构体以及字符串长度传入kull_m_memory_search函数中

跟进这个函数首先定义了一个跟参数search一样的结构体并把属性定义为了KULL_M_MEMORY_GLOBAL_OWN_HANDLE这个结构体存的是KULL_M_MEMORY_TYPE_OWN属性,这里因为下面流程根据参数里面的内存属性走了KULL_M_MEMORY_TYPE_PROCESScase然后作者就使用kull_m_memory_copy函数(里面跟进内存属性走对应的case这里使用了ReadProcessMemory函数拷贝)将search结构体内容都拷贝自己定义的sbuffer结构体中然后又再次调用kull_m_memory_search函数进入这个函数由于sbuffer结构体成员属性作者定义为了KULL_M_MEMORY_TYPE_OWN所以会走KULL_M_MEMORY_TYPE_OWN的case

进入KULL_M_MEMORY_TYPE_OWNcase后利用for循环从kdcsvc.dll的首地址2c28dde0080开始查找Kerberos-Newer-Keys字符串,由于最后匹配上之后继续执行了一次curentPtr++后续通过currentptr--调整回来指向kdcsvc.dll中Kerberos-Newer-Keys字符串的正确地址

由于search结构体中存储了kdcsvc.dll内容但是没办法直接在其内存中搜索字符串将其拷贝到sbuffer后然后在sbuffer中找到Kerberos-Newer-Keys字符串并计算其偏移再加上kdcsvc.dll真实的首地址就获得了该字符串的真实偏移通过windbg中查询数据可以看到

所以到这里kull_m_memory_search目的就是找Kerberos-Newer-Keys字符串在kdcsvc.dll中的位置。

kdcsvc.dll是windowsserver系统上才有的dll文件如果是域控服务器的话lsass进程会加载该dll作为kdc服务来颁发piao据

ntds.dit数据库文件有个补充凭证(supplementalCredentials)属性该属性中又包含了Kerberos-Newer-Keys属性存储了明文密码的加密hash,kerberos-Newer-Keys可以设置各种加密属性比如AES加密的话会涉及salt参加加密,但是RC4-HMAC加密就不涉及salt了。Kerberos新的加密类型(如AES)会要求将salt字符串(通常是用户名)添加到密钥派生函数中,要使不同用户的密码相同,需要创建不相同的加密密钥。如果攻ji者要支持AES加密的话需要离线计算并存储所有域用户的密钥,这需要大量内存;或实时计算相关用户的密钥,这可能会导致DC的性能问题,但RC4-HMAC不涉及salt,所有用户的骨架RC4-HMAC密钥不变,所以攻ji者一般会采取RC4-HMAC加密。

其中在WIN2008及其更新的版本中必须创建一个KERB_STORED_CREDENTIAL_NEW结构体。然后必须将此值 与属性名称“Primary:Kerberos-Newer-Keys”一起放置在USER_PROPERTY结构中才能放在补充凭据中使用,USER_PROPERTY如下图

由于USER_PROPERTY结构体中包含了Kerberos-Newer-Keys字符串所以接下来又通过kull_m_memory_search函数从kdcsvc.dll中搜索该结构体的位置

0x7ffa33072b60就存储了该USER_PROPERTY结构体,其中属性名称是7ffa33066a8指向Kerberos-Newer-Keys字符串

接下来通过rtlzeromemory函数将orig结构体置0然后通过kull_m_memory_copy函数将kdcsvc.dll中的USER_PROPERTY结构体也置0了

调用writeprocessmemory函数将7ffa33072b60地址处16字节的数据置0

数据拷贝后内存值可以看到Kerberos-Newer-Keys包没有了这样就可以保证没办法使用带salt的AES加密方法了

我们可以通过ida打开kdcsvc.dll查看具体的Kerberos-Newer-Keys包的使用细节发现是SamIRetrieveMultiplePrimaryCredentials函数调用的

由于SamIRetrieveMultiplePrimaryCredentials函数是samsrv.dll的导出函数所以想知道具体的返回值我在samsrv.dll中进行了分析SampExtRetrieveMultiplePrimaryCredentialsDs是其主要实现

我们跟进函数然后发现SampLoadDsExtensionDll函数继续跟进分析

发现该函数会查询注册表是否有DirectoryServiceExtPt值我的域控是2016的看了一下没有这个值

接下来会继续尝试加载该文件由于没有所以走下面的else返回0xc00000BB

通过微软官方文档查询报错值发现是不支持该请求类型的错误STATUS_NOT_SUPPORTED,keberos认证流程中客户端会在AS-REQ中声明自己支持的所有加密类型etype,域控会并检查客户端是否支持AES如果客户端支持则域控会在AS-REP的PA-ETYPE-INFO2中响应对应的加密方式。但如果客户端支持的加密类型中有AES,但DC并没有回应对应的AES加密类型,并且报错类型是STATUS_NOT_SUPPORTED很大可能受到了skeleton攻ji

我这里测了一下skeleton攻ji并抓取了流量包,客户端发送的AS-REQ请求存在AES加密

但是域控的响应中却没有AES加密同时确实是报错STATUS_NOT_SUPPORTED

我们继续看下kdcsvc.dll的流程在域控通过kdcgetuserskey函数接收到用户的密码后不单单是会走SamIRetrieveMultiplePrimaryCredentials函数同时会将密码传递给kerbhashpassword函数最终调用cryptdll.dll的导出函数CDLocateCSystem选择对应的加密系统对用户传入的凭据进行处理

接下来就可以看到mimikatz继续利用kull_m_process_getVeryBasicModuleInformationsForName函数也是通过pebldr从lsass进程中获取cryptdll.dll的模块信息,同时获取了该dll的句柄保存在localaddr中

然后通过CDLocateCSystem函数找到RC4类型的加密系统存储在pCrypt结构体中,可以看到该加密类型的一系列函数存储在结构体中

接下来给最开始定义的extensions赋值,将cryptdll.dll!rc4HmacInitialize,cryptdll.dll!rc4HmacDecrypt函数地址传入

接下来kull_m_remotelib_CreateRemoteCodeWitthPatternReplace函数中传入了几个函数名其实是传入函数的地址

第一个参数是lsass进程的内存属性,第二个是rc4_init函数的地址,第三个参数rc4_end函数地址-rc4_init函数地址代表的是rc4_init到rc4_end之间的内存内容其实这块空间包含了init函数decrypt两块函数内容,第四个参数是存储extensions的结构体指针,第五个参数是lsass进程的指针

跟进后首先通过kull_m_remotelib_GetProcAddressMultipleModules函数这个函数主要也是调用kull_m_process_getVeryBasicModuleInformations函数通过PEBldr获取模块信息,给RemoteExt结构体也就是extensions结构体数组填充对应的值

可以看到被填充后的数据如下

这里是新创了个空间aLocalAddr大小跟rc4_end-rc4_init函数地址相距的大小一致,并把rc4_end-rc4_init函数地址内存的内容拷贝到新空间中,并将extensions[j].ToReplace的值在该空间中查找其对应的位置,后续用extensions[j].Point的值替换该值

我们可以看到rc4_init函数到rc4_end函数内存空间中还有rc4_init_decrypt函数其中rc4_init函数与rc4_decrypt函数中有很多函数不过名字为0x4a4a这种一串奇怪的数字,同时我们前面被填充好的extensions结构体数组也存在这些奇怪的数字并且存在正常的模块名称及其对应的函数名以及函数对应的地址,就是通过内存搜索这些特殊字符然后进行对应的替换。

替换后这些函数内容应该是这样的

该空间内存内容替换为正常的函数内容后使用kull_m_memory_alloc函数在lsass进程中开辟了一块可读可写可执行的内存空间并且大小也是函数rc4_int到函数rc4_end内存空间的大小并通过kull_m_memory_copy函数将该内容拷贝lsass进程中拷贝到新开辟的空间中

可以看到下图左边是lsass进程中新开辟的内存空间的内容与我们之前函数空间的内容一致。新开辟的地址是000001f3`d4fb0000

所以函数kull_m_remotelib_CreateRemoteCodeWitthPatternReplace主要的目的就是将rc4_int,rc4_decrypt函数修复后将代码注入到lsass空间中,后续通过kull_M_memory_copy函数将原地址7ffa3cabf628指向的是cryptdll!rc4HmacInitialize函数的指针替换为了0xc79bcdfd88地址中存储的8字节的值也就是01f3d4fb0000,这块地址指向的我们自己定义的kuhl_misc_skeleton_rc4_init函数的地址也就是hook了原始的rc4HmacInitialize函数

从下图可以看到该地址处替换前地址是00007ffa3cab84c0,右侧为替换后

原始的RC4_init函数被hook后,后续又开始计算自己定义的rc4_decrypt函数相较于rc4_init函数的偏移,然后ptrValue加上偏移就是指向我们自定义rc4_decrypt函数的地址指针,aLsass.address也通过偏移计算得到lsass进程中原始的rc4_decrypt函数的地址的指针,然后通过kull_m_memory_copy函数将我们的rc4_decrypt函数替换掉原始的

可以看到又是调用WriteProcessMemory函数将原地址7ffa3cabf638指向的是cryptdll!rc4HmacDecrypt函数的指针替换为了0xc79bcdfc20地址中存储的8字节的值也就是01f3d4fb016c,这块地址指向的我们自己定义的kuhl_misc_skeleton_rc4_init_decrypt函数的地址也就是hook了原始的rc4HmacDecrypt函数

从下图我们可以看到hook前的情况7ffa3cabf638存的是原始的rc4HmacDecrypt函数

到这里skeleton模块函数已经全部分析完,接下来我们来分析一下我们自定义的rc4初始化函数,在rc4init函数中首先有个kiwiKey数组我们的认证都是传输的hash这个数组正好也是32位很有可能就是skeleton的默认密码mimikatz的hash 通过计算mimikatz的hash发现这里确实就是存的skeleton的密码hash

通过LocalAlloc分配了内存指向pContext,将Key(用户的输入的原始密码hash的地址)传入rc4HmacInitialize函数中生成原始密码的秘钥流完成后续的rc4加密解密的操作,将生成的秘钥流拷贝到开始分配的空间中大小为16字节,接着将kiwiKey(mimikatz字符串的hash)传入rc4HmacInitialize函数中生成mimikatz的秘钥流,接着又继续拷贝到开始分配的空间中大小为16字节,最后将原始的密码hash地址Key也拷贝到pContext空间中

我们给rc4HmacInitialize函数下断点域控输入net use \\win2016\c$/user:"test.com\administrator" "mimikatz",断下来后我们执行完*(LPCVOID *) ((PBYTE) *pContext + 32)= Key这段代码就可以看到pContext存储的内容了也就是上图反汇编中rax指向的地址,我们查看dq rax 第一排是原始密码生成的秘钥流,第二排是mimikatz生成的秘钥流,第三排前8字节是我们输入的原始密码hash的地址查看该地址的内容发现就是mimikatz的hash,这里由于我们的原始密码输入的就是mimikatz所以第一排第二排的秘钥流一致。

由于rc4的对称算法加密与解密是一致的都是用上面的秘钥流,解密函数中传入了开始init函数中的pContext以及被加密的数据,首先函数里面也是先分配了一段空间buffer,将待解密的数据拷贝到buffer中,将buffer以及pContext传入rc4HmacDecrypt函数中进行解密,这里是利用第一次的秘钥流解密如果解密成功的话就返回了,如果解密失败会再次调用rc4HmacDecrypt函数使用第二次我们设置的mimikatz生成的秘钥流解密如果成功就会改变pContext存在的原始Key改成我们设置的kiwikey的值,这样的方式就保证了我们无论是输入正确的密码还是我们设置的kiwikey来走rc4初始化加密解密流程都是可以通过的。

自定义kiwikey:zhuzhuxiaoba重新编译后认证成功,不过当前的mimikatz的skeleton万能密码只支持kerberos流程,其实在ntlm认证流程中也是可以实现万能钥shi的早期的万能钥shi的恶意样本就是这样操作的。通过在MSV1_0.dll中定位函数MsvpSamValidate,MsvpSamValidate函数中有一个对MsvpPasswordValidate函数的调用,通过hook该函数,同样的可以实现万能密码的功能。github上有个类似的利用笔者在win10系统上测了一下确实可以利用,不过在server上未能复现

3.防御方案

由于该攻ji是对lsass进程进行了注入, 从Windows 8.1(和Server 2012 R2)开始,Microsoft引入了一项称为LSA保护的功能。此功能基于PPL技术,它是一种纵深防御的安全功能,旨在“防止非管理员非PPL进程通过打开进程之类的函数串改PPL进程的代码和数据”。防止对进程lsass.exe 的代码注入,这样一来就无法使用 mimikatz 对 lsass.exe 进行注入,相关操作也会失败。在注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa

添加RunAsPPL=dword:00000001,重启就可以开启了PPL保护了

开启后mimikatz注入lsass进程的行为将会失败,虽然mimikatz后面又增加了一个mimidrv.sys驱动来绕过PPL保护,但是加载驱动这一行为已经有明显的日志可以供我们进行检测。

windows日志4697记录了mimidrv.sys驱动的安装

sysmon日志13可以很明显看到mimidrv服务以及对应的驱动程序

我们可以使用zBang 工具扫描当前的域控环境是否已经被注入了万能密码。

(https://github.com/cyberark/zBang),因为Skeleton Key 是注入恶意代码到 lsass.exe 进程的,所以它只存在于内存中,如果域控制器重启,注入的 Skeleton Key 将会失效。