Go 和 Rust 是近几年比较受关注的新编程语言,两者没有直接的竞争关系,更多是互补。如果你想使用两者的优势并喜欢互操作,本文也许对你有帮助!
大多数主流编程语言都努力适应一些通用标准,以提高互操作性并减少采用摩擦。不过 Golang 不是其中之一。在这篇博文中,我们将展示如何克服 Go 的孤立主义设计并与另一种语言(在我们的例子中为 Rust)集成。
为什么我们需要与 Go 互操作?mirrord 通过将系统调用挂接到操作系统[1]并应用决定是在本地还是远程执行的逻辑来工作。为此,镜像侧加载(使用LD_PRELOAD
)到进程中,然后挂钩相关函数。为了涵盖最常见的场景,镜像挂 libc 函数,这适用于大多数常见语言(Python、macOS 上的 Go、Rust、Node 等等),因为它们都依赖于 libc。
Go 在 Linux 上不使用 libc[2],而是直接调用系统调用。这对普通开发人员来说几乎是无害的——他们不关心程序集、系统调用、链接等——他们只希望他们的二进制文件能够工作。因此,自包含提供了非常好的用户体验,因为 Go 应用程序不依赖于本地机器的 libc。
不过,这对我们来说是非常有害的。由于我们显式地覆盖了 libc 函数,因此我们的软件在与 Go 应用程序(或任何其他不调用 libc 的进程)一起运行时根本无法运行。因此,我们必须 Hook Golang 函数!
幸运的是,Go 应用程序与其他软件并不完全不同。Golang必须与操作系统一起工作,所以它必须使用系统调用。由于 libc 并没有在它包装的系统调用之上添加太多逻辑,我们仍然可以使用我们现有的所有代码——我们只需要用它覆盖一个不同的函数。
我们如何 Hook Golang 函数?我们使用 libc 函数的方式与 Frida[3] 相同。问题是编写可以在 Go 例程调用状态下工作的 Rust 代码并非易事。Go 有自己的 ABI,它不符合任何常见的 ABI。不过,这种不符合项相对常见。例如,Rust 也有一个不稳定的内部 ABI。如果我们可以在加载到 Go 二进制文件之前重新编译它,我们可以使用 cgo 来访问标准 C ABI,但在我们的用例中不能。这意味着我们必须实现 trampoline[4]。
rust, go, asm trampoline
trampoline 将用 Assembly 编写,其目的是将 Go 函数调用转换为 Rust 函数调用,然后返回原始 Go 函数的调用者期望它返回的结果。
查看我们的 Go 二进制文件[5]和包的依赖项的回溯net/http
,很明显它涉及到syscall
包的使用。通过使用Ghidra[6]对 Go 二进制文件进行逆向工程,我们将相关流程(socket、listen、accept 等)映射到我们需要 Hook 的三个不同函数:
syscall.Syscall6.abi0
- 带有 6 个参数的系统调用让运行时知道我们切换到阻塞操作,以便它可以在另一个线程/goroutine 上调度。syscall.Syscall.abi0
- 相同syscall.Syscall6.abi0
但具有三个参数。syscall.RawSyscall.abi0
- 与上述相同,但不通知运行时。让我们从一个非常基本的 trampoline 开始,钩子syscall.RawSyscall.abi0
(一个使用 3 个参数调用系统调用的例程,也在socket
syscall 包中使用)。下面是这个函数的反汇编:
disassembly of syscall.RawSyscall.abi0 using Ghidra
我们将按照 Rust 在 C ABI 中的期望将参数从堆栈移动到寄存器来实现这个蹦床,然后按照 Go 的期望在堆栈上返回结果。
mov rsi, QWORD PTR [rsp+0x10]
mov rdx, QWORD PTR [rsp+0x18]
mov rcx, QWORD PTR [rsp+0x20]
mov rdi, QWORD PTR [rsp+0x8]
Golang 有自己的 ABI(如前所述),准确地说 ABI0
和 ABIInternal
。Go 与基于堆栈的调用约定以及最近引入的基于寄存器的调用约定保持向后兼容性[7]。事实证明,ABI0
函数遵循基于堆栈的约定,这就是我们从堆栈而不是寄存器中移动值的原因。
call c_abi_syscall_handler
遵循 Go 中基于堆栈的约定,我们将参数移动到寄存器。但究竟是什么登记,为什么?由于我们要挂钩一个直接进行系统调用的函数,因此我们需要一个处理程序来为我们管理系统调用。我们的处理程序将使用 C ABI 调用约定进行调用,它将匹配系统调用并根据它们的类型将它们重定向到它们的特定弯路,并将结果返回到符合 C ABI 的特定寄存器中。
#[no_mangle]
unsafe extern "C" fn c_abi_syscall_handler(
syscall: i64,
param1: i64,
param2: i64,
param3: i64,
) -> i32 {
let res = match syscall {
libc::SYS_socket => {
let sock = libc::socket(param1 as i32, param2 as i32, param3 as i32);
sock
}
_ => libc::syscall(syscall, param1, param2, param3) as i32,
};
return res;
}
asm_linux_amd64.s[8]:
// func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.
如上所述,正如我们在反汇编中看到的,我们将把处理程序返回的结果移回堆栈,如下所示:
mov QWORD PTR [rsp+0x28],rax
mov QWORD PTR [rsp+0x30],rdx
mov QWORD PTR [rsp+0x38],0x0
ret
#[cfg(target_os = "linux")]
#[cfg(target_arch = "x86_64")]
#[naked]
unsafe extern "C" fn go_raw_syscall_detour() {
asm!(
"mov rsi, QWORD PTR [rsp+0x10]",
"mov rdx, QWORD PTR [rsp+0x18]",
"mov rcx, QWORD PTR [rsp+0x20]",
"mov rdi, QWORD PTR [rsp+0x8]",
"call c_abi_syscall_handler",
"mov QWORD PTR [rsp+0x28],rax",
"mov QWORD PTR [rsp+0x30],rdx",
"mov QWORD PTR [rsp+0x38],0x0",
"ret",
options(noreturn),
);
}
注意Naked 功能特性[9]的使用。裸函数使我们可以完全控制生成的程序集(根据我们的用例的需要),因为 Rust 不会为它们生成 epilogue/prologue。
让我们做一个示例运行,看看是否一切正常:
running the gin server with the rawsyscall hook
伟大的!它就像我们预期的那样工作。但是,mirrord 中的实际弯路包含日志并进行大量记账。让我们从添加一个简单的调试语句开始,看看情况如何。
#[no_mangle]
unsafe extern "C" fn c_abi_syscall_handler(
syscall: i64,
param1: i64,
param2: i64,
param3: i64,
) -> i32 {
debug!("c_abi_sycall_handler received syscall: {syscall:?}");
let res = match syscall {
libc::SYS_socket => {
let sock = libc::socket(param1 as i32, param2 as i32, param3 as i32);
sock
}
_ => libc::syscall(syscall, param1, param2, param3) as i32,
};
return res;
}
行动:
mehula@mehul-machine:~/golang-e2e/server$ LD_PRELOAD=../target/debug/libmirrord.so ./server
2022-08-15T17:15:36.497241Z DEBUG mirrord: LD_PRELOAD SET
2022-08-15T17:15:36.498403Z DEBUG mirrord: "syscall.RawSyscall.abi0" hooked
Server listening on port 8080
2022-08-15T17:15:36.505606Z DEBUG mirrord: c_abi_sycall_handler received syscall: 41
2022-08-15T17:15:36.505689Z DEBUG mirrord: c_abi_sycall_handler received syscall: 41
2022-08-15T17:15:36.505738Z DEBUG mirrord: c_abi_sycall_handler received syscall: 41
unexpected fault address 0x0
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x80 addr=0x0 pc=0x7fa45a87f6b2]
goroutine 1 [running]:
runtime.throw({0x7e0b21?, 0x46?})
/usr/local/go/src/runtime/panic.go:992 +0x71 fp=0xc0002372a8 sp=0xc000237278 pc=0x4354b1
runtime: unexpected return pc for runtime.sigpanic called from 0x7fa45a87f6b2
stack: frame={sp:0xc0002372a8, fp:0xc0002372f8} stack=[0xc000218000,0xc000238000)
0x000000c0002371a8: 0x000000000045551b <runtime.write+0x000000000000003b> 0x0000000000000002
0x000000c0002371b8: 0x000000c0002371f0 0x0000000000436bae <runtime.recordForPanic+0x000000000000004e>
0x000000c0002371c8: 0x000000000045551b <runtime.write+0x000000000000003b> 0x0000000000000002
0x000000c0002371d8: 0x00000000008833ec 0x0000000000000001
0x000000c0002371e8: 0x0000000000000001 0x000000c000237228
0x000000c0002371f8: 0x0000000000436eb2 <runtime.gwrite+0x00000000000000f2> 0x00000000008833ec
0x000000c000237208: 0x0000000000000001 0x0000000000000001
0x000000c000237218: 0x000000c000237295 0x0000000000000003
0x000000c000237228: 0x000000c000237278 0x000000000046274e <runtime.systemstack+0x000000000000002e>
0x000000c000237238: 0x00000000004356f0 <runtime.fatalthrow+0x0000000000000050> 0x000000c000237248
0x000000c000237248: 0x0000000000435720 <runtime.fatalthrow.func1+0x0000000000000000> 0x000000c0000021a0
0x000000c000237258: 0x00000000004354b1 <runtime.throw+0x0000000000000071> 0x000000c000237278
0x000000c000237268: 0x000000c000237298 0x00000000004354b1 <runtime.throw+0x0000000000000071>
0x000000c000237278: 0x000000c000237280 0x00000000004354e0 <runtime.throw.func1+0x0000000000000000>
0x000000c000237288: 0x00000000007e0b21 0x0000000000000005
0x000000c000237298: 0x000000c0002372e8 0x000000000044a8c5 <runtime.sigpanic+0x0000000000000305>
0x000000c0002372a8: <0x00000000007e0b21 0x0000000000000046
0x000000c0002372b8: 0x00007fa45a0f27c0 0x00007fa45a89e8e9
0x000000c0002372c8: 0x0000000000000000 0x0000000000000000
0x000000c0002372d8: 0x00007fa45a0f27c0 0x0000000000000000
0x000000c0002372e8: 0x000000c000237ab0 !0x00007fa45a87f6b2
0x000000c0002372f8: >0x000000c000237320 0x000000c0002373e8
0x000000c000237308: 0x00007fa45a0f27c0 0x0000000000000000
0x000000c000237318: 0x00007fa45a0f27c0 0x00007fa45a0f27c0
0x000000c000237328: 0x00007fa45a0f27c0 0x0000000000000000
0x000000c000237338: 0x0000000000000000 0x0000000000000000
0x000000c000237348: 0x00007fa45a895d84 0x0000000000000000
0x000000c000237358: 0x000000000237c328 0x00007fa45a0f2840
0x000000c000237368: 0x0000000000000000 0x00007fa45b698f50
0x000000c000237378: 0x0000000000000000 0xffffffffffffffff
0x000000c000237388: 0x00007fa45a0f2840 0x0000000000000000
0x000000c000237398: 0x00007fa45a0f2840 0xffffffffffffffff
0x000000c0002373a8: 0x0000000000000000 0x00007fa45a0f27c0
0x000000c0002373b8: 0x00007fa45a0f27c0 0x00007fa45a0f27c0
0x000000c0002373c8: 0x00007fa45a0f27c0 0x00007fa45a87e8e7
0x000000c0002373d8: 0x0000000000000000 0x00007fa45a895d44
0x000000c0002373e8: 0x000000c000237420 0x000000000237c340
runtime.sigpanic()
/usr/local/go/src/runtime/signal_unix.go:825 +0x305 fp=0xc0002372f8 sp=0xc0002372a8 pc=0x44a8c5
Go 的运行时调度程序遵循一种非常奇特但智能的方式来调度 goroutine。调度器主要作用于四个重要对象:
如 Go 运行时调度程序的设计文档中所述[10],
“当一个新的 G 被创建或一个现有的 G 变为可运行时,它被推送到当前 P 的可运行 goroutine 列表中。当 P 执行完 G 时,它首先尝试从自己的可运行 goroutine 列表中弹出一个 G;如果列表为空,P 会选择一个随机受害者(另一个 P)并尝试从中窃取一半可运行的 goroutine。”
总之,每个 G 在分配给 P 的 M 上运行。
现在我们对 Go 如何调度 goroutine 有了一些了解,通过查看这个[11]源文件,我们可以看到 Golang 不能使用“系统堆栈”(在 Linux 上大多数情况下是 pthread 堆栈),而是使用自己的 goroutine 堆栈实现最小大小为 2048 字节。
Goroutine 堆栈是动态的,即它根据当前需求不断扩展/收缩。这意味着在系统堆栈中运行的任何通用代码都假定它可以随心所欲地增长(直到超过最大堆栈大小),而实际上,除非使用 Go API 进行扩展,否则它不能。我们的 Rust 代码没有意识到这一点,因此它使用了实际上不可用的部分堆栈并导致堆栈溢出。
我们缺少一些步骤。有人可能会考虑使用runtime.morestack
,但这对我们来说可能并不理想,因为这涉及到根据我们的需要手动管理堆栈。幸运的是,我们不是第一个在 Go 中做 FFI 的人,所以我们研究了 cgo 在调用外部函数时做了什么:
引用自 runtime/cgocall.go[12]:
// Cgo call and callback support.1
//
// To call into the C function f from Go, the cgo-generated code calls
// runtime.cgocall(_cgo_Cfunc_f, frame), where _cgo_Cfunc_f is a
// gcc-compiled function written by cgo.
//
// runtime.cgocall (below) calls entersyscall so as not to block
// other goroutines or the garbage collector, and then calls
// runtime.asmcgocall(_cgo_Cfunc_f, frame).
//
// runtime.asmcgocall (in asm_$GOARCH.s) switches to the m->g0 stack
// (assumed to be an operating system-allocated stack, so safe to run
// gcc-compiled code on) and calls _cgo_Cfunc_f(frame).
//
// _cgo_Cfunc_f invokes the actual C function f with arguments
// taken from the frame structure, records the results in the frame,
// and returns to runtime.asmcgocall.
//
// After it regains control, runtime.asmcgocall switches back to the
// original g (m->curg)'s stack and returns to runtime.cgocall.
//
// After it regains control, runtime.cgocall calls exitsyscall, which blocks
// until this m can run Go code without violating the $GOMAXPROCS limit,
// and then unlocks g from m.
//
我们将跳过非阻塞部分,即调用 runtime.entersyscall/runtime.exitsyscall 以让调度程序提防“阻塞”调用,以便调度程序可以将其时间让给另一个 goroutine,如 Syscall.Syscall6.abi0
的情况所示Syscall.Syscall.abi0
。因此,我们只需使用 runtime.asmcgocall.abi0
。
mov rbx, QWORD PTR [rsp+0x10]
mov r10, QWORD PTR [rsp+0x18]
mov rcx, QWORD PTR [rsp+0x20]
mov rax, QWORD PTR [rsp+0x8]
mov rdx, rsp
mov rdi, QWORD PTR fs:[0xfffffff8]
cmp rdi, 0x0
je 2f
mov r8, QWORD PTR [rdi+0x30]
mov rsi, QWORD PTR [r8+0x50]
cmp rdi, rsi
je 2f
mov rsi, QWORD PTR [r8]
cmp rdi, rsi
je 2f
call go_systemstack_switch
mov QWORD PTR fs:[0xfffffff8], rsi
mov rsp, QWORD PTR [rsi+0x38]
sub rsp, 0x40
and rsp, 0xfffffffffffffff0
mov QWORD PTR [rsp+0x30], rdi
mov rdi, QWORD PTR [rdi+0x8]
sub rdi, rdx
mov QWORD PTR [rsp+0x28],rdi
mov rsi, rbx
mov rdx, r10
mov rdi, rax
call c_abi_syscall_handler
在将参数保存在一些未触及的寄存器中之后,我们调用系统堆栈上的处理程序,并打乱寄存器/堆栈数据以匹配 Go 的期望,主要是返回参数到堆栈中的特定位置。
mov QWORD PTR [rsp+0x28], -0x1
mov QWORD PTR [rsp+0x30], 0x0
neg rax
mov QWORD PTR [rsp+0x38], rax
xorps xmm15, xmm15
mov r14, QWORD PTR FS:[0xfffffff8]
ret
3:
mov QWORD PTR [rsp+0x28], rax
mov QWORD PTR [rsp+0x30], 0x0
mov QWORD PTR [rsp+0x38], 0x0
xorps xmm15, xmm15
mov r14, QWORD PTR FS:[0xfffffff8]
ret
在将所有系统调用绕道与 mirrord 拼接在一起之后 ABI0
,让我们看看事情是否按预期工作。
running the gin server with the rawsyscall hook
成功!
此处[13]提供了所有挂钩的完整实现。
我们决定不处理 Go 所做的非阻塞更改,主要是因为它对我们的用例并不重要(“一点延迟”对于我们尝试通过 mirrord 提供的值并不重要)。不过,我们计划稍后解决它。
[1]mirrord 通过将系统调用挂接到操作系统: https://metalbear.co/blog/mirrord-internals-hooking-libc-functions-in-rust-and-fixing-bugs/
[2]Go 在 Linux 上不使用 libc: https://lwn.net/Articles/771441/
[3]Frida: https://www.frida.re/
[4]trampoline: https://metalbear.co/blog/hooking-go-from-rust-hitchhikers-guide-to-the-go-laxy/
[5]Go 二进制文件: https://github.com/metalbear-co/mirrord/blob/main/tests/go-e2e/main.go
[6]通过使用Ghidra: https://github.com/NationalSecurityAgency/ghidra
[7]向后兼容性: https://go.googlesource.com/proposal/+/master/design/27539-internal-abi.md
[8]asm_linux_amd64.s: https://go.googlesource.com/go/+/c0d6d33/src/syscall/asm_linux_amd64.s
[9]Naked 功能特性: https://rust-lang.github.io/rfcs/2972-constrained-naked.html
[10]设计文档中所述: https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit
[11]这个: https://cs.opensource.google/go/go/+/master:src/runtime/stack.go
[12]runtime/cgocall.go: https://go.dev/src/runtime/cgocall.go
[13]此处: https://github.com/metalbear-co/mirrord/blob/main/mirrord-layer/src/go_hooks.rs
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/vBy4Y5Xds6OYExMa1nbe9g
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。