Loading... ## 某蒸汽平台DRM逆向分析 之前在玩国产一个恐怖游戏,因为选的噩梦模式没法中途降难度,所以来分析下游戏的逻辑。最后发现实际上去分析逆向了蒸汽平台强制登陆Steam以及保护游戏代码的逻辑,分析完搜索了下发现这个保护机制叫做[SteamDRM](https://partner.steamgames.com/doc/features/drm)。 开始分析,PaperDollsOriginal.exe是入口 ## 启动程序 ~~~c++ int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { char *v6; // rdi __int64 v8; // rax __int64 v9; // rcx WCHAR *v10; // rbx char *v11; // r14 __int64 v12; // rcx __int64 v13; // rax __int64 v14; // rcx __int64 v15; // rax __int64 v16; // rcx __int64 v17; // rax unsigned __int64 v19; // rbx wchar_t *v20; // rsi DWORD v21; // ebx DWORD LastError; // edi __int64 v23; // rcx WCHAR *v24; // rbx DWORD ExitCode; // [rsp+50h] [rbp-B0h] BYREF struct _PROCESS_INFORMATION ProcessInformation; // [rsp+58h] [rbp-A8h] BYREF struct _STARTUPINFOW StartupInfo; // [rsp+70h] [rbp-90h] BYREF WCHAR pszBuf[264]; // [rsp+E0h] [rbp-20h] BYREF WCHAR pszPath[264]; // [rsp+2F0h] [rbp+1F0h] BYREF WCHAR Filename[264]; // [rsp+500h] [rbp+400h] BYREF GetModuleFileNameW(hInstance, Filename, 0x208u); PathCanonicalizeW(pszBuf, Filename); PathRemoveFileSpecW(pszBuf); v6 = sub_7FF618CA1270(hInstance, (const WCHAR *)201); if ( v6 ) { v8 = -1LL; do ++v8; while ( *(_WORD *)&v6[2 * v8] ); v9 = -1LL; do ++v9; while ( pszBuf[v9] ); v10 = (WCHAR *)operator new(saturated_mul(v8 + 20 + v9, 2uLL)); wsprintfW(v10, L"%s\\%s", pszBuf, v6); PathCanonicalizeW(pszPath, v10); j_j_free(v10); PathRemoveFileSpecW(pszPath); v11 = sub_7FF618CA1270(hInstance, (const WCHAR *)202); v12 = -1LL; do ++v12; while ( *(_WORD *)&lpCmdLine[2 * v12] ); v13 = -1LL; do ++v13; while ( *(_WORD *)&v11[2 * v13] ); v14 = v13 + v12; v15 = -1LL; do ++v15; while ( *(_WORD *)&v6[2 * v15] ); v16 = v15 + v14; v17 = -1LL; while ( pszBuf[++v17] != 0 ) ; v19 = v16 + v17 + 20; v20 = (wchar_t *)operator new(saturated_mul(v19, 2uLL)); swprintf(v20, v19, L"\"%s\\%s\" %s %s", pszBuf, v6, v11, lpCmdLine); j_j_free(v11); j_j_free(v6); v21 = sub_7FF618CA1000(pszBuf, pszPath); if ( !v21 ) { memset(&StartupInfo.cb + 1, 0, 0x64uLL); StartupInfo.cb = 104; memset(&ProcessInformation, 0, sizeof(ProcessInformation)); if ( CreateProcessW(0LL, v20, 0LL, 0LL, 0, 0, 0LL, 0LL, &StartupInfo, &ProcessInformation) ) { WaitForSingleObject(ProcessInformation.hProcess, 0xFFFFFFFF); ExitCode = 9006; GetExitCodeProcess(ProcessInformation.hProcess, &ExitCode); CloseHandle(ProcessInformation.hThread); CloseHandle(ProcessInformation.hProcess); v21 = ExitCode; } else { LastError = GetLastError(); v23 = -1LL; do ++v23; while ( v20[v23] ); v24 = (WCHAR *)operator new(saturated_mul(v23 + 50, 2uLL)); wsprintfW(v24, L"Couldn't start:\n%s\nCreateProcess() returned %x.", v20, LastError); MessageBoxW(0LL, v24, 0LL, 0); j_j_free(v24); v21 = 9005; } } j_j_free(v20); return v21; } else { MessageBoxW(0LL, L"This program is used for packaged games and is not meant to be run directly.", 0LL, 0); return 9000; } } ~~~ 可以发现获取了当前目录和一个ID为201的资源数据,使用ResourceHacker查看发现资源值为`PDS/Binaries/Win64/PDS-Win64-Shipping.exe` <img src="http://xherlock.top/usr/uploads/2026/04/2239604499.png" alt="image-20260318105507639" style="zoom:50%;" style=""> 利用swprintf将当前目录和资源里的目录拼接 接着又读取了ID为202的资源数据(值为`PSD`),然后和上一个做了拼接,最终拼接的启动命令是`"游戏目录\PDS/Binaries/Win64/PDS-Win64-Shipping.exe" PDS 其他参数` 接着调用sub_7FF618CA1000检查了`MSVCP140.DLL`、`ucrtbase.dll`等DLL依赖,如果不存在就启动`Engine\\Extras\\Redist\\en-us\\UE4PrereqSetup_x64.exe`进行安装 ~~~c++ __int64 __fastcall sub_7FF618CA1000(const WCHAR *a1, const WCHAR *a2) { DWORD ExitCode[4]; // [rsp+20h] [rbp-E0h] BYREF SHELLEXECUTEINFOW pExecInfo; // [rsp+30h] [rbp-D0h] BYREF WCHAR pszDest[264]; // [rsp+A0h] [rbp-60h] BYREF wchar_t Destination[1024]; // [rsp+2B0h] [rbp+1B0h] BYREF WCHAR Text[1024]; // [rsp+AB0h] [rbp+9B0h] BYREF memset(Destination, 0, sizeof(Destination)); if ( !LoadLibraryW(L"MSVCP140.DLL") && (PathCombineW(pszDest, a2, L"MSVCP140.DLL"), !LoadLibraryW(pszDest)) || !LoadLibraryW(L"ucrtbase.dll") && (PathCombineW(pszDest, a2, L"ucrtbase.dll"), !LoadLibraryW(pszDest)) ) { wcscat_s(Destination, 0x400uLL, L"Microsoft Visual C++ 2015 Runtime\n"); } if ( !LoadLibraryW(L"XINPUT1_3.DLL") ) { PathCombineW(pszDest, a2, L"XINPUT1_3.DLL"); if ( !LoadLibraryW(pszDest) ) wcscat_s(Destination, 0x400uLL, L"DirectX Runtime\n"); } if ( !Destination[0] ) return 0LL; wsprintfW(Text, L"The following component(s) are required to run this program:\n\n%s", Destination); PathCombineW(pszDest, a1, L"Engine\\Extras\\Redist\\en-us\\UE4PrereqSetup_x64.exe"); if ( GetFileAttributesW(pszDest) == -1 ) { MessageBoxW(0LL, Text, 0LL, 0); return 9001LL; } wcscat_s(Text, 0x400uLL, L"\nWould you like to install them now?"); if ( MessageBoxW(0LL, Text, 0LL, 4u) == 7 ) return 9002LL; memset(&pExecInfo, 0, sizeof(pExecInfo)); pExecInfo.cbSize = 112; pExecInfo.lpFile = pszDest; pExecInfo.fMask = 64; pExecInfo.nShow = 1; if ( !ShellExecuteExW(&pExecInfo) ) return 9003LL; ExitCode[0] = 0; WaitForSingleObject(pExecInfo.hProcess, 0xFFFFFFFF); GetExitCodeProcess(pExecInfo.hProcess, ExitCode); CloseHandle(pExecInfo.hProcess); if ( ExitCode[0] ) return 9004LL; else return 0LL; } ~~~ 是典型的Steam平台引导程序,不需要分析过多,直接去分析`PDS\Binaries\Win64`下的`PDS-Win64-Shipping.exe` ## PDS-Win64-Shipping.exe的.bind段 start函数首先push了全部寄存器  接着调用一个函数,开头计算出了start-0xf0的地址  发现地址指向一块没法反汇编的区域  ~~~assembly .bind:00007FF737420406 48 89 84 24 10 mov [rsp+0BC8h+var_8B8], rax .bind:00007FF737420406 03 00 00 .bind:00007FF73742040E .bind:00007FF73742040E loc_7FF73742040E: ; CODE XREF: sub_7FF7374203D0+97↓j .bind:00007FF73742040E 48 83 BC 24 18 cmp [rsp+0BC8h+var_8B0], 0 ; 初始值为0xF0,正好对应代码段长度 .bind:00007FF73742040E 03 00 00 00 .bind:00007FF737420417 76 50 jbe short loc_7FF737420469 .bind:00007FF737420419 48 8B 84 24 08 mov rax, [rsp+0BC8h+var_8C0] .bind:00007FF737420419 03 00 00 .bind:00007FF737420421 48 8B 8C 24 10 mov rcx, [rsp+0BC8h+var_8B8] .bind:00007FF737420421 03 00 00 .bind:00007FF737420429 0F B6 09 movzx ecx, byte ptr [rcx] .bind:00007FF73742042C 88 08 mov [rax], cl .bind:00007FF73742042E 48 8B 84 24 08 mov rax, [rsp+0BC8h+var_8C0] .bind:00007FF73742042E 03 00 00 .bind:00007FF737420436 48 FF C0 inc rax .bind:00007FF737420439 48 89 84 24 08 mov [rsp+0BC8h+var_8C0], rax .bind:00007FF737420439 03 00 00 .bind:00007FF737420441 48 8B 84 24 10 mov rax, [rsp+0BC8h+var_8B8] .bind:00007FF737420441 03 00 00 .bind:00007FF737420449 48 FF C0 inc rax .bind:00007FF73742044C 48 89 84 24 10 mov [rsp+0BC8h+var_8B8], rax .bind:00007FF73742044C 03 00 00 .bind:00007FF737420454 48 8B 84 24 18 mov rax, [rsp+0BC8h+var_8B0] .bind:00007FF737420454 03 00 00 .bind:00007FF73742045C 48 FF C8 dec rax .bind:00007FF73742045F 48 89 84 24 18 mov [rsp+0BC8h+var_8B0], rax .bind:00007FF73742045F 03 00 00 .bind:00007FF737420467 EB A5 jmp short loc_7FF73742040E .bind:00007FF737420469 ; --------------------------------------------------------------------------- .bind:00007FF737420469 .bind:00007FF737420469 loc_7FF737420469: ; CODE XREF: sub_7FF7374203D0+47↑j .bind:00007FF737420469 48 8D 84 24 50 lea rax, [rsp+0BC8h+var_378] ~~~ 接着开始循环拷贝字节到`[rsp+0BC8h+var_378]`,然后循环异或,每次后四字节异或前四字节  检查解密出来的qword0.HIDWORD是否为0xC0DEC0DF(类似魔数),然后计算了start-0x3380310(qword2),计算出的地址指向了一个ImaeBase HEADER PE头;  接着又计算了start-0x310(qword3.LODWORD),指向一个可疑代码段  拷贝这个代码段(大小0x220,经过(qword5.HIDWORD+15)&0x0FFFFFFF0计算得到,然后.bind位置+qword5.LODWORD得到代码起始位置)到内存,和上面相同的异或处理  解密完发现是API字符串  字符串如下,可以看到steam一些字眼以及API函数名 ~~~ b"calloc\x00free\x00vsprintf\x00TerminateProcess\x00GetLastError\x00OpenEventA\x00OpenFileMappingA\x00MapViewOfFile\x00WaitForSingleObject\x00CreateEventA\x00UnmapViewOfFile\x00CloseHandle\x00GetCurrentProcessId\x00VirtualAlloc\x00VirtualFree\x00VirtualProtect\x00IsBadReadPtr\x00OutputDebugStringA\x00FreeLibrary\x00afterimports\x00kernel32.dll\x00msvcrt.dll\x00user32.dll\x00GetProcAddress\x00GetModuleHandleA\x00LoadLibraryA\x00MessageBoxA\x00Local\\SteamStart_SharedMemFile\x00Local\\SteamStart_SharedMemLock\x00Steam Error\x00Application load error X:XXXXXXXXXX\x00Payload routine failed with %u ('%c')\n\x00Unpack step %u\n\x00steam" ~~~ 往后看就能看到开始GetModuleHandleA(ImageBase+qword25)调用API   获取了kernel32.dll地址,然后GetProcAddress(ImageBase+qword26)查找"LoadLibraryA"地址  然后获取了msvcrt.dll的calloc、free、vsprintf,kernel32.dll的TerminateProcess、GetLastError、OpenEventA等 接着是一处反调试(读取了qword7.HIDWORD,类似于CheckDebugFlag),这里需要手动修改BeingDebugged的值为0  否则弹窗报错  继续向下先calloc分配了0x28720大小(qword6.HIDWORD),对.bind的一块区域(当前函数后面的区域,偏移值为qword6.DOWORD)进行XTEA解密(密钥来自qword19、20)  解密完的结果异或v90(0x55填充),调试到解密完已经能看到PE头了,nice!  写一个脚本直接dump(后面分析可知是DLL,里面包含了steam启动相关) ~~~python def dump_bytes(start_ea: int, size: int, out_path: str) -> bool: """ Dump bytes from IDA database to a file. - start_ea: start address (EA) - size: number of bytes to dump - out_path: output file path (e.g., r"C:\temp\dump.bin") Returns True on success, False otherwise. """ if size <= 0: ida_kernwin.warning("Size must be > 0") return False # get_bytes returns None if it can't read the requested range data = ida_bytes.get_bytes(start_ea, size) if data is None: ida_kernwin.warning(f"Failed to read bytes at 0x{start_ea:X} (size=0x{size:X})") return False try: with open(out_path, "wb") as f: f.write(data) ida_kernwin.msg(f"[+] Dumped 0x{size:X} bytes from 0x{start_ea:X} to: {out_path}\n") return True except Exception as e: ida_kernwin.warning(f"Failed to write file:\n{out_path}\n\nError: {e}") return False start_ea = 0x297D66F0080 size = 0x28720 out_path = r"xxxx\steamdrmp.dll" dump_bytes(start_ea, size, out_path) ~~~ 对多段数据做了CRC校验 * 前面两段解密出来的数据 * a1(start地址开始的0x34A0字节,正好覆盖整个代码段),这里得把全部断点取消防止crc出现错误值,可以在比较值地方设置硬件断点 * 前面解密出来的DLL  正确设置断点和调试来到比较CRC值处可以发现值相等(CRC值为qword8.HIDWORD)  校验PE文件并把DLL写入内存  后面可以看到查找了WriteFile、ReadFile、SetFilePointerEx、CloseHandle等API 接着开始处理DLL  调用了一个导出函数,把前面解密出来的数据(结构体)和数据长度传入  要求返回值是48 ## steamdrmp.dll emmm drm=Digital Rights Management,看来用来保护的 DLL直接看导出函数,发现steam函数  ~~~c++ char __fastcall steam(__int64 a1, __int64 a2, unsigned __int64 a3) { HMODULE ModuleHandleA; // rax FARPROC ProcAddress; // rbx HANDLE CurrentThread; // rax DWORD TickCount; // r15d char v10; // al char v11; // r14 int v12; // edi LPWSTR CommandLineW; // rax CHAR *v14; // rbx WCHAR v15; // cx WCHAR v16; // cx WCHAR v17; // dx int v18; // eax CHAR v19; // cl __int64 v20; // rax unsigned __int8 v21; // cl _BYTE *v22; // r10 unsigned __int8 *v23; // r9 unsigned __int64 v24; // rdx __int64 v25; // r11 char v26; // cl char v27; // al __int64 v28; // rax __int64 v29; // r8 int v30; // eax unsigned int v31; // r14d _OWORD *v32; // rax _OWORD *v33; // rbx __int64 v34; // rsi const void *v35; // r12 HANDLE CurrentProcess; // rax CHAR ModuleName[16]; // [rsp+40h] [rbp-C0h] BYREF CHAR ProcName[16]; // [rsp+50h] [rbp-B0h] BYREF char v39[8]; // [rsp+60h] [rbp-A0h] BYREF struct _MEMORY_BASIC_INFORMATION Buffer; // [rsp+68h] [rbp-98h] BYREF WCHAR File[512]; // [rsp+A0h] [rbp-60h] BYREF WCHAR WideCharStr[2048]; // [rsp+4A0h] [rbp+3A0h] BYREF DWORD flOldProtect; // [rsp+14E0h] [rbp+13E0h] BYREF DWORD v44; // [rsp+14E8h] [rbp+13E8h] BYREF if ( a3 < 0xF0 || *(_DWORD *)(a2 + 4) != 0xC0DEC0DF ) return 81; if ( (*(_BYTE *)(a2 + 60) & 0x20) == 0 ) { if ( IsDebuggerPresent() ) return 84; *(__m128i *)ProcName = _mm_load_si128((const __m128i *)&xmmword_180018880); // NtSetInformation strcpy(ModuleName, "ntdll.dll"); strcpy(v39, "Thread"); ModuleHandleA = GetModuleHandleA(ModuleName); ProcAddress = GetProcAddress(ModuleHandleA, ProcName); CurrentThread = GetCurrentThread(); ((void (__fastcall *)(HANDLE, __int64, _QWORD))ProcAddress)(CurrentThread, 17LL, 0LL); } TickCount = GetTickCount(); if ( (*(_BYTE *)(a2 + 60) & 2) == 0 && !(unsigned __int8)sub_1800064E0((void *)(a1 - *(_QWORD *)(a2 + 16))) ) return 51; v10 = sub_180006680(a2); v11 = v10; if ( v10 != 48 ) { if ( v10 != 53 ) return v11; v12 = *(_DWORD *)(a2 + 56); CommandLineW = GetCommandLineW(); v14 = &MultiByteStr; if ( CommandLineW ) { v15 = *CommandLineW; if ( *CommandLineW != 34 ) goto LABEL_18; v16 = CommandLineW[1]; ++CommandLineW; if ( v16 ) { v17 = v16; do { v15 = v17; if ( v17 == 34 ) break; v15 = CommandLineW[1]; ++CommandLineW; v17 = v15; } while ( v15 ); LABEL_18: if ( v15 ) { while ( v15 != 32 ) { v15 = CommandLineW[1]; ++CommandLineW; if ( !v15 ) goto LABEL_25; } v18 = WideCharToMultiByte(0xFDE9u, 0, CommandLineW + 1, -1, &MultiByteStr, 2048, 0LL, 0LL); v19 = MultiByteStr; if ( !v18 ) v19 = 0; MultiByteStr = v19; } } } LABEL_25: swprintf(&::Buffer, 0x800uLL, "steam://run/%u//", v12); v20 = -1LL; byte_180027F4F = 0; do ++v20; while ( *((_BYTE *)&::Buffer + v20) ); v21 = MultiByteStr; v22 = (char *)&::Buffer + v20; v23 = (unsigned __int8 *)&::Buffer + v20; v24 = 2048 - v20; if ( MultiByteStr ) { v25 = 0x43FFFFFF01FFBLL; do { if ( (unsigned __int8)(v21 - 97) <= 0x19u || (unsigned __int8)(v21 - 45) <= 0x32u && _bittest64(&v25, (char)(v21 - 45)) ) { if ( v24 ) { v28 = 1LL; v29 = -1LL; goto LABEL_38; } v26 = 0; } else { if ( v24 >= 3 ) { v23[1] = a0123456789abcd[(unsigned __int64)v21 >> 4]; v27 = a0123456789abcd[v21 & 0xF]; v21 = 37; v23[2] = v27; v28 = 3LL; v29 = -3LL; LABEL_38: *v23 = v21; v24 += v29; v23 += v28; v26 = 1; goto LABEL_39; } v26 = 0; } LABEL_39: ++v14; if ( !v26 ) goto LABEL_43; v21 = *v14; } while ( *v14 ); } if ( v24 ) *v23 = 0; else LABEL_43: *v22 = 0; if ( (unsigned __int8)sub_180004D60((BYTE *)File, 0x200u)sub_180004D60(File) && MultiByteToWideChar(0xFDE9u, 0, (LPCCH)&::Buffer, -1, WideCharStr, 2048) > 0 ) { ShellExecuteW(0LL, 0LL, File, WideCharStr, 0LL, 1); return v11; } ShellExecuteA(0LL, "open", (LPCSTR)&::Buffer, 0LL, 0LL, 1); return v11; } v30 = *(_DWORD *)(a2 + 60); if ( (v30 & 4) == 0 ) { v31 = *(_DWORD *)(a2 + 80) + 32; v32 = j__malloc_base(v31); v33 = v32; if ( !v32 ) return 87; v34 = a1 - *(_QWORD *)(a2 + 16); v35 = (const void *)(v34 + *(_QWORD *)(a2 + 72)); *v32 = *(_OWORD *)(a2 + 120); v32[1] = *(_OWORD *)(a2 + 136); if ( !VirtualQuery(v35, &Buffer, 0x30uLL) ) return 68; v44 = 4; flOldProtect = 0; if ( !VirtualProtect(Buffer.BaseAddress, Buffer.RegionSize, 4u, &flOldProtect) ) return 68; sub_180005240(v34, *(_QWORD *)(a2 + 8) - v34, a2); memmove(v33 + 2, v35, *(unsigned int *)(a2 + 80)); if ( (unsigned int)sub_180004200((_DWORD)v33, v31, (int)a2 + 88, 32, 16, (__int64)v35, *(_DWORD *)(a2 + 80)) != *(_DWORD *)(a2 + 80) ) return 68; j__free_base(v33); sub_180005240(v34, v34 - *(_QWORD *)(a2 + 8), a2); if ( !VirtualProtect(Buffer.BaseAddress, Buffer.RegionSize, flOldProtect, &v44) ) return 68; CurrentProcess = GetCurrentProcess(); if ( !FlushInstructionCache(CurrentProcess, Buffer.BaseAddress, Buffer.RegionSize) ) return 68; v30 = *(_DWORD *)(a2 + 60); } if ( (v30 & 0x20) != 0 || GetTickCount() - TickCount <= 0x2710 ) return 48; else return 83; } ~~~ 很直白的函数,dll的话可以直接exe启动dll内存调试(Application填exe,目录填exe目录,input file为空) 首先检查了传入参数,要求a3不小于240,a2[4]数据要等于0xC0DEC0DF,正好对应前面exe里的分析 ### 反调试分析 接着进入if里会有3处反调试,分别是 1. IsDebuggerPresent 2. NtSetInformationThread(第二个参数17对应ThreadHideFromDebugger),只要调用就会挂掉调试器 3. GetTickCount(),在最后检查了时间是否超过10000ms ### 防篡改分析 进入sub_1800064E0,里面调用ReadFile读取了自身exe ~~~c++ char __fastcall sub_1800064E0(int *Buf1, const CHAR *a2) { void *v2; // rbx DWORD ModuleFileNameW; // eax char *FileW; // rax void *v6; // rsi DWORD FileSize; // eax unsigned __int64 v8; // r15 char *v9; // rax char *v10; // rdi void *v11; // rcx __int64 v12; // rcx WCHAR Filename[1044]; // [rsp+40h] [rbp-828h] BYREF DWORD NumberOfBytesRead; // [rsp+880h] [rbp+18h] BYREF HMODULE hModule; // [rsp+888h] [rbp+20h] BYREF v2 = 0LL; hModule = 0LL; if ( !GetModuleHandleExA(6u, a2, &hModule) ) goto LABEL_19; if ( !hModule ) goto LABEL_19; ModuleFileNameW = GetModuleFileNameW(hModule, Filename, 0x400u); if ( ModuleFileNameW - 1 > 0x3FE ) goto LABEL_19; Filename[ModuleFileNameW] = 0; FileW = (char *)CreateFileW(Filename, 0x80000000, 1u, 0LL, 3u, 0x80u, 0LL); v6 = FileW; if ( (unsigned __int64)(FileW - 1) > 0xFFFFFFFFFFFFFFFDuLL ) goto LABEL_19; FileSize = GetFileSize(FileW, 0LL); v8 = FileSize; if ( FileSize > 0x3FFFFFFF ) goto LABEL_17; v9 = (char *)j__malloc_base(FileSize); v10 = v9; v11 = v6; if ( !v9 ) { LABEL_18: CloseHandle(v11); goto LABEL_19; } if ( !ReadFile(v6, v9, v8, &NumberOfBytesRead, 0LL) || NumberOfBytesRead != (_DWORD)v8 ) { j__free_base(v10); LABEL_17: v11 = v6; goto LABEL_18; } v2 = v10; if ( !(unsigned int)sub_1800084C0(v10, (unsigned int)v8) ) { if ( !Buf1 || v8 >= 0x40 && !memcmp(Buf1, Buf1, 0x40uLL) && (v12 = Buf1[15], v8 >= v12 + 304) && (*(_QWORD *)&v10[v12 + 48] = *(_QWORD *)((char *)Buf1 + v12 + 48), !memcmp(Buf1, v10, v12 + 304)) ) { j__free_base(v10); return 1; } } LABEL_19: j__free_base(v2); return 0; } ~~~ 然后调用sub_1800084C0 ~~~c++ __int64 __fastcall sub_1800084C0(__int64 a1, __int64 a2) { return sub_180008090(a1, a2, &off_180026008, 2LL); } __int64 __fastcall sub_180008090(__int64 a1, unsigned __int64 a2, __int64 a3, unsigned int a4) { unsigned int v4; // r14d unsigned __int64 v6; // r10 unsigned int v8; // r15d __int64 v9; // rax __int16 v10; // cx __int64 v11; // r13 __int64 v12; // rdi int v13; // ecx __int64 v14; // rbx int v15; // eax char v16; // al unsigned int v17; // esi _BYTE *v18; // rdi _BYTE *v19; // r14 unsigned __int64 v20; // rax unsigned int v21; // r11d unsigned __int8 v22; // r10 unsigned __int8 v23; // r9 char v24; // r8 char v25; // r8 char v26; // dl __int64 v27; // [rsp+30h] [rbp-3A8h] __int128 v28; // [rsp+40h] [rbp-398h] __int128 v29; // [rsp+50h] [rbp-388h] __int128 v30; // [rsp+60h] [rbp-378h] __int128 v31; // [rsp+70h] [rbp-368h] __int128 v32; // [rsp+80h] [rbp-358h] __int128 v33; // [rsp+90h] [rbp-348h] __int128 v34; // [rsp+A0h] [rbp-338h] __int128 v35; // [rsp+B0h] [rbp-328h] _OWORD v36[8]; // [rsp+C0h] [rbp-318h] BYREF _BYTE v37[608]; // [rsp+140h] [rbp-298h] BYREF int v38; // [rsp+3E8h] [rbp+10h] v4 = a4; if ( a2 < 0x200 ) return 6LL; if ( *(_WORD *)a1 != 0x5A4D ) return 6LL; v6 = *(int *)(a1 + 60); if ( v6 < 0x40 || v6 >= a2 - 248 || *(_DWORD *)(v6 + a1) != 17744 ) return 6LL; if ( *(_DWORD *)(a1 + 64) != 'VLV' ) return 2LL; if ( *(_DWORD *)(a1 + 68) != 1 ) return 4LL; if ( a2 < *(unsigned int *)(a1 + 72) ) return 3LL; v8 = 0; v9 = v6 + a1; v10 = *(_WORD *)(v6 + a1 + 24); v27 = 0LL; v11 = 0LL; v12 = 0LL; if ( v10 != 0x10B ) { if ( v10 == 0x20B ) { v12 = v9 + 24; v27 = v9 + 24; goto LABEL_15; } return 3LL; } v11 = v9 + 24; LABEL_15: v28 = *(_OWORD *)(a1 + 80); v36[0] = v28; v29 = *(_OWORD *)(a1 + 96); v36[1] = v29; v30 = *(_OWORD *)(a1 + 112); v36[2] = v30; v31 = *(_OWORD *)(a1 + 128); v36[3] = v31; v32 = *(_OWORD *)(a1 + 144); v36[4] = v32; v33 = *(_OWORD *)(a1 + 160); v36[5] = v33; v34 = *(_OWORD *)(a1 + 176); v36[6] = v34; v35 = *(_OWORD *)(a1 + 192); v36[7] = v35; memset((void *)(a1 + 80), 0, 0x80uLL); if ( v11 ) { v13 = *(_DWORD *)(v11 + 64); *(_DWORD *)(v11 + 64) = 0; v14 = *(_QWORD *)(v11 + 128); *(_QWORD *)(v11 + 128) = 0LL; v38 = v13; } else { v15 = *(_DWORD *)(v12 + 64); *(_DWORD *)(v12 + 64) = 0; v14 = *(_QWORD *)(v12 + 144); v38 = v15; *(_QWORD *)(v12 + 144) = 0LL; } v16 = 0; v17 = 0; while ( v17 < v4 ) { v18 = *(_BYTE **)(a3 + 8LL * v17); if ( !v18 || !*v18 ) { v16 = 0; goto LABEL_36; } v19 = v37; v20 = -1LL; do ++v20; while ( v18[v20] ); if ( v20 > 0xFFFFFFFF || (v20 & 1) != 0 ) goto LABEL_34; v21 = 0; if ( (_DWORD)v20 ) { while ( 1 ) { v22 = (v18[v21 + 1] - 16) & 0x1F; v23 = (v18[v21] - 16) & 0x1F; if ( (v21 & 0xFFFFFFFE) >= 0x4B0 ) break; v24 = v23 - 7; if ( v23 <= 9u ) v24 = (v18[v21] - 16) & 0x1F; v25 = 16 * v24; v26 = v22 - 7; if ( v22 <= 9u ) v26 = (v18[v21 + 1] - 16) & 0x1F; v21 += 2; *v19++ = v26 + v25; if ( v21 >= (unsigned int)v20 ) goto LABEL_33; } LABEL_34: v4 = a4; v16 = 0; ++v17; } else { LABEL_33: v16 = sub_180005900((_BYTE *)(_DWORD)a1, *(_DWORD *)(a1 + 72), (int)v36, 128, (__int64)v37, v21 >> 1); v4 = a4; LABEL_36: ++v17; if ( v16 ) break; } } *(_OWORD *)(a1 + 80) = v28; *(_OWORD *)(a1 + 96) = v29; *(_OWORD *)(a1 + 112) = v30; *(_OWORD *)(a1 + 128) = v31; *(_OWORD *)(a1 + 144) = v32; *(_OWORD *)(a1 + 160) = v33; *(_OWORD *)(a1 + 176) = v34; *(_OWORD *)(a1 + 192) = v35; if ( v11 ) { *(_DWORD *)(v11 + 64) = v38; *(_QWORD *)(v11 + 128) = v14; } else { *(_DWORD *)(v27 + 64) = v38; *(_QWORD *)(v27 + 144) = v14; } LOBYTE(v8) = v16 == 0; return v8; } ~~~ 可以看到里面读取了PE文件,并提取了数据,下断点在`while ( v18[v20] );`处可以看到他读取了dll里的一个Hex字符串,然后转为字节作为参数传入sub_180005900,同时还传入了上一层exe的0x80字节   sub_180005900函数如下,可以看到常量 ~~~c++ char __fastcall sub_180005900(_BYTE *a1, unsigned int a2, int a3, int a4, __int64 a5, int a6) { __int64 v7; // rdi __int64 v8; // rdx __int64 v9; // rbx unsigned __int8 v10; // al int v11; // ecx _BYTE v13[32]; // [rsp+30h] [rbp-59h] _BYTE v14[64]; // [rsp+50h] [rbp-39h] BYREF _DWORD v15[5]; // [rsp+90h] [rbp+7h] int v16; // [rsp+A4h] [rbp+1Bh] __int16 v17; // [rsp+A8h] [rbp+1Fh] _DWORD v18[8]; // [rsp+B0h] [rbp+27h] BYREF v7 = a2; if ( (unsigned __int8)sub_1800053A0(a3, a4, a5, a6, (__int64)v18) ) { v9 = 0LL; v15[0] = 0x67452301; v10 = 0; v15[1] = 0xEFCDAB89; v15[2] = 0x98BADCFE; v11 = 0; v15[3] = 0x10325476; LOBYTE(v8) = 3; v15[4] = 0xC3D2E1F0; v16 = 0; v17 = 768; if ( v7 ) { while ( 1 ) { v16 = v11 + 1; --v7; v14[v10 ^ (unsigned __int64)(unsigned __int8)v8] = *a1; v10 = v17 + 1; LOBYTE(v17) = v10; if ( v10 == 64 ) { sub_180006A00(v14); v10 = 0; LOBYTE(v17) = 0; } ++a1; if ( !v7 ) break; v8 = HIBYTE(v17); v11 = v16; } } sub_180006B40(v14, v8); v13[0] = *((_BYTE *)v15 + HIBYTE(v17)); v13[1] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 1LL)); v13[2] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 2LL)); v13[3] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 3LL)); v13[4] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 4LL)); v13[5] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 5LL)); v13[6] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 6LL)); v13[7] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 7LL)); v13[8] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 8LL)); v13[9] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 9LL)); v13[10] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 0xALL)); v13[11] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 0xBLL)); v13[12] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 0xCLL)); v13[13] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 0xDLL)); v13[14] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 0xELL)); v13[15] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 0xFLL)); v13[16] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 0x10LL)); v13[17] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 0x11LL)); v13[18] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 0x12LL)); v13[19] = *((_BYTE *)v15 + (HIBYTE(v17) ^ 0x13LL)); while ( v18[v9] == *(_DWORD *)&v13[v9 * 4] ) { if ( (unsigned __int64)++v9 >= 5 ) return 1; } } return 0; } ~~~ 存在sha1加密特征,这部分涉及到了一些加密签名运算等,可以交给AI分析,分析结果如下 > 核心流程: > > 1. 调 sub_1800053A0(sig, sig_len, pub_der, pub_der_len, out_hash20),从签名里解出并校验 PKCS#1 v1.5 结构,提取出 20 字节哈希。 > 2. 对 data[0..data_len) 自己做一遍 SHA-1(内部初始化常量是 67452301/EFCDAB89/98BADCFE/10325476/C3D2E1F0,压缩函数是 sub_180006A00,收尾是 sub_180006B40)。 > 3. 将计算得到的 20 字节摘要与 out_hash20 比较,5 个 dword 全相等才返回 1。 > > sub_1800053A0 的证据点(你这份样本里): > > - 检查公钥 DER 前缀:30 0D 06 09 2A 86 48 86 F7 0D 01 01 05 00(RSA OID)。 > - 验证签名解密后块里 DigestInfo 前缀:00 30 21 30 09 06 05 2B 0E 03 02 1A 05 00 04 14,即 SHA1 的 ASN.1 标识。 > - 对应就是典型 RSA PKCS#1 v1.5 + SHA1 验签路径。 函数可以抽象为 ~~~c++ bool verify_sha1_sig( uint8_t *data, uint32_t data_len, // a1,a2 uint8_t *sig, uint32_t sig_len, // a3,a4 uint8_t *pub_der, uint32_t pub_der_len // a5,a6 ); ~~~ 验证通过后返回1 所以综上sub_1800064E0在检查**模块文件本身合法且签名通过,并且内存中的模块头没有被改** ### 启动Steam sub_180006680疑似调用了多个API,出现了多个函数字符串 ~~~c++ char __fastcall sub_180006680(__int64 a1) { __int64 v2; // rdi const CHAR *CreateInterface; // rax unsigned int v5; // esi unsigned int v6; // r12d char v7; // bl __int64 v8; // rax __int64 v9; // rbx int v10; // eax __int64 v11; // r14 int v12; // eax __int64 v13; // r13 __int64 (__fastcall ***v14)(_QWORD, _QWORD, _QWORD *, __int64, unsigned int *, unsigned int *, unsigned int *, int *); // rax unsigned int v15; // ebx int v16; // [rsp+40h] [rbp-C0h] BYREF unsigned int v17; // [rsp+44h] [rbp-BCh] BYREF HMODULE hModule; // [rsp+48h] [rbp-B8h] BYREF __int64 v19; // [rsp+50h] [rbp-B0h] BYREF _QWORD v20[128]; // [rsp+60h] [rbp-A0h] BYREF _BYTE v21[1024]; // [rsp+460h] [rbp+360h] BYREF unsigned int v22; // [rsp+8A8h] [rbp+7A8h] BYREF unsigned int v23; // [rsp+8B0h] [rbp+7B0h] BYREF int v24; // [rsp+8B8h] [rbp+7B8h] BYREF hModule = 0LL; v2 = sub_180007AF0(&hModule, 0, 0LL, (__int64)"SteamClient014"); if ( !v2 || !hModule ) return 53; CreateInterface = (const CHAR *)GetProcAddress(hModule, "CreateInterface"); if ( !CreateInterface ) { sub_180007F70(); return 66; } if ( !sub_1800064E0((int *)hModule, CreateInterface) ) { sub_180007F70(); return 51; } v5 = (**(__int64 (__fastcall ***)(__int64))v2)(v2); if ( !v5 ) { sub_180007F70(); return 53; } v6 = (*(__int64 (__fastcall **)(__int64, _QWORD))(*(_QWORD *)v2 + 16LL))(v2, v5); if ( !v6 ) { v7 = 53; LABEL_45: (*(void (__fastcall **)(__int64, _QWORD))(*(_QWORD *)v2 + 8LL))(v2, v5); sub_180007F70(); return v7; } v8 = (*(__int64 (__fastcall **)(__int64, _QWORD, const char *))(*(_QWORD *)v2 + 72LL))(v2, v5, "SteamUtils007"); v9 = v8; if ( !v8 ) goto LABEL_12; v10 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v8 + 16LL))(v8); v11 = v10; if ( v10 != 4 || (*(_BYTE *)(a1 + 60) & 0x10) != 0 ) { v12 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v9 + 72LL))(v9); if ( v12 ) { if ( v12 == *(_DWORD *)(a1 + 56) ) { v13 = (*(__int64 (__fastcall **)(__int64, _QWORD, _QWORD, const char *))(*(_QWORD *)v2 + 40LL))( v2, v6, v5, "SteamUser017"); v14 = (__int64 (__fastcall ***)(_QWORD, _QWORD, _QWORD *, __int64, unsigned int *, unsigned int *, unsigned int *, int *))(*(__int64 (__fastcall **)(__int64, _QWORD, _QWORD, const char *))(*(_QWORD *)v2 + 96LL))(v2, v6, v5, "STEAMAPPTICKET_INTERFACE_VERSION001"); if ( v13 ) { if ( v14 ) { v15 = (**v14)(v14, *(unsigned int *)(a1 + 56), v20, 1024LL, &v17, &v22, &v23, &v24); if ( v15 ) { if ( v17 && v22 && v23 && v24 && v17 != v22 && v17 != v23 && v22 != v23 && v23 <= v15 && v17 <= v15 - v24 && v22 <= v15 - v24 ) { if ( *(_DWORD *)((char *)v20 + v17) != *(_DWORD *)(a1 + 56) ) { v7 = 86; goto LABEL_44; } (*(void (__fastcall **)(__int64, __int64 *))(*(_QWORD *)v13 + 16LL))(v13, &v19); if ( *(_QWORD *)((char *)v20 + v22) == v19 && SHIDWORD(v19) >> 24 == (_DWORD)v11 ) { v16 = 1024; if ( (((_DWORD)v11 - 1) & 0xFFFFFFFC) != 0 || (_DWORD)v11 == 3 ) { v7 = 80; goto LABEL_44; } if ( (unsigned __int8)sub_180004E90(*(&off_180026030 + v11), v21, &v16) && (unsigned __int8)sub_180005900( (unsigned int)v20, v15 - v24, (unsigned int)v20 + v23, v24, (__int64)v21, v16) ) { v7 = 48; goto LABEL_44; } } } } } } v7 = 54; goto LABEL_44; } v7 = 86; LABEL_44: (*(void (__fastcall **)(__int64, _QWORD, _QWORD))(*(_QWORD *)v2 + 32LL))(v2, v5, v6); goto LABEL_45; } LABEL_12: v7 = 53; goto LABEL_44; } return 57; } ~~~ 先看sub_180007AF0 ~~~c++ __int64 __fastcall sub_180007AF0(HMODULE *a1, char a2, __int64 a3, __int64 a4) { char v5; // [rsp+38h] [rbp+10h] BYREF v5 = a2; return sub_1800076A0(a1, &v5, a3, a4); } __int64 __fastcall sub_1800076A0(HMODULE *a1, _BYTE *a2, __int64 a3, __int64 a4) { __int64 result; // rax char v8; // r15 __int64 v9; // rax HKEY v10; // rdi WCHAR *lpWideCharStr; // rbx LSTATUS v12; // edi WCHAR *v13; // rbx DWORD v14; // r8d HANDLE v15; // rax void *v16; // rdi char v17; // bl __int64 v18; // rdi WCHAR *v19; // rbx HMODULE Library; // rdi FARPROC CreateInterface; // rax __int64 (__fastcall *v22)(_QWORD, _QWORD); // rbx DWORD ExitCode; // [rsp+30h] [rbp-D0h] BYREF DWORD cbData; // [rsp+34h] [rbp-CCh] BYREF HKEY phkResult; // [rsp+38h] [rbp-C8h] BYREF DWORD Type[4]; // [rsp+40h] [rbp-C0h] BYREF CHAR OutputString[1040]; // [rsp+50h] [rbp-B0h] BYREF CHAR MultiByteStr[1072]; // [rsp+460h] [rbp+360h] BYREF DWORD Data; // [rsp+8A0h] [rbp+7A0h] BYREF sub_180007BD0(); if ( !a1 ) return 0LL; Destination = 0; qword_180028460 = 0LL; qword_180028468 = 0LL; qword_180028470 = 0LL; *a1 = 0LL; memset(MultiByteStr, 0, 0x410uLL); v8 = sub_1800072D0(MultiByteStr); if ( *a2 ) { v9 = sub_180007B10(lpLibFileName); *a1 = (HMODULE)v9; if ( v9 ) { sub_180007260(OutputString, "[S_API] SteamAPI_Init(): Loaded local '%s' OK.\n"); OutputDebugStringA(OutputString); } } if ( !*a1 ) { if ( !*a2 ) { phkResult = 0LL; v10 = (HKEY)sub_1800075F0("HKCU"); Data = 0; lpWideCharStr = (WCHAR *)operator new(0x46uLL); if ( !MultiByteToWideChar(0xFDE9u, 0, "Software\\Valve\\Steam\\ActiveProcess", -1, lpWideCharStr, 35) ) *lpWideCharStr = 0; v12 = RegOpenKeyExW(v10, lpWideCharStr, 0, 0x20219u, &phkResult); j_j_j__free_base(lpWideCharStr); if ( !v12 ) { cbData = 4; v13 = (WCHAR *)operator new(8uLL); if ( !MultiByteToWideChar(0xFDE9u, 0, "pid", -1, v13, 4) ) *v13 = 0; v12 = RegQueryValueExW(phkResult, v13, 0LL, Type, (LPBYTE)&Data, &cbData); j_j_j__free_base(v13); RegCloseKey(phkResult); } v14 = 0; if ( !v12 ) v14 = Data; v15 = OpenProcess(0x400u, 0, v14); ExitCode = 0; v16 = v15; if ( !v15 || (!GetExitCodeProcess(v15, &ExitCode) || ExitCode != 259 ? (v17 = 0) : (v17 = 1), CloseHandle(v16), !v17) ) { OutputDebugStringA("[S_API] SteamAPI_Init(): SteamAPI_IsSteamRunning() did not locate a running instance of Steam.\n"); } } if ( !v8 ) { OutputDebugStringA("[S_API] SteamAPI_Init(): Could not determine Steam client install directory.\n"); return 0LL; } v18 = -1LL; do ++v18; while ( MultiByteStr[v18] ); v19 = (WCHAR *)operator new(saturated_mul((int)v18 + 1LL, 2uLL)); if ( !MultiByteToWideChar(0xFDE9u, 0, MultiByteStr, -1, v19, v18 + 1) ) *v19 = 0; Library = LoadLibraryExW(v19, 0LL, 8u); j_j_j__free_base(v19); if ( !Library ) Library = LoadLibraryExA(MultiByteStr, 0LL, 8u); *a1 = Library; if ( !Library ) { sub_180007260(OutputString, "[S_API] SteamAPI_Init(): Sys_LoadModule failed to load: %s\n"); OutputDebugStringA(OutputString); return 0LL; } if ( *a2 ) { sub_180007260(OutputString, "[S_API] SteamAPI_Init(): Loaded '%s' OK. (First tried local '%s')\n"); OutputDebugStringA(OutputString); *a2 = 0; } else { sub_180007260(OutputString, "[S_API] SteamAPI_Init(): Loaded '%s' OK.\n"); OutputDebugStringA(OutputString); } } if ( *a1 ) { CreateInterface = GetProcAddress(*a1, "CreateInterface"); v22 = (__int64 (__fastcall *)(_QWORD, _QWORD))CreateInterface; if ( CreateInterface ) { qword_180028360 = ((__int64 (__fastcall *)(const char *, _QWORD))CreateInterface)("SteamClient017", 0LL); Steam_ReleaseThreadLocalMemory = (__int64)GetProcAddress(hLibModule, "Steam_ReleaseThreadLocalMemory"); result = v22(a4, 0LL); ++qword_1800283D0; return result; } } sub_180007260(OutputString, "[S_API FAIL] SteamAPI_Init() failed; unable to locate interface factory in %s.\n"); OutputDebugStringA(OutputString); if ( *a1 ) FreeLibrary(*a1); *a1 = 0LL; return 0LL; } ~~~ 里面果然调用了SteamAPI,分析如下 #### 读取注册表 sub_1800075F0根据传入字符串返回对应预定义键 ~~~c++ MACRO_HKEY __fastcall sub_1800075F0(char *String1) { if ( !stricmp(String1, "HKEY_LOCAL_MACHINE") || !stricmp(String1, "HKLM") ) return HKEY_LOCAL_MACHINE; if ( !stricmp(String1, "HKEY_CURRENT_USER") || !stricmp(String1, "HKCU") ) return HKEY_CURRENT_USER; if ( !stricmp(String1, "HKEY_CLASSES_ROOT") || !stricmp(String1, "HKCR") ) return HKEY_CLASSES_ROOT; return 0LL; } ~~~ 获取HKCU并使用RegOpenKeyExW打开"Software\\Valve\\Steam\\ActiveProcess"注册表项   接着RegQueryValueExW检索pid到Data,然后OpenProcess打开进程,如果进程仍然运行就会返回退出码0x103(STILL_ACTIVE)  进程还存在则v9=1,读取"HKCU\Software\\Valve\\Steam\\ActiveProcess"注册表项的SteamClientDll键,前面注册表截图我们知道是Steam安装根目录下的一个Dll路径 接着对提取路径的目录  sub_1800072D0如下 ~~~c++ char __fastcall sub_1800072D0(char *Destination, int a2) { size_t v3; // r15 HKEY v5; // rbp __int64 v6; // rdi WCHAR *lpWideCharStr; // rbx LSTATUS v8; // ebp bool v9; // zf BYTE *v10; // rax BYTE *v11; // r14 WCHAR *v12; // rbx LSTATUS v13; // ebp bool v14; // bp HMODULE ModuleHandleA; // rax int v16; // eax CHAR v17; // cl int v18; // edi __int64 i; // rcx char v20; // al HKEY phkResult; // [rsp+40h] [rbp-658h] BYREF WCHAR Filename[264]; // [rsp+50h] [rbp-648h] BYREF CHAR MultiByteStr[1040]; // [rsp+260h] [rbp-438h] BYREF DWORD cbData; // [rsp+6B0h] [rbp+18h] BYREF DWORD Type; // [rsp+6B8h] [rbp+20h] BYREF v3 = a2; if ( ::Destination[0] && !Destination ) return 1; memset(MultiByteStr, 0, sizeof(MultiByteStr)); v5 = (HKEY)sub_1800075F0("HKCU"); phkResult = 0LL; v6 = -1LL; lpWideCharStr = (WCHAR *)operator new(0x46uLL); if ( !MultiByteToWideChar(0xFDE9u, 0, "Software\\Valve\\Steam\\ActiveProcess", -1, lpWideCharStr, 35) ) *lpWideCharStr = 0; v8 = RegOpenKeyExW(v5, lpWideCharStr, 0, 0x20219u, &phkResult); j_j_j__free_base(lpWideCharStr); v9 = v8 == 0; if ( !v8 ) { v10 = (BYTE *)operator new(0x822uLL); cbData = 2080; v11 = v10; v12 = (WCHAR *)operator new(0x22uLL); if ( !MultiByteToWideChar(0xFDE9u, 0, "SteamClientDll64", -1, v12, 17) ) *v12 = 0; v13 = RegQueryValueExW(phkResult, v12, 0LL, &Type, v11, &cbData); j_j_j__free_base(v12); if ( !v13 ) { *(_WORD *)&v11[2 * ((unsigned __int64)cbData >> 1)] = 0; if ( !WideCharToMultiByte(0xFDE9u, 0, (LPCWCH)v11, -1, MultiByteStr, 1040, 0LL, 0LL) ) v13 = 122; } j_j_j__free_base(v11); RegCloseKey(phkResult); v9 = v13 == 0; } v14 = v9; if ( !MultiByteStr[0] ) { Filename[0] = 0; ModuleHandleA = GetModuleHandleA(lpLibFileName); if ( GetModuleFileNameW(ModuleHandleA, Filename, 0x104u) - 1 <= 0x102 ) { Filename[259] = 0; v16 = WideCharToMultiByte(0xFDE9u, 0, Filename, -1, MultiByteStr, 1040, 0LL, 0LL); v17 = MultiByteStr[0]; if ( !v16 ) v17 = 0; MultiByteStr[0] = v17; } } if ( Destination ) { strncpy(Destination, MultiByteStr, v3); Destination[v3 - 1] = 0; } strncpy(::Destination, MultiByteStr, 0x40FuLL); byte_18002835F = 0; do ++v6; while ( ::Destination[v6] ); v18 = v6 - 1; if ( v18 > 0 ) { for ( i = v18; i > 0; --i ) { v20 = ::Destination[i]; if ( v20 == '\\' ) break; if ( v20 == '/' ) break; --v18; } ::Destination[v18] = 0; } return v14; } ~~~ 同样先读取了"HKCU\Software\\Valve\\Steam\\ActiveProcess"的SteamClientDll64,如果存在就提取值,不存在就会GetModuleHandleA获取'steamclient64.dll'的句柄,然后获取目录名 #### 加载DLL LoadLibraryExA调用前面的steamclient64.dll,然后GetProcAddress获取了CreateInterface,然后这个函数传入了"SteamClient017", 0两个参数,获取了一些东西 再次调用sub_1800064E0检查steamclient64.dll防篡改 调用steamclient64_Steam_CreateSteamPipe,调试的时候可以发现在这里返回0进入if并返回0x35,然后F9就会出现Steam启动  构造启动字符串`steam://run/xxxxxx//` ~~~c++ bool __fastcall sub_180004D60(BYTE *lpFileName, unsigned int a2) { unsigned __int64 v2; // rsi LSTATUS v4; // ebx __int64 v5; // rdx DWORD cbData; // [rsp+60h] [rbp+18h] BYREF HKEY hKey; // [rsp+68h] [rbp+20h] BYREF v2 = a2; hKey = 0LL; cbData = 0; if ( RegOpenKeyExA(HKEY_LOCAL_MACHINE, "Software\\Valve\\Steam", 0, 0x201u, &hKey) ) return 0; cbData = 2 * v2; v4 = RegQueryValueExW(hKey, L"InstallPath", 0LL, 0LL, lpFileName, &cbData); RegCloseKey(hKey); if ( v4 || cbData < 2 || cbData >> 1 >= (unsigned int)v2 ) return 0; v5 = -1LL; *(_WORD *)&lpFileName[2 * ((unsigned __int64)cbData >> 1)] = 0; do ++v5; while ( *(_WORD *)&lpFileName[2 * v5] ); if ( v5 && *(_WORD *)&lpFileName[2 * v5 - 2] == 92 ) *(_WORD *)&lpFileName[2 * v5-- - 2] = 0; if ( v5 + 11 > v2 ) return 0; *(_OWORD *)&lpFileName[2 * v5] = xmmword_180018798; *(_DWORD *)&lpFileName[2 * v5 + 16] = 6619256; *(_WORD *)&lpFileName[2 * v5 + 20] = 0; return GetFileAttributesW((LPCWSTR)lpFileName) != -1; } ~~~ sub_180004D60读取注册表获取steam目录("HKEY_LOCAL_MACHINE\Software\\Valve\\Steam\InstallPath"),然后拼接`\\steam.exe`,接着调用GetFileAttributesW判断文件是否存在 判断完就会调用ShellExecuteW执行 `xxx\\steam.exe steam://run/xxxxxx//`,查询可知在启动Steam并尝试运行AppID为xxx的游戏(AppID对应a2+56,也就是qword7.LODWORD),他会去找SteamLibrary\steamapps目录下对应的`appmanifest_appid.acf` ### AES解密 如果我们修改sub_180006680返回值为48就不会执行shell命令,而是进入修改代码的if块中,这里读取的是v2+60(对应的是qword7.HIDWORD,检测调试器标志位)  v2+80(qword10.LODWORD)提取出了一个长度数值,然后+32并分配内存,很像补齐的一个操作 v2+16是ImageBase地址值,v2+120、v2+136提取出了32字节(qword15-18),v2+72(qword9)是0x1000和ImageBase相加得到的正好是PDS-Win64-Shipping.exe的.text段 来看sub_180005240函数,调试可以知道a1是ImageBase ~~~c++ __int64 __fastcall sub_180005240(IMAGE_DOS_HEADER *a1, __int64 a2, __int64 a3) { __int64 result; // rax __int64 v7; // r8 unsigned int v8; // edx _DWORD *v9; // r9 unsigned __int64 v10; // r8 __int64 v11; // r11 __int64 v12; // rdx char *v13; // rdi unsigned int v14; // ebx _WORD *v15; // r11 int v16; // eax unsigned __int16 v17; // cx unsigned __int64 v18; // rdx unsigned __int64 v19; // r8 unsigned __int64 v20; // rdx unsigned __int64 v21; // r8 result = a1->e_lfanew; v7 = *(unsigned int *)((char *)&a1[2].e_res2[4] + result); if ( (_DWORD)v7 ) { if ( *(_DWORD *)((char *)&a1[2].e_res2[6] + result) ) { v8 = *(_DWORD *)((char *)&a1->e_magic + v7); v9 = (_DWORD *)((char *)&a1->e_magic + v7); if ( v8 ) { do { v10 = *(_QWORD *)(a3 + 72); v11 = v8; if ( v8 >= v10 + *(unsigned int *)(a3 + 80) || v8 + 4095 < v10 ) { LODWORD(v12) = v9[1]; } else { v12 = (unsigned int)v9[1]; v13 = (char *)a1 + v11; v14 = 0; v15 = v9 + 2; if ( ((v12 - 8) & 0xFFFFFFFFFFFFFFFEuLL) != 0 ) { do { v16 = (unsigned __int16)*v15 >> 12; v17 = *v15 & 0xFFF; if ( v16 ) { if ( v16 == 3 ) { v20 = *(_QWORD *)(a3 + 72); v21 = *v9 + (unsigned int)v17; if ( v21 < v20 + *(unsigned int *)(a3 + 80) && v21 >= v20 ) *(_DWORD *)&v13[v17] += a2; } else if ( v16 == 10 ) { v18 = *(_QWORD *)(a3 + 72); v19 = *v9 + (unsigned int)v17; if ( v19 < v18 + *(unsigned int *)(a3 + 80) && v19 >= v18 ) *(_QWORD *)&v13[v17] += a2; } else { __debugbreak(); } } v12 = (unsigned int)v9[1]; ++v14; ++v15; } while ( v14 < (unsigned __int64)(v12 - 8) >> 1 ); } } result = (unsigned int)v12; v9 = (_DWORD *)((char *)v9 + (unsigned int)v12); v8 = *v9; } while ( *v9 ); } } } return result; } ~~~ 分析可知这个函数是**在针对主代码段进行重定位修正** sub_180004200函数分析发现AES解密    调试来到sub_180004200看传入参数,发现rcx(a1)存储了v2+120(v33)开始的32字节+text段代码(长度为qword11.LODWORD,memmove实现了拷贝到v33开始32字节后面),rdx(a2)存储了0x1F8000应该是text段数据长度,r8(a3)存储了v2+88开始的值,r9(a4)应该是长度0x20,a6存储了.text段数据,a7为0x190 分析可知sub_1800084E0是expand_key,v32是轮密钥;sub_180001A10是aes解密函数,首先对传入的a1做解密,解密得到结果为16字节的iv,然后从a1+16开始循环做AES CBC解密  没有魔改AES,老外搞对抗还是太友好了。调试跳过函数,此时去text段发现已经可以反编译了  ## DRMP结构体分析 到这里我们基本分析完全部逻辑,现在尝试还原DRMP那段数据,可以将其视为一个结构体(里面有magic、key、iv、地址等各种数据),结合前面分析的qword0、qword1...以及dll里v2+xxx等还原出结构体如下 ~~~c++ struct DRMP { unsigned int XOR_key; unsigned int Magic; // 0xC0DEC0DF long long rva; // 64位base?0x140000000,但疑似没有用到 long long ImageBase_Offset; // start-ImageBase_Offset指向ImageBase unsigned int BindData_Offset; // start-BindData_Offset指向一段加密数据(.bind) unsigned int Code_size; long long text_offset; // 解密完的text段的偏移 unsigned int APIString_offset; // API字符串长度 unsigned int APIString_len; // start-BindData_Offset+APIString_offset指向API字符串加密后的数据 unsigned int DLL_offset; // 经过XTEA加密后的DLL数据偏移(相对ImageBase) unsigned int DLL_len; // DLL数据长度 unsigned int AppID; // 待启动游戏的AppID unsigned int NoCheckDebugger; // 不检查调试器 unsigned int unknown; // 没用上 unsigned int CRC; long long real_text_offset; // .text段偏移(相对ImageBase) unsigned int text_len; // .text段长度 unsigned int unknown1; // 虽然读取了但是没加入任何计算 byte AES_key[32]; // AES密钥 byte AES_iv[16]; // 加密后的AES iv byte cipher[16]; unsigned int XTEA_key[4]; byte zeros[32]; long long GetModuleHandleA_RVA; // 相对前面API字符串加密后的数据的偏移 long long GetProcAddress_RVA; long long LoadLibraryA_RVA; long long LoadLibraryW_RVA; long long GetProcAddress1_RVA; }; ~~~ 到这里就可以结束了,后面就是实现脚本自动化解密text段,涉及到版权保护不再放出来,这里只做分析 放一张破解DRM还原游戏主逻辑的截图  最后修改:2026 年 04 月 09 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏