我承認標得下得太過火。
這裡所謂的 kernel ,指的是拿掉 GUI 部份,關鍵動作之執行,筆者只試用過免費版的 GM8,所以只針對單機原理做簡易說明。至於是不是有方法可以破神盾,筆者沒研究,因摸這塊純粹是因為興趣,但筆者興趣並不包含電腦遊戲 ( 會玩電腦遊戲純粹是了解流程、開發程式而玩 )。
由於使用之 Win32 API 不少,不逐一說明,附上 MSDN 連結,有興趣請前往閱讀。
若搜尋到此篇文章,歡迎任何技術面之探討,筆者不願 blog 開始出現「索取文」之出現。
About Game Master
Game Master 這套軟體相信很多人都用過,在玩單機遊戲時是常拿來做數值屬性修改之軟體,本文只針對這部份做介紹。
Game Master 筆者認為最麻煩的事情是,裡面沒再弄一個簡單的 script 出來,這對需要重覆步驟之搜尋而言並非好事,同時可注意到,它最多只會搜尋到 2500 筆資料 ( 應該是定義死了 ) 。
由於需要一款遊戲當 demo,為方便說明,這裡設計一個很簡單的遊戲,先建立 int arr1[2],再 allocate int arr2[2],當輸入 4 個數字符合 arr1[0], arr1[1], arr2[0], arr2[1] 時,則顯示猜中,否則繼續猜下去。
假定這執行檔就叫 game.exe,且多加了一個東西:輸出 arr1[0:1] 與 arr2[0:1] 之 address。
#include <stdio.h> #include <stdlib.h> #include <time.h> #define GAME_LOOP 3 int main() { int arr1[2]; int *arr2 = (int*)malloc(2 * sizeof(int)); int i, guess_cnt=0; int g1, g2, g3, g4; srand((unsigned)time(NULL)); for(i=0; i<GAME_LOOP; ++i){ arr1[0]=rand()%10, arr1[1]=rand()%10; arr2[0]=rand()%10, arr2[1]=rand()%10; printf("< ---game-%d start--->\n", i); printf("%p%p%p%p\n",&arr1[0], &arr1[1], &arr2[0], &arr2[1]); // printf("%d %d %d %d\n", arr1[0], arr1[1], arr2[0], arr2[1]); do{ ++guess_cnt; printf("<%d> input 4 number :", guess_cnt); scanf("%d%d%d%d",&g1,&g2,&g3,&g4); }while (g1!=arr1[0] || g2!=arr1[1] || g3!=arr2[0] || g4!=arr2[1]); printf("get wrong answer for loop %d !!\n\n", i); } free(arr2); return 0; }
掛住程序(Process)
不論是要搜尋、讀取、還是寫入,第一步驟都進行 OpenProcess,但一般較多都是從 Window name 過來,於是要先有一小段 code 將 hwnd 轉換成 hprocess 出來。
const char *title = "D:\\Setting\\xp_desktop2\\mytest\\game.exe"; HWND hwnd = FindWindow(NULL, title); DWORD proc_id; HANDLE hProcess; int a, b, c, d; if(hwnd==NULL) { printf("can't find %s\n", title); getchar(); return 1; } // Open Process GetWindowThreadProcessId(hwnd, &proc_id); hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, proc_id); if(!hProcess) { printf("open process fail at code : %d.\n", GetLastError()); getchar(); return 1; }
結束時,記得要 CloseHandle(hProcess)。
打不開?
VC 在開發時,以 IDE compile、link、execute 正常應都可執行。但一旦拿出來之後常發生 OpenProcess 失敗之情形,調用 GetLastError(),大多得到的是 5 。原因在於 OpenProcess 此函式之調用必須有一定之權限,故必須以程式碼把該權限提昇。
Win32 API 裡做這件事之函式為 OpenProcessToken,至於要查有沒有提昇成功的話,就用 LookupPrivilegeValue 去做,最後更改完權限時,還必須再用 AdjustTokenPrivileges 告知系統已提昇權限。
BOOL RaisePrivilege() { HANDLE hToken; TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if(OpenProcessToken(GetCurrentProcess(),TOKEN_ALL_ACCESS,&hToken)) if(LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&tp.Privileges[0].Luid)) AdjustTokenPrivileges(hToken,FALSE,&tp,0,NULL,0); if(hToken) {// success CloseHandle(hToken); return TRUE; } else { printf("last error : %d\n", GetLastError()); return FALSE; } }
讀取
為先簡化問題,在 game.exe 裡我們已先輸出 address,假設我們已拿到這四個 address 為 0012FF5C 0012FF60 00392A10 00392A14,且我們已知道其資料型態為 int,假設 4bytes,要對其做讀取時,用到的是 ReadProcessMemory API,如下參考。
#include <windows.h> #include <stdio.h> #include <stdlib.h> BOOL RaisePrivilege() { HANDLE hToken; TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if(OpenProcessToken(GetCurrentProcess(),TOKEN_ALL_ACCESS,&hToken)) if(LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&tp.Privileges[0].Luid)) AdjustTokenPrivileges(hToken,FALSE,&tp,0,NULL,0); if(hToken) {// success CloseHandle(hToken); return TRUE; } else { printf("last error : %d\n", GetLastError()); return FALSE; } } int main() { const char *title = "D:\\Setting\\xp_desktop2\\mytest\\game.exe"; HWND hwnd = FindWindow(NULL, title); DWORD proc_id; HANDLE hProcess; int a, b, c, d; if(hwnd==NULL) { printf("can't find %s\n", title); getchar(); return 1; } // raise privilege RaisePrivilege(); // Open Process GetWindowThreadProcessId(hwnd, &proc_id); hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, proc_id); if(!hProcess) { printf("open process fail at code : %d.\n", GetLastError()); getchar(); return 1; } // // read process memory // ReadProcessMemory(hProcess, (LPCVOID)(0x0012FF5C), (LPVOID)&a, sizeof(int), NULL ); ReadProcessMemory(hProcess, (LPCVOID)(0x0012FF60), (LPVOID)&b, sizeof(int), NULL ); ReadProcessMemory(hProcess, (LPCVOID)(0x00392A10), (LPVOID)&c, sizeof(int), NULL ); ReadProcessMemory(hProcess, (LPCVOID)(0x00392A14), (LPVOID)&d, sizeof(int), NULL ); printf("%d%d%d%d",a,b,c,d); // // close handle // CloseHandle(hProcess); getchar(); return 0; }
第五個參數蠻多人不知道什麼意思,它代表實際讀入之 byte 數 (若我沒意會錯 msdn 的話應是此意)。
拿出來的答案,再拿去輸入 "game.exe" 即可,發現每次都百發百中 ( 這試驗還真無聊 )。
寫入
剛剛我們的方式是從 game.exe 裡面讀 4 個值出來,再把讀出來的值輸入 game.exe 裡面去。
這次我們直接將裡面產生的 4 個數值重新改成 0 0 0 0,如此一來直接在 game.exe 裡面只要輸入4個0就過了。
這次用到的 API 也很簡單,WriteMemoryProcess ,且原始碼只改 5 行而已。
// // write process memory // If the function succeeds, the return value is nonzero. // If the function fails, the return value is 0 (zero). // To get extended error information, call GetLastError. // a=b=c=d=0; WriteProcessMemory(hProcess, (LPVOID)(0x0012FF5C), (LPCVOID)&a, sizeof(int), NULL ); WriteProcessMemory(hProcess, (LPVOID)(0x0012FF60), (LPCVOID)&b, sizeof(int), NULL ); WriteProcessMemory(hProcess, (LPVOID)(0x00392A10), (LPCVOID)&c, sizeof(int), NULL ); WriteProcessMemory(hProcess, (LPVOID)(0x00392A14), (LPCVOID)&d, sizeof(int), NULL );
搜尋
上述之方法簡化了最難的一個步驟:搜尋。事實上要寫得完整、寫得好,勢必將 Memory Management API 與其管理機制熟讀,甚至可能要讀 Windows 系列叢書 ( 像 windows internal 、windows kernel 之類的,要寫出完整的 Game Master,需要的也不是這篇文章,而是一本又一本的好書),同時也不代表任何 process 都可以使用此法搜尋到 < 說不定 process 還有其他的保護機制,如某 page 本來是 ReadWrite,改成 NoAccess >,建議先 K 過下面幾份函式 ( 結構體 )。
(1) memory search api
SYSTEM_INFO < struct > : 存放 processor , memory information 之簡易資訊,主要拿最小定址與最大定址做參考。 [ winbase.h -> windows.h ]
MEMORY_BASIC_INFORMATION < struct > : 取得 allocate memory 、page 之 basement, 與其保護機制 (唯讀/可讀可寫等)。[winnt.h -> windows.h ]
VirtualQueryEx : 取得 process memory block 狀態。[windows.h / kernel32.lib]
另必須熟讀 MEMORY_BASIC_INFORMATION 裡 參數 DWORD AllicationProtect 之 Memory Protection Constant 代表意義。
(2) other uility api
StrFormatByteSize : 將一數值,依適當之 K, M, G, 轉成字串,主要拿來表示 bytes 數。這 API 似乎只有 MSVC 支援 (gcc 沒編譯過),但應不難自己寫一份。 [Shlwapi.h / Shlwapi.lib]
(3) memory_list
由於機制甚為龐大,筆者必須強調,下面只是一份 sample code,簡化了不少情況。
#include <windows.h> #include <stdio.h> #include <windows.h> #include <Shlwapi.h> #pragma comment(lib, "Shlwapi.lib") void ShowProtection(DWORD dwTarget) { if((dwTarget & PAGE_READONLY) == PAGE_READONLY) printf(", ReadOnly"); if((dwTarget & PAGE_GUARD) == PAGE_GUARD) printf(", Guard"); if((dwTarget & PAGE_NOCACHE) == PAGE_NOCACHE) printf(", NoChange"); if((dwTarget & PAGE_READWRITE) == PAGE_READWRITE) printf(", ReadWrite"); if((dwTarget & PAGE_EXECUTE) == PAGE_EXECUTE) printf(", Execute"); if((dwTarget & PAGE_EXECUTE_READ) == PAGE_EXECUTE_READ) printf(", Execute Read"); if((dwTarget & PAGE_EXECUTE_READWRITE) == PAGE_EXECUTE_READWRITE) printf(", Execute ReadWrite"); if((dwTarget & PAGE_EXECUTE_WRITECOPY) == PAGE_EXECUTE_WRITECOPY) printf(", Write Copy"); if((dwTarget & PAGE_NOACCESS) == PAGE_NOACCESS) printf(", No Access"); } void memory_list(HANDLE hProcess) { LPCVOID pBlock; // memory block LPCVOID pEnd; SYSTEM_INFO si = {0}; // system information MEMORY_BASIC_INFORMATION mbi = {0}; // buffer of information char szSize[MAX_PATH]; // get system information GetSystemInfo(&si); // polling address space of hProcess pBlock = (LPVOID)si.lpMinimumApplicationAddress; while (pBlock < si.lpMaximumApplicationAddress) { // get next virtual memory block information if ( VirtualQueryEx(hProcess,pBlock,&mbi,sizeof(mbi))==sizeof(mbi)) { pEnd = (PBYTE) pBlock + mbi.RegionSize; // calculate end and size of block StrFormatByteSize(mbi.RegionSize, szSize, MAX_PATH) ; // display address and size printf("[%08x,%08x] (%7s) ",pBlock, pEnd, szSize); // display state of block switch(mbi.State){ case MEM_COMMIT : printf("Committed"); break; case MEM_FREE : printf("Free"); break; case MEM_RESERVE: printf("Reserved"); break; } // display protect status if(mbi.Protect==0 && mbi.State!=MEM_FREE) mbi.Protect = PAGE_READONLY; ShowProtection(mbi.Protect); // display type of mbi switch (mbi.Type) { case MEM_IMAGE: printf(", Image"); break; case MEM_MAPPED: printf(", Mapped"); break; case MEM_PRIVATE: printf(", Private"); break; } puts(""); // move pointer to get next block pBlock = pEnd; } } } int main() { memory_list(GetCurrentProcess()); getchar(); return 0; }
(5) 其他
目前筆者所用之方式效率很差。
step 1 : 輸入 val,提昇 application 權限,找到 hProcess,OpenProcess。
step 2 : 用 VirtualQueryEx 查 process memory 狀態,只抓 MEMORY_BASIC_INFORMATION::state 為 MEM_COMMIT ,MEMORY_BASIC_INFORMATION::Protect 為 PAGE_EXECUTE_READWRITE。
step 3 : 符合 step 2 條件者,在取出來之 block,逐一 polling,查看是否與 val 相等,相等則放入一份 list 中。
step 4 : 再次輸入一個新之 val ,回到 step 2,這次判斷的部份除了符合 step 2 外,同時必須判斷是否存在 list 中 < 這裡有很多方式加速沒錯 >。
關鍵在於 step 3 速度很慢,也可能是筆者之 code 還不夠成熟,於此謹做為紀錄。
留言列表