[原创]某游戏皮肤修改工具的逆向与实现
某游戏也就是国内目前很火的竞技游戏英雄联盟了,前不久在网上发现到了一个本地修改皮肤的辅助工具,话说作者以前一直以为这种辅助工具是通过修改本地皮肤文件来实现的,想来没什么技术含量,需要的时候找到那个文件修改就行了。这次简单的调试了一下,发现该工具是通过修改游戏内存实现的,于是觉得有意思,就有了本文。(作者技术以及表达能力都很差,请大家见谅,有问题可以短信我或者邮件我)
工具没加壳,比较简单,本文主要找到其思路,然后自己写代码实现(为了保护工具以及游戏,本文仅提供辅助工具部分反汇编代码和作者自己按照其思路写的文件以及部分源码,不提供该工具名称、文件以及作者自己写的完整源代码,你可以根据此文逆向作者的dll,然后自己实现)
首先分析辅助的文件的作用,辅助修改游戏内存,大部分就两种,ReadProcessMemory等API和注入dll,TX的游戏有TP保护的,所以ReadProcessMemory这种方法肯定行不通。推测辅助是注入dll到游戏的,so在打开工具的情况下打开游戏,使用XT查看游戏相关进程模块,发现游戏进程“League of Legends.exe”(以后称其为游戏进程)注入了Skin.dll。而游戏启动进程” lol.launcher_tencent.exe”(以后称其为启动进程)注入了Mon.dll,同时辅助工具也加载了Mon.dll,修改皮肤时会创建一个Skin.cfg配置文件,里面记录了修改的信息,应该是某个dll会根据里面的信息来修改的。所以我们重点调试Mon.dll和Skin.dll。(逆向过程不是很好表述,所以作者是通过截图+文字来表述的,有些流程是一些简单的字符串处理流程和条件判断流程,为了节省篇幅,都会略过)。
一:Mon.dll的调试
定位到dll入口处,







部分源码
CreateProcessAHook代码
BOOL WINAPI CreateProcessAHook( __in LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
__in LPSECURITY_ATTRIBUTES lpProcessAttributes,
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in BOOL bInheritHandles,
__in DWORD dwCreationFlags,
__in LPVOID lpEnvironment,
__in LPCTSTR lpCurrentDirectory,
__in LPSTARTUPINFO lpStartupInfo,
__out LPPROCESS_INFORMATION lpProcessInformation)
{
//修改输入参数,调用原函数
int ret=OLD_CreateProcessA(lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags | CREATE_SUSPENDED,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation);
GetModuleFileName(g_hModule,g_szPath,MAX_PATH);
memset(g_bWriteCode,0,1000);
CONTEXT ThreadContext;
ThreadContext.ContextFlags = CONTEXT_FULL;
GetThreadContext(lpProcessInformation->hThread,&ThreadContext);
LPVOID lpAddress = VirtualAllocEx(lpProcessInformation->hProcess,NULL,1000,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
if (!lpAddress)
{
DebugPrintf("VirtualAllocEx Error %d",GetLastError());
return ret;
}
BYTE szBuffer[1000];
BYTE *bP;
RtlZeroMemory(szBuffer,1000);
szBuffer[0] = 0x9C;
szBuffer[1] = 0x60;
szBuffer[2] = 0xE8;
*(DWORD*)(szBuffer+3) = 0x104;
RtlCopyMemory(szBuffer + 7,g_szPath,MAX_PATH);
bP = &szBuffer[0x104+7];
*bP = 0xB8;
*(DWORD*)(bP+1) = (DWORD)LoadLibraryA;
*(bP+5) = 0xFF;
*(bP+6) = 0xD0;
*(bP+7) = 0x61;
*(bP+8) = 0x9d;
*(bP+9) = 0x68;
*(DWORD*)(bP+10) = ThreadContext.Eip;
*(bP+14) = 0xc3;
DebugPrintf("lpAddress = %x",lpAddress);
if(!WriteProcessMemory(lpProcessInformation->hProcess, lpAddress, szBuffer, 1000, NULL))
{
DebugPrintf("WriteProcessMemory Error,%d",GetLastError());
return ret;
}
FlushInstructionCache(lpProcessInformation->hProcess, lpAddress, 1000);
ThreadContext.Eip = (DWORD)lpAddress;
SetThreadContext(lpProcessInformation->hThread, &ThreadContext);
ResumeThread(lpProcessInformation->hThread);
return ret;
} StartMon线程处源码
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
DWORD dwPid = 0,dwThreadId = 0;
while (1)
{
Sleep(1000);
if(bIsWin64)
{
if(CheckProcess(&dwPid,szGameExeName))
{
continue;
}
while (!CheckProcess(&dwPid,szGameExeName))
{
Sleep(100);
}
if(CheckModule( dwPid))
{
DebugPrintf("Find continue");
continue;
}
if(GetProThread(dwPid,&dwThreadId))
{
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPid);
if (!hProcess)
{
DebugPrintf("OpenProcessError");
continue;
}
HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS,FALSE,dwThreadId);
if (!hThread)
{
DebugPrintf("OpenThread Error");
continue;
}
SuspendThread(hThread);
CONTEXT ThreadContext;
ThreadContext.ContextFlags = CONTEXT_FULL;
GetThreadContext(hThread,&ThreadContext);
LPVOID lpAddress = VirtualAllocEx(hProcess,NULL,1000,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
if (!lpAddress)
{
DebugPrintf("VirtualAllocEx Error %d",GetLastError());
CloseHandle(hThread);
CloseHandle(hProcess);
continue;
}
BYTE szBuffer[1000];
BYTE *bP;
RtlZeroMemory(szBuffer,1000);
szBuffer[0] = 0x9C;
szBuffer[1] = 0x60;
szBuffer[2] = 0xE8;
*(DWORD*)(szBuffer+3) = 0x104;
RtlCopyMemory(szBuffer + 7,g_szSkinDllPath,MAX_PATH);
bP = &szBuffer[0x104+7];
*bP = 0xB8;
*(DWORD*)(bP+1) = (DWORD)LoadLibraryA;
*(bP+5) = 0xFF;
*(bP+6) = 0xD0;
*(bP+7) = 0x61;
*(bP+8) = 0x9d;
*(bP+9) = 0x68;
*(DWORD*)(bP+10) = ThreadContext.Eip;
*(bP+14) = 0xc3;
DebugPrintf("lpAddress = %x",lpAddress);
WriteProcessMemory(hProcess, lpAddress, szBuffer, 1000, NULL);
FlushInstructionCache(hProcess, lpAddress, 1000);
ThreadContext.Eip = (DWORD)lpAddress;
SetThreadContext(hThread, &ThreadContext);
ResumeThread(hThread);
CloseHandle(hThread);
CloseHandle(hProcess);
Sleep(1000);
}
}
else
{
if(CheckProcess(&dwPid,szTentcentExeName))
{
CreateRemoteDll(g_szMonDllPath,dwPid);
}
}
}
return 1;
}
总结:Mon.dll的作用就是
判断当前系统是32位还是64位,如果是32位,则使用CreateRemoteThread注入自己模块(mon.dll)到游戏启动进程,同时HOOK 游戏启动进程的CreateProcess 当游戏启动进程启动游戏时加载模块。
如果是64位,则直接判断游戏进程是否启动,如果启动了,直接暂停游戏进程,然后修改EIP注入dll
值得注意的是,32位系统是先注入mon.dll到游戏启动进程,然后CreateProcess时注入skin.dll到游戏进程,而64位系统是直接注入skin.dll到游戏进程。
二:Skin.dll的逆向














Skin.dll实现代码
// dllmain.cpp : 定义 DLL 应用程序的入口点。
//请注意:我删除了sub_100010B0函数实现代码(通过特征码得到函数地址,类似于OD搜索二进制功能),感兴趣的可以自己去实现。
#include "stdafx.h"
#include "include/detours.h"
#include "include/detver.h"
#pragma comment(lib,"lib.X86\\detours.lib")
char *szMatchCode[] = {"E8????????B0018B4C242864890D00000000595E5B8BE55DC21400|1|2",
"6A006A016A03E8????????83C4108D4317|7|1"};//只有两段特征码和修改皮肤有关,so我只保留了2段。我自己的函数通配符是AA
void CDECL DebugPrintf(TCHAR * szFormat, ...){
TCHAR szBuffer [1024] ;
va_list pArgList ;
// The va_start macro (defined in STDARG.H) is usually equivalent to:
// pArgList = (char *) &szFormat + sizeof (szFormat) ;
va_start (pArgList, szFormat) ;
// The last argument to wvsprintf points to the arguments
vsnprintf (szBuffer, sizeof (szBuffer) / sizeof (TCHAR),
szFormat, pArgList) ;
// The va_end macro just zeroes out pArgList for no good reason
va_end (pArgList) ;
OutputDebugString(szBuffer);
}
static HANDLE (WINAPI* OLD_CreateMutexA)(
__in LPSECURITY_ATTRIBUTES lpMutexAttributes,
__in BOOL bInitialOwner,
__in LPCTSTR lpName)=CreateMutexA;
DWORD dw_Pid = 0;
ULONG InterestAddr[2] = {0};
BOOL MatchGetAddr()
{
DWORD dwOffect = 0;
int dType = 0;
char szMatchItem[MAX_PATH];
ULONG uTmpAddr;
int index= 0;
for(int i = 0;i<2;i++)
{
index =0;
sscanf(szMatchCode[i], "%[^|]|%x|%d", szMatchItem, &dwOffect, &dType);
uTmpAddr = sub_100010B0(0,(char*)0x00401000,0x2000000,szMatchItem);
DebugPrintf("Match Code %s dwOffect %x dType %d Result = %08x",szMatchCode[i],dwOffect,dType,uTmpAddr);
if(uTmpAddr == 0) continue;
if (dType == 1)
{
InterestAddr[i] = uTmpAddr + dwOffect;
}
if (dType == 2)
{
InterestAddr[i] = (uTmpAddr + dwOffect + *(DWORD *)(uTmpAddr + dwOffect) + 4);
}
}
DebugPrintf("InterestAddr = %08x %08x",InterestAddr[0],InterestAddr[1]);
return 1;
};
DWORD SAVEOLD1;
WORD SAVEOLD2;
ULONG uRetAddr;
DWORD MySeat;
int SkinReplace(int par1,int par2,void* lp)
{
if ( lp && !IsBadReadPtr(lp, 1u) )
{
if ( *(BYTE *)lp )
{
if ( *(BYTE *)lp == 0x65 )
{
if ( par1 >= 0x12 )
{
DebugPrintf("ReplaceSkin bType = 0, dwId = %x\n", *(DWORD *)((char *)lp + 1));
if ( dw_Pid == *(DWORD *)((char *)lp + 1) )
{
if ( lp != (void *)-17 )
{
DebugPrintf("My::ReplaceSkin bType = 0x65, dwFilter = 0x%X, SkinName = %s\n", par1, (char *)lp + 17);
if ( !*(DWORD *)((char *)lp + 9) )
{
*(DWORD*)((char *)lp + 9) = 1;//全部换为第一个皮肤
return 1;
}
}
}
}
}
}
else // 这都无从证实。全部和他写的一样就行了。
{
if ( par1 >= 0xC )
{
if ( dw_Pid == *((DWORD *)lp + 2) )
MySeat = *((DWORD *)lp + 1);
DebugPrintf(
"My::ReplaceSkin bType = 0, dwFilter = 0x%X, g_dwCamp = 0x%X, g_dwCamp+4 = 0x%X\n",
par1,
MySeat,
*((DWORD *)lp + 2));
}
}
}
return 1;
//#第一个HOOK处理完毕
}
//照抄。。。
int _stdcall HOOK00_1(int par1, void *lp, int flag, int par2)
{
signed int result; // eax@3
if ( flag == 3 && par1 )
result = SkinReplace(par1, par2, lp);
else
result = 1;
return result;
}
DWORD OriCallMachineCode = 0;
DWORD OriCallAddr = 0;
void _stdcall HOOKHERO(DWORD _ebx)
{
DWORD mayheroname; // esi@1
int GameSeat; // edi@2
mayheroname = _ebx + 0x97;
if ( _ebx != 0xFFFFFF69 )
{
GameSeat = *(DWORD *)(_ebx + 9);
DebugPrintf("HOOKHERO dwCamp=%x,g_dwCamp=%x", *(DWORD *)(_ebx + 9), MySeat);
if ( GameSeat == MySeat )
{
DebugPrintf("HOOKHERO ReplaceSkin bType = 0x48, Camp = 0x%X, pszSkinName = %p, SkinName = %s\n",
GameSeat,
mayheroname,
mayheroname);
if ( !*(DWORD *)(_ebx + 0x13))
{
*(DWORD*)(_ebx + 0x13) = 1;//换成第一个皮肤
DWORD flOldProtect;
if ( VirtualProtect((PVOID)InterestAddr[1], 4, PAGE_EXECUTE_READWRITE, &flOldProtect) )
{ // 这里只hook一次。。。应该是初始化游戏时的一次。jmphook 就是游戏每次显示的皮肤。也许
*(DWORD *)InterestAddr[1] = OriCallMachineCode;
VirtualProtect((PVOID)InterestAddr[1], 4, flOldProtect, &flOldProtect);
DebugPrintf("Unhook success!");
}
else
{
DebugPrintf("Unhook failed!");
}
}
}
}
}
_declspec(naked) void HookCall11()
{
__asm
{
pushad
pushfd
push ebx
call HOOKHERO
popfd
popad
push OriCallAddr
retn
}
}
_declspec(naked) void HOOK00()
{
__asm {
pushad
pushfd
mov eax, [ebp+10h]
push eax
mov eax, [ebp+0Ch]
push eax
mov eax, [ebp+18h]
push eax
mov eax, [ebp+14h]
push eax
call HOOK00_1
test eax, eax
jz RET1
popfd
popad
MOV EAX, DWORD PTR FS:[0] //原指令
jmp uRetAddr
RET1:
popfd
popad
mov esp, ebp
pop ebp
retn 14h
}
}
void Hook()
{
ULONG uHookAddr = InterestAddr[0];
HANDLE handle=GetCurrentProcess();
char MyJMP[5]={0};
MyJMP[0]=(char)0xe9;
ULONG uTempAddr=(ULONG)MyJMP;
uRetAddr = uHookAddr + 6;
ULONG uSkillJmp=(ULONG)HOOK00-uHookAddr-5;
__asm
{
mov eax,uSkillJmp
mov ebx, uTempAddr
add ebx ,1
mov [ebx],eax
mov ecx,[ebx]
}
WriteProcessMemory(handle,(LPVOID)(uHookAddr),(LPVOID)MyJMP,5,NULL);
}
HANDLE WINAPI NEW_CreateMutexA(
__in LPSECURITY_ATTRIBUTES lpMutexAttributes,
__in BOOL bInitialOwner,
__in LPCTSTR lpName)
{
//修改输入参数,调用原函数
DebugPrintf("Enter MutexHook");
HANDLE ret=OLD_CreateMutexA(lpMutexAttributes,
bInitialOwner,
lpName);
if(!InterestAddr[0])//Mark
{
MatchGetAddr();//得到特征码对应的地址
if(InterestAddr[0]){
InterestAddr[0] = InterestAddr[0]+13;//jmp HOOK的地址
SAVEOLD1 = *(DWORD*)InterestAddr[0];
SAVEOLD2 = *(WORD*)(InterestAddr[0]+4);//这里保存之前的6个字节。不保存也行。。。反正又不用恢复HOOK
Hook();//HOOK InterestAddr[0]这个地址
OriCallMachineCode = *(DWORD *)InterestAddr[1];
OriCallAddr = (DWORD)((char *)InterestAddr[1] + *(DWORD *)InterestAddr[1] + 4);
DWORD flOldProtect;
if ( VirtualProtect((PVOID)InterestAddr[1], 4, PAGE_EXECUTE_READWRITE, &flOldProtect) )
{
*(DWORD *)InterestAddr[1] = (DWORD)((char *)HookCall11- InterestAddr[1] - 4);
VirtualProtect((PVOID)InterestAddr[1], 4, flOldProtect, &flOldProtect);// 修改call
DebugPrintf("Hook success!");
}
else
{
DebugPrintf("Hook failed!");
}
//Detours恢复HOOK
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)OLD_CreateMutexA,NEW_CreateMutexA);
DetourTransactionCommit();
}
}
return ret;
}
BOOL HookMutex()
{
DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)OLD_CreateMutexA,NEW_CreateMutexA);
DetourTransactionCommit();
return TRUE;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
char * szCommand;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hModule);
DebugPrintf("Dll 已加载");
szCommand = GetCommandLine();
for (int i = strlen(szCommand)-1;i>0;i--)
{
if ( *(szCommand+i) == '=' &&
*(szCommand+i-1) == '=')
{
DebugPrintf("Find == %s",szCommand+i+1);
dw_Pid = strtoul(szCommand+i+1,0,0);
DebugPrintf("dw_Pid = %x",dw_Pid);
break;
}
}
HookMutex();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
PS:IDA内部分注释是之前分析时猜测的,有很多错误。图片内容有一些是矛盾的,以为图片也是分析一部分做一张的,我这里解释下一些明显的错误。首先图片中有些Detours开发包写成了Detous开发包。Skin.dll中注释内容我写成了进程pid,其实不是进程pid,是游戏角色相关信息。HOOK API那些函数重命名全是错误的,具体可以参考Detours开发包中的函数。LOG信息中重复调用CreateMutex的原因是TX 代理的LOL是加过壳的,代码初始是加密的,只有解密完成才可以搜索到特征码。逆向是一个学习过程,软件原作者一些思路是值得学习的(对我这个菜鸟来说值得学习)。比如SuspendThread然后注入dll;通过HOOK CreateMutex然后搜索特征码定位地址。
我发的工具在win7 64位系统上测试成功,其他系统没经过测试。源码我只给部分是防止有些人使用不稳定的源码然后加一些病毒什么的传播到网上去。关键代码已经给出,有心人可以自己实现未给出的代码。
发两张成功图:


bin:
bin.rar
全部图片:
allpic.rar
