【文章标题】: 突破封锁线:第五章--外围篇
【文章作者】: Austin
【作者邮箱】: austiny.cn@gmail.com
【软件名称】: 神州数码网络客户登陆程序
【软件大小】: 不重要
【下载地址】: 自己搜索下载
【加壳方式】: UltraProtect 1.x
【保护方式】: 加壳 网络封禁
【编写语言】: VC6.0
【使用工具】: VC6.0
【操作平台】: Windows2003
【软件介绍】: 神州数码小区宽带网络登陆客户端,诸多限制
【作者声明】: 技术交流中。。。
--------------------------------------------------------------------------------
【详细过程】
网络采用了神州数码公司的网络管理产品。主要弊端是:1.封禁代理;2.封禁路由器;3.封禁BT。
(以下省略声讨文字若干),总之,有压迫的地方就有反抗。让我们来解决它。
一、知己知彼
神州数码采用了802.1x的认证方式,即认证流于数据流分开,
这样降低了认证服务器的负担,提高了网络通讯效率,也为我们破解提供了方便。
下面描述一下正常情况下,使用客户端登陆的流程:
1.打开客户端(废话)
2.选择网卡,申请服务(此时客户端会向认证服务器发送第一个数据包,我们叫它服务包)
3.填写正确的用户名和密码,登陆(此时客户端会向服务器发送第二个数据包,认证包)
4.此后每隔三十秒,客户端会向服务器发送保持登陆的数据
(这就是第三个包,连接包,根据收到的反馈信息,由客户端会随机产生)
5.在客户端运行过程中,它会不断检验代理和BT软件的运行,一旦发现,主动断开连接。
有上述分析,可以看到,路由器的封禁是在第3步实现的(通过读取网卡信息),而代理和BT的封禁在第5步实现。
二、弱点分析
1.在认证方式的设计中,一旦登陆成功,其连接包将不会改变,
所以只要截下此包,以三十秒的周期向服务器发送,便可以强行关闭客户端,其代理和BT封禁便迎刃而解。
2.对于路由器的封锁稍微麻烦些,关键是正确登陆信息的取得和发出,和保证客户端算出对应的连接信息,
所以在此过程中,不可以抛开客户端。
三、作战部署
由前面的分析可以看到,破解的核心在于截包(网络嗅探)。
通常的编程方法是利用wincap等开发库监视底层通讯,或者将网卡置于混杂模式,过滤通过其所有数据。
但这些方法太面向底层,复杂且开销过大。杀鸡焉用牛刀?
对付小小的神州数码,决定采用一种较为轻便优雅的方法----API拦截(或叫API钩子)。
因为在Windows平台上,程序的功能最终都是通过调用Windows API来实现的。
如这里的网络通讯,收发都使用了UDP协议,典型的由Windows API函数sendto和recvfrom来实现。
所以截获了这两个函数,就能轻松完成截包工作。
(这样,我们的任务就和大多数网游外挂要完成的任务相仿了)
不过,要截获API函数,也不是一件简单的工作。
因为API钩子的编写不像键盘钩子(用于偷密码),鼠标钩子(察看*密码、屏幕取词等)有现成的钩子函数可以使用,
它是一种非常规编程技术。说白了,就是要我们自己的代码在别的程序的空间内运行。
我们知道,在Windows平台下,使用了虚拟地址空间技术将不同进程的代码和数据分离,
但dll文件在整个内存中是共享的,所以,通过使目标进程加载我们自己编写的dll文件,
就有可能让自己的代码在目标进程中执行,这就是所谓的dll注入技术。
四、瞒天过海
我们提到要使用dll注入技术来完成我们的工作。
幸好,Windows API中为我们提供了一个美妙的函数CreateRemoteThread,恰好可以为我们所用。
这个函数用来在目标进程中运行一个线程,而如果这个线程所完成的任务就是加载我们的dll文件,
那注入的任务不就正好搞定了吗?
但是还有一个问题,我们可以让目标进程运行LoadLibraryA这个函数,因为它在每个进程空间里的地址都是一样的,
但我们的dll路径如何传给目标呢?
不用急,Windows也为我们准备好了,
VirtualAllocEx和WriteProcessMemory函数正好完成了在目标进程里内存分配和写入数据的任务。
这样,只要三个函数配合使用,目标进程就乖乖的加载好了我们的dll。
BOOL WINAPI InjectLib(DWORD dwProcessId, LPCSTR strLibFile)
{
BOOL result=FALSE;
HANDLE hProcess=NULL, hThread=NULL;
LPCSTR strLibFileRemote=NULL;
__try
{
hProcess=OpenProcess(
PROCESS_CREATE_THREAD|
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE,
FALSE,dwProcessId);
if(hProcess==NULL)__leave;
int cch=1+strlen(strLibFile);
int cb= cch*sizeof(CHAR);
strLibFileRemote=(LPCSTR)
VirtualAllocEx(hProcess,NULL,cb,MEM_COMMIT,PAGE_READWRITE);
if(strLibFileRemote==NULL)__leave;
if(!WriteProcessMemory(hProcess,(PVOID)strLibFileRemote,
(PVOID)strLibFile,cb,NULL))__leave;
PTHREAD_START_ROUTINE pfnThreadRtn=(PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle("Kernel32"),"LoadLibraryA");
if(pfnThreadRtn==NULL)__leave;
hThread=CreateRemoteThread(hProcess,NULL,0,pfnThreadRtn,(PVOID)strLibFileRemote,0,NULL);
if(hThread==NULL)__leave;
WaitForSingleObject(hThread,INFINITE);
result=TRUE;
}
__finally
{
if(strLibFileRemote!=NULL)
VirtualFreeEx(hProcess,(PVOID)strLibFileRemote,0,MEM_RELEASE);
if(hThread!=NULL)
CloseHandle(hThread);
if(hProcess!=NULL)
CloseHandle(hProcess);
}
return result;
}
注意,现在只是加载,并没有运行。不过我们知道,每次dll被加载时,它的DllMain函数都会被调用,
是的,聪明的你应该已经想到,我们的劫持代码就是在这儿被调用的。
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
//MessageBox(NULL,"Attached to Process","Caution",MB_OK);
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2,2);
err = WSAStartup( wVersionRequested, &wsaData );
if(err)MessageBox(NULL,"WSAStartup Error","Error",MB_OK);
s=socket(AF_INET,SOCK_DGRAM,0);
MyFunc(); //这里面就是我们的劫持代码
}
case DLL_PROCESS_DETACH:
{
//MessageBox(NULL,"Detached from Process","Caution",MB_OK);
}
case DLL_THREAD_ATTACH:
{
//MessageBox(NULL,"Attached to Thread","Caution",MB_OK);
}
case DLL_THREAD_DETACH:
{
//MessageBox(NULL,"Detached from Thread","Caution",MB_OK);
}
}
return TRUE;
}
下面来关注劫持的问题。大家知道,Windows的每个进程都会维护一张虚拟函数地址表。
程序显示链接的函数地址都保存在这张表中。
所以,只要修改了这张表,就可以使得对API函数的调用编程对我们自己函数的调用(amazing, huh?)。
于是话不多说,改!
可是,我们会郁闷的发现,由于加壳的缘故,
神州数码的客户端并没有使用显示链接到SendTo函数(就是我们要劫持的那个,记得吗),
而是用LibraryA函数动态加载。所以,修改虚拟函数的方法失效了。
真的没办法了吗?当然不是。不然我还写这篇东西干吗呢?
凡走过比留下痕迹。改还是要改的,不过不改SendTo了,而是改GetProcAddress。
因为动态加载通过它来返回函数地址,只要我们把对SendTo的加载返回成自己函数的地址,瞒天过海之计就成了。
extern "C" __declspec(dllexport) void MyFunc()
{
HOOKAPI a;
LPHOOKAPI pHookApi=&a;
HMODULE hm1=(HMODULE)0x00400000;
pHookApi->szFunc=(LPCSTR)"GetProcAddress";
pHookApi->pNewProc=(PROC)MyGetProcAddress;
HookAPIByName(hm1,"kernel32.dll",pHookApi);
pHookApi->szFunc=(LPCSTR)"LoadLibraryA";
pHookApi->pNewProc=(PROC)MyLoadLibrary;
HookAPIByName(hm1,"kernel32.dll",pHookApi); //这里劫持了LoadLibraryA函数
}
typedef struct tag_HOOKAPI
{
LPCSTR szFunc;//待HOOK的API函数名
PROC pNewProc;//新的函数指针
PROC pOldProc;//老的函数指针
}HOOKAPI, *LPHOOKAPI;
PIMAGE_IMPORT_DESCRIPTOR GetNamedImportDescriptor(HMODULE hModule, LPCSTR szImportMod)
{
//首先是DOS头
PIMAGE_DOS_HEADER pDOSHeader = (PIMAGE_DOS_HEADER) hModule;
if(pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE) return NULL;
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDOSHeader + (DWORD)(pDOSHeader->e_lfanew));
if(pNTHeader->Signature != IMAGE_NT_SIGNATURE) return NULL;
//如果没有Import部分,返回失败
if(pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)
return NULL;
//取Import部分
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
((DWORD)pDOSHeader + (DWORD)(pNTHeader->OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress));
//寻找与szImportMod相配部分
while (pImportDesc->Name)
{
PSTR szCurrMod = (PSTR)((DWORD)pDOSHeader + (DWORD)(pImportDesc->Name));
if (stricmp(szCurrMod, szImportMod) == 0)
break; //找到
pImportDesc++;
}
if(pImportDesc->Name == NULL) return NULL;
return pImportDesc;
}
HookAPIByName(HMODULE hModule/*被HOOK的目标进程MODULE*/, LPCSTR szImportMod/*如GDI32.DLL*/,LPHOOKAPI pHookApi/*指定函数
名,如"MessageBoxW"*/)
{
PIMAGE_IMPORT_DESCRIPTOR pImportDesc =
GetNamedImportDescriptor(hModule, szImportMod);
if (pImportDesc == NULL)
return FALSE; //需要改换的API不能取到正确描
PIMAGE_THUNK_DATA pOrigThunk = (PIMAGE_THUNK_DATA)((DWORD)hModule + (DWORD)(pImportDesc->OriginalFirstThunk));
PIMAGE_THUNK_DATA pRealThunk =
(PIMAGE_THUNK_DATA)((DWORD)hModule + (DWORD)(pImportDesc->FirstThunk));
while(pOrigThunk->u1.Function)
{
if((pOrigThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) != IMAGE_ORDINAL_FLAG)
{
PIMAGE_IMPORT_BY_NAME pByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)hModule + (DWORD)
(pOrigThunk->u1.AddressOfData));
if(pByName->Name[0] == '\0')
return FALSE; //失败
if(strcmpi(pHookApi->szFunc, (char*)pByName->Name) == 0)
{
//改变thunk保护属性
MEMORY_BASIC_INFORMATION mbi_thunk;
VirtualQuery(pRealThunk, &mbi_thunk, sizeof(MEMORY_BASIC_INFORMATION));
VirtualProtect(mbi_thunk.BaseAddress,mbi_thunk.RegionSize, PAGE_READWRITE,
&mbi_thunk.Protect);
//保存原来的API函数指针
if(pHookApi->pOldProc == NULL)
pHookApi->pOldProc = (PROC)pRealThunk->u1.Function;
//改变API函数指针
pRealThunk->u1.Function = (PDWORD)pHookApi->pNewProc;
//将thunk保护属性改回来
DWORD dwOldProtect;
VirtualProtect(mbi_thunk.BaseAddress, mbi_thunk.RegionSize,
mbi_thunk.Protect, &dwOldProtect);
}
}
pOrigThunk++;
pRealThunk++;
}
SetLastError(ERROR_SUCCESS);
return TRUE;
}
五、实战演练
现在万事俱备,只欠一个我们反复提到的----自己的函数就可以完成全部的任务了。
那这个自己的函数要做些什么呢?其实很简单,就是把通过它发送的数据包一一记录下来备用即可。
而记录的数据,将要被我们自己的程序所使用,所以存在dll的共享数据区里面。
这是其一,对于解除代理和BT的封禁,这已经足够了。
但要解决路由器的问题,还需要再加一点功能。
我们会把正确的不带路由器的认证包(记为A包)存起来,然后在有路由器环境下,
客户端会产生指示使用了路由器的认证包(记为B包),用A包代替B包发送出去,就起到了欺骗服务器的作用,
解除了路由器的封禁。
static int WINAPI Mysendto (
SOCKET s,
const char FAR * buf,
int len,
int flags,
const struct sockaddr FAR * to,
int tolen
)
{
int ii=0;
if(len!=26)
{
if(statue_client==0)
{
if(stat_authorize==0)
{
for(int tt=0;tt<len;tt++)
{
*(buf_authorize+tt)=*(buf+tt);
}
len_authorize=len;
tolen_authorize=tolen;
flags_authorize=flags;
strcpy(to_authorize.sa_data,to->sa_data);
to_authorize.sa_family=to->sa_family;
stat_authorize=1;
ii=::sendto(s,buf,len,flags,to,tolen);
}
else
{
ii=::sendto(s,buf_authorize,len_authorize,flags_authorize,&to_authorize,tolen_authorize);
}
statue_client=1;
}
else if(statue_client==1)
{
statue_client=2;
for(int tt=0;tt<len;tt++)
{
*(buf_keepconn+tt)=*(buf+tt);
}
len_keepconn=len;
tolen_keepconn=tolen;
flags_keepconn=flags;
strcpy(to_keepconn.sa_data,to->sa_data);
to_keepconn.sa_family=to->sa_family;
stat_keepconn=1;
statue_client=3;
ii=::sendto(s,buf,len,flags,to,tolen);
}
}
else
{
for(int tt=0;tt<len;tt++)
{
*(buf_getservice+tt)=*(buf+tt);
}
len_getservice=len;
tolen_getservice=tolen;
flags_getservice=flags;
strcpy(to_getservice.sa_data,to->sa_data);
to_getservice.sa_family=to->sa_family;
stat_getservice=1;
ii=::sendto(s,buf,len,flags,to,tolen);
}
return ii;
}
六、屠城三日
至此,神州数码的三大封禁已基本被我们解决,
剩下来的工作,就是愉快的上网,愉快的用代理,愉快的用路由器,愉快的BT了。
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
2006年05月13日 3:00:40
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!