freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

【安全研究】从mimikatz学习万能密码
2021-09-17 17:55:59

1.背景介绍

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

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

2.模块详细分析

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

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

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

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

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

接下来传入lsass.exe到kull_m_process_getProcessIdForName函数中用来获取lsass进程的pid,该函数主要通过NtQuerySystemInformationAPI函数来获取1631868708_61445724f090249c4e984.png!small?1631868682025

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

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

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

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

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

1631868755_61445753a16999989c377.png!small?1631868728527

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

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

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

主要是利用PEB找到LDR以及双向链表InLoadOrderModuleList来遍历lsass进程模块找到kdcsvc.dll1631868788_614457742a655136a69d1.png!small?1631868760823

1631868793_61445779223bd092e6968.png!small?1631868765611

可以看到确实获取到了kdcsvc.dll的模块信息并把信息存储在cryptInfos结构体中1631868803_6144578340f0347c726a0.png!small?1631868777641

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

跟进这个函数首先定义了一个跟参数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的case1631868816_614457902f82e9225001a.png!small?1631868788855

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

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

所以到这里kull_m_memory_search目的就是找Kerberos-Newer-Keys字符串在kdcsvc.dll中的位置。1631868846_614457ae555ce7e9d9d2a.png!small?1631868818857

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

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

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

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

0x7ffa33072b60就存储了该USER_PROPERTY结构体,其中属性名称是7ffa33066a8指向Kerberos-Newer-Keys字符串1631868930_61445802b28a38912c36c.png!small?1631868905804

接下来通过rtlzeromemory函数将orig结构体置0然后通过kull_m_memory_copy函数将kdcsvc.dll中的USER_PROPERTY结构体也置0了1631868938_6144580ab3f565c2f4655.png!small?1631868911195

调用writeprocessmemory函数将7ffa33072b60地址处16字节的数据置01631868947_6144581373af2aabdbf34.png!small?1631868920455

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

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

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

我们跟进函数然后发现SampLoadDsExtensionDll函数继续跟进分析1631868973_6144582d2a253102d959b.png!small?1631868945532

发现该函数会查询注册表是否有DirectoryServiceExtPt值我的域控是2016的看了一下没有这个值1631868982_61445836a913e8549f1be.png!small?1631868956182

接下来会继续尝试加载该文件由于没有所以走下面的else返回0xc00000BB1631868987_6144583b7fd349bfcec16.png!small?1631868959962

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

我这里测了一下skeleton攻击并抓取了流量包,客户端发送的AS-REQ请求存在AES加密1631869000_61445848a94f39dbadaa0.png!small?1631868973586

但是域控的响应中却没有AES加密同时确实是报错STATUS_NOT_SUPPORTED1631869005_6144584db78a5e34fc106.png!small?1631868978970

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

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

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

1631869034_6144586ade416e25ff0bb.png!small?1631869007460

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

1631869044_61445874d4c6ba708ac34.png!small?1631869017534

接下来kull_m_remotelib_CreateRemoteCodeWitthPatternReplace函数中传入了几个函数名其实是传入函数的地址1631869090_614458a20236d848afe67.png!small?16318690624521631869094_614458a63c3cb75e7d1d7.png!small?1631869066831

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

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

可以看到被填充后的数据如下1631869118_614458beb864c3c4a57fc.png!small?1631869091517

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

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

1631869133_614458cde8b6129a213bf.png!small?1631869106294

替换后这些函数内容应该是这样的1631869140_614458d43f92d890e12ad.png!small?1631869112957

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

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

所以函数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函数1631869180_614458fc38a0ef6230eca.png!small?1631869152828

从下图可以看到该地址处替换前地址是00007ffa3cab84c0,右侧为替换后1631869190_614459066ccf22ed2f356.png!small?1631869169437

1631869199_6144590f289a0dc81060a.png!small?1631869174568

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

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

从下图我们可以看到hook前的情况7ffa3cabf638存的是原始的rc4HmacDecrypt函数1631869321_61445989972ebed7b47b2.png!small?1631869296071

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

1631869332_614459945c325907d2b47.png!small?1631869305185

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

我们给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所以第一排第二排的秘钥流一致。1631869342_6144599ee6bde2c1668d9.png!small?1631869315559

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

1631869358_614459ae16f250c4e5746.png!small?1631869330574

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

3.防御方案

由于该攻击是对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保护了1631869392_614459d046ce072bea668.png!small?1631869365028

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

windows日志4697记录了mimidrv.sys驱动的安装1631869401_614459d9145ebfb913539.png!small?1631869374094

sysmon日志13可以很明显看到mimidrv服务以及对应的驱动程序1631869411_614459e3d6a481d215bcc.png!small?1631869396110

我们可以使用zBang 工具扫描当前的域控环境是否已经被注入了万能密码。
(https://github.com/cyberark/zBang),因为Skeleton Key 是注入恶意代码到 lsass.exe 进程的,所以它只存在于内存中,如果域控制器重启,注入的 Skeleton Key 将会失效。

1631869422_614459ee1664ff1f48c59.png!small?1631869396113

# 系统安全 # 企业安全 # 网络安全技术
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录