欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]某游戏皮肤修改工具的逆向与实现

[原创]某游戏皮肤修改工具的逆向与实现

        某游戏也就是国内目前很火的竞技游戏英雄联盟了,前不久在网上发现到了一个本地修改皮肤的辅助工具,话说作者以前一直以为这种辅助工具是通过修改本地皮肤文件来实现的,想来没什么技术含量,需要的时候找到那个文件修改就行了。这次简单的调试了一下,发现该工具是通过修改游戏内存实现的,于是觉得有意思,就有了本文。(作者技术以及表达能力都很差,请大家见谅,有问题可以短信我或者邮件我)
        工具没加壳,比较简单,本文主要找到其思路,然后自己写代码实现(为了保护工具以及游戏,本文仅提供辅助工具部分反汇编代码和作者自己按照其思路写的文件以及部分源码,不提供该工具名称、文件以及作者自己写的完整源代码,你可以根据此文逆向作者的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

上传的附件 01.png
02.png
03.png
04.png
05.png
06.png
07.png
001.png
002.png
003.png
004.png
005.png
006.png
007.png
008.png
009.png
010.png
011.png
012.png
013.png
014.png
1111.jpg
2222.jpg
allpic.rar
bin.rar

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途。
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功