QQGame浅析系列(1)--多开大厅刷分你也可以
本系列仅以学习交流使用,非法使用者,后果自负。
最近闲来无聊就赔朋友一起打打牌,是QQ游戏斗地主游戏,我们通常在一个桌(Table)打牌,这样配合胜率会高一点.有一次他正在和别人打牌,四人已满,我就不能和他一起打了,就随便点了一下发现能进得此桌,原来游戏提供了"旁观"的功能,不过是当时才知道的.于是我就一个个地"旁观"别人的牌,将牌符告诉我的朋友,他果然"运筹帷幄,决胜千里",呵呵....
于是想到了一个"坏点子":自己开两个号打牌,一个用来旁观,另一个用来打牌,岂不是很容易就取胜?当我兴冲冲地准备"作祟"时,发现QQGame并不能同时运行两个实例,所以我上述的想法就暂时不能实现了.要想实现,就要突破这一关,于是抱着试试看的心理分析下"QQGame不能同时运行两个实例"的原理.
程序实现只能运行一个实例的原理通常是使用互斥元,临界区,信号量等机制.
于是就OD载入,下断:
bp CreateMutexA
F9运行,中断,再次F9......若干次后会看到堆栈提示:
0013FE90 100118DF /CALL 到 CreateMutexA 来自 Utility.100118D9
0013FE94 00000000 |pSecurity = NULL
0013FE98 00000001 |InitialOwner = TRUE
0013FE9C 00F88700 \MutexName = "QQGame_Mutex03/01/2003"
MutexName为"QQGame_Mutex03/01/2003"的就是我们所关心的.
如果知道了这一点,那么要想绕过"QQGame不能同时运行两个实例"就很简单了,我这里采用的是在不破坏原程序完整性
的方法,现介绍如下:
MutexName为"QQGame_Mutex03/01/2003"的串存放在一个叫做MainLogi.dll文件中,在游戏安装目录下的Logic文件夹里.如果每次运行游戏程序MutexName与上次的都不相同,不就可以绕过了吗?也就是每次运行程序前偷偷地改变MainLogi.dll文件中的MutexName.具体实现见代码及分析:
program QQgameSpy;
uses
windows,dialogs,sysutils,classes,graphics,shellapi ,registry;
var
reg:TRegistry;
strPath:string;
strFileName:string;
strNewFile:string;
strGameFile:string;
strMutex:string;
i:integer;
hMutex:DWORD;
hFile:DWORD;
dwBytesOfRW:Cardinal;
buffer:BYTE;
DstFile:TFileStream;
hwnd:DWORD;
{$r QQgameSpy.RES}
function RenameFile(hdl:THandle;lpOrgName,lpNewName:PAnsichar):boolean;
var
shf:TSHFILEOPSTRUCT;
begin
try
FillChar(shf,SizeOf(shf),0);
shf.Wnd :=hdl;
shf.wFunc :=FO_RENAME;
shf.pFrom :=lpOrgName;
shf.pTo :=lpNewName;
shf.fFlags :=FOF_NOCONFIRMATION+FOF_SILENT;
Result:=(0=SHFileOperation(shf));
except
Result:=false;
end;
end;
{ 主程序开始 }
begin
reg:= TRegistry.Create;
reg.RootKey:=HKEY_CURRENT_USER;
if reg.OpenKeyReadOnly('\Software\Tencent\QQGame\SYS')= false then
exit;
strPath:=reg.ReadString('GameDirectory');
//showmessage(strPath);
reg.CloseKey;
reg.Free;
strFileName:=strPath+'Logic\MainLogi.dll';
strMutex:='QQGame_Mutex03/01/2000';
for i:=0 to 3 do begin
hMutex:=CreateMutex(nil,false,pchar(strMutex));
if hMutex=0 then
continue;
if GetLastError<>ERROR_ALREADY_EXISTS then
break;
Inc( strMutex[Length(strMutex)] );
end;
//showmessage(strMutex);
buffer:=BYTE(strMutex[Length(strMutex)]);
if hMutex=0 then
exit;
CloseHandle(hMutex);
//DstFile:=TFileStream.Create(strFileName,fmOpenReadWrite);
//DstFile.Seek($581C1,FILE_BEGIN);
//DstFile.WriteBuffer(buffer,sizeof(buffer));
for i:=0 to 2 do begin
strNewFile:=strFileName+inttostr(i);
DeleteFile(strNewFile);
end;
for i:=0 to 2 do begin
strNewFile:=strFileName+inttostr(i);
if FileExists(strNewFile)=false then
break;
end;
RenameFile(0,pchar(strFileName),pchar(strNewFile));
CopyFile(pchar(strNewFile),pchar(strFileName),true);
hFile:=CreateFile(pchar(strFileName),GENERIC_WRITE or GENERIC_READ,FILE_SHARE_WRITE or FILE_SHARE_READ,
nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
if hFile=INVALID_HANDLE_VALUE then
exit;
SetFilePointer(hFile,$581C1,nil,FILE_BEGIN);
WriteFile(hFile,buffer,sizeof(buffer),dwBytesOfRW,nil);
CloseHandle(hFile);
strGameFile:=strPath+'QQGame.exe';
//showmessage(strGameFile);
ShellExecute(0,'Open',pchar(strGameFile),nil,nil,SW_SHOWNORMAL);
end.
解释: 当程序运行时通过注册表查询QQGame的安装目录,进而获取MainLogi.dll文件的完整文件名。由于QQ游戏一般来说没桌最多不超过四人,所以每次运行前首先查看MutexName为:
'QQGame_Mutex03/01/2000';
'QQGame_Mutex03/01/2001';
'QQGame_Mutex03/01/2002';
'QQGame_Mutex03/01/2000';
的互斥元是否存在,如果遇到有不存在的,说明可以以此互斥元名运行QQ游戏,就改写原MainLogi.dll文件名为其他,复制一份MainLogi.dll文件并更换其中的MutexName,最后ShellExecute QQ游戏,这样当QQ游戏运行检测时就会使用我们更改过的MutexName,由于我们实现已经检测过这个MutexName还没有被使用,当然可以成功运行了。
这里你可能很诧异:MainLogi.dll文件被游戏加载过一次,下次还能被改名吗?我试过了,删除此文件不可以,但是可以对其重命名。上面声明的RenameFile就是实现更改文件名功能的。
运行后,可以开N个大厅,于是乎狂玩了一下午,很快刷到了600多分,胜负率高达60%以上啊!不过也就是当时一会的劲头,再后来就没有玩过了,现在将此方法公布与大家分享一下,希望玩得愉快!
说明:在我欲发此贴时,QQGame有所更新,使用上述方法不行了,如果不给出一个新的解决方法,总感觉不甚完美,于是重新又跟了一次。思路如下:
如果已有一个登录窗体运行了,那么再运行实例会给那个已运行的实例一个焦点,让用户知道“你已经运行啦”,然后退出。
于是
Bp FindWindowA
Bp FindWindowExA
F9运行,中断一个,看堆栈提示:
0013FE7C 100119A3 /CALL 到 FindWindowExA 来自 Utility.1001199D
0013FE80 00000000 |hParent = NULL
0013FE84 00000000 |hAfterWnd = NULL
0013FE88 01069BD0 |Class = "QQGame_MainFrame"
0013FE8C 00000000 \Title = NULL
返回到调用的过程:
《Utility领空》
1001199D FF15 18920110 call dword ptr [<&USER32.FindWindowEx>; USER32.FindWindowExA
100119A3 8BF8 mov edi, eax
100119A5 57 push edi
100119A6 FF15 78920110 call dword ptr [<&USER32.IsWindow>] ; USER32.IsWindow
100119AC 85C0 test eax, eax
100119AE 74 0E je short 100119BE
100119B0 53 push ebx
100119B1 53 push ebx
100119B2 FF76 50 push dword ptr [esi+50]
100119B5 57 push edi
100119B6 FF15 7C920110 call dword ptr [<&USER32.PostMessageA>; USER32.PostMessageA
100119BC EB 10 jmp short 100119CE
100119BE 83C6 14 add esi, 14
100119C1 68 1CE70110 push 1001E71C
100119C6 56 push esi
100119C7 E8 4A4FFFFF call 10006916
100119CC 59 pop ecx
100119CD 59 pop ecx
100119CE 5F pop edi
100119CF 5E pop esi
100119D0 5B pop ebx
100119D1 C3 retn
执行流程是这样的,检测是否已有窗体运行了,如果有找到它,给它发个消息(这个消息估计就是让那个窗体拥有焦点的)然后退出。
我们由此过程再次返回,发现是
100118F5 E8 92000000 call 1001198C
调用的上述过程,前后浏览一下代码:
《Utility领空》
10011846 55 push ebp
10011847 8BEC mov ebp, esp
10011849 51 push ecx
1001184A 8B45 18 mov eax, dword ptr [ebp+18]
1001184D 8365 FC 00 and dword ptr [ebp-4], 0
10011851 53 push ebx
10011852 56 push esi
10011853 8BF1 mov esi, ecx
10011855 57 push edi
10011856 BB 00010000 mov ebx, 100
1001185B 8946 54 mov dword ptr [esi+54], eax
1001185E 8B45 14 mov eax, dword ptr [ebp+14]
10011861 8DBE 58010000 lea edi, dword ptr [esi+158]
10011867 8946 50 mov dword ptr [esi+50], eax
1001186A 85FF test edi, edi
1001186C 74 21 je short 1001188F
1001186E 837D 10 00 cmp dword ptr [ebp+10], 0
10011872 74 1B je short 1001188F
10011874 53 push ebx
10011875 8D4E 45 lea ecx, dword ptr [esi+45]
10011878 FF75 10 push dword ptr [ebp+10]
1001187B E8 16050000 call 10011D96
10011880 85C0 test eax, eax
10011882 74 0B je short 1001188F
10011884 FF75 10 push dword ptr [ebp+10]
10011887 57 push edi
10011888 E8 19580000 call <jmp.&MSVCRT.strcpy>
1001188D 59 pop ecx
1001188E 59 pop ecx
1001188F 8D7E 58 lea edi, dword ptr [esi+58]
10011892 85FF test edi, edi
10011894 74 21 je short 100118B7
10011896 837D 0C 00 cmp dword ptr [ebp+C], 0
1001189A 74 1B je short 100118B7
1001189C 53 push ebx
1001189D 8D4E 45 lea ecx, dword ptr [esi+45]
100118A0 FF75 0C push dword ptr [ebp+C]
100118A3 E8 EE040000 call 10011D96
100118A8 85C0 test eax, eax
100118AA 74 0B je short 100118B7
100118AC FF75 0C push dword ptr [ebp+C]
100118AF 57 push edi
100118B0 E8 F1570000 call <jmp.&MSVCRT.strcpy>
100118B5 59 pop ecx
100118B6 59 pop ecx
100118B7 837D 08 00 cmp dword ptr [ebp+8], 0
100118BB 74 5F je short 1001191C
100118BD FF75 08 push dword ptr [ebp+8]
100118C0 E8 E7570000 call <jmp.&MSVCRT.strlen>
100118C5 F7D8 neg eax
100118C7 1BC0 sbb eax, eax
100118C9 59 pop ecx
100118CA 40 inc eax
100118CB 8945 10 mov dword ptr [ebp+10], eax
100118CE 75 4C jnz short 1001191C
100118D0 FF75 08 push dword ptr [ebp+8]
100118D3 6A 01 push 1
100118D5 5F pop edi
100118D6 57 push edi
100118D7 6A 00 push 0
100118D9 FF15 E8900110 call dword ptr [<&KERNEL32.CreateMutexA>] ; kernel32.CreateMutexA
100118DF 85C0 test eax, eax
100118E1 8946 4C mov dword ptr [esi+4C], eax
100118E4 74 24 je short 1001190A
100118E6 FF15 AC900110 call dword ptr [<&KERNEL32.GetLastError>] ; ntdll.RtlGetLastWin32Error
100118EC 3D B7000000 cmp eax, 0B7
100118F1 8BCE mov ecx, esi
100118F3 75 07 jnz short 100118FC
100118F5 E8 92000000 call 1001198C
100118FA EB 2A jmp short 10011926
100118FC E8 64010000 call 10011A65
10011901 85C0 test eax, eax
10011903 74 21 je short 10011926
10011905 897D FC mov dword ptr [ebp-4], edi
10011908 EB 1C jmp short 10011926
1001190A 83C6 14 add esi, 14
1001190D 68 F8E60110 push 1001E6F8
10011912 56 push esi
10011913 E8 FE4FFFFF call 10006916
10011918 59 pop ecx
10011919 59 pop ecx
1001191A EB 0A jmp short 10011926
1001191C 8BCE mov ecx, esi
1001191E E8 42010000 call 10011A65
10011923 8945 FC mov dword ptr [ebp-4], eax
10011926 8B45 FC mov eax, dword ptr [ebp-4]
10011929 5F pop edi
1001192A 5E pop esi
1001192B 5B pop ebx
1001192C C9 leave
1001192D C2 1400 retn 14
在运行到
100118D9 FF15 E8900110 call dword ptr [<&KERNEL32.CreateMutexA>] ; kernel32.CreateMutexA
一句时,MutexName没有变,就是和我们之前的分析是一样的,只是多了一个对窗口的检测,呵呵,好对付。
再次返回:
《MainLogi领空》
00F6714E 56 push esi
00F6714F 8BF1 mov esi, ecx
00F67151 57 push edi
00F67152 83BE A8010000 0>cmp dword ptr [esi+1A8], 0
00F67159 75 25 jnz short 00F67180
00F6715B E8 BF0F0100 call 00F7811F
00F67160 85C0 test eax, eax
00F67162 74 1C je short 00F67180
00F67164 8B10 mov edx, dword ptr [eax]
00F67166 68 02900004 push 4009002
00F6716B 68 8440F800 push 00F84084 ; ASCII "Utility.dll"
00F67170 56 push esi
00F67171 8BC8 mov ecx, eax
00F67173 FF52 08 call dword ptr [edx+8]
00F67176 85C0 test eax, eax
00F67178 74 06 je short 00F67180
00F6717A 8986 A8010000 mov dword ptr [esi+1A8], eax
00F67180 8B86 A8010000 mov eax, dword ptr [esi+1A8]
00F67186 8BD6 mov edx, esi
00F67188 F7DA neg edx
00F6718A 8D48 04 lea ecx, dword ptr [eax+4]
00F6718D 8D7E 04 lea edi, dword ptr [esi+4]
00F67190 1BD2 sbb edx, edx
00F67192 6A 00 push 0
00F67194 8B01 mov eax, dword ptr [ecx]
00F67196 23D7 and edx, edi
00F67198 52 push edx
00F67199 FF10 call dword ptr [eax]
00F6719B 8B8E A8010000 mov ecx, dword ptr [esi+1A8]
00F671A1 68 5E100000 push 105E
00F671A6 68 861F0000 push 1F86
00F671AB 68 1887F800 push 00F88718 ; ASCII "QQGame_MainFrame"
00F671B0 8B01 mov eax, dword ptr [ecx]
00F671B2 68 EC80F800 push 00F880EC ; ASCII "QQGame"
00F671B7 68 0087F800 push 00F88700 ; ASCII "QQGame_Mutex03/01/2003"
00F671BC FF50 0C call dword ptr [eax+C] ; 进
00F671BF 8BF8 mov edi, eax
00F671C1 83C6 08 add esi, 8
00F671C4 57 push edi
00F671C5 68 C886F800 push 00F886C8 ; ASCII "Logout_Optimize CheckMultiInstanceRunning() return [%d]"
00F671CA 56 push esi
00F671CB E8 EEF1FEFF call 00F563BE
00F671D0 83C4 0C add esp, 0C
00F671D3 8BC7 mov eax, edi
00F671D5 5F pop edi
00F671D6 5E pop esi
00F671D7 C3 retn
再次返回
《QQGame领空》
004010CE |. FF50 38 call dword ptr [eax+38] ;进
004010D1 |. 85C0 test eax, eax
004010D3 |. 74 13 je short 004010E8
注意这个返回值就很重要了,如果暴力一点的话就就将004010D3 je short 004010E8
Nop掉就OK了。
更新的版本主要是多了一个对窗口的检测,标志值存放在eax里,如果想跟踪的话就关键看对eax的修改即可,我就不跟了。
不过这样有一个缺陷,那就是如果程序中增加了文件校验就会发现程序被修改而拒绝执行,不过“兵来将挡,水来土掩”,知道了这个机理解决办法还是不少的。

《“无法显示网页”是由于我没有联网》