trailofbits/go-panikint
GitHub: trailofbits/go-panikint
一个修改版 Go 编译器,在编译期注入运行时检测,自动捕获整数溢出、下溢和类型截断问题并触发 panic。
Stars: 42 | Forks: 3
# 注意
**⚠️ go-panikint 正在被整合进 [gosentry](https://github.com/kevin-valerio/gosentry),这是一个专注于安全的 Go 工具集。它集成了 LibAFL fuzzing、go-panikint 等功能。如果你打算使用 go-panikint,应该先看看 [gosentry](https://github.com/kevin-valerio/gosentry)。**
## Go-Panikint
[](https://github.com/trailofbits/go-panikint/actions/workflows/go.yml)
### 概述
`Go-Panikint` 是 Go 编译器的一个修改版本,它为整数算术运算添加了**自动溢出/下溢检测**,并为整数转换添加了**类型截断检测**。当检测到溢出或截断时,会触发一个包含详细错误信息的 **panic**,其中包括具体的操作类型和涉及的整数类型。
**算术运算**:处理有符号和无符号整数类型的加法 `+`、减法 `-`、乘法 `*` 和除法 `/`。对于有符号整数,涵盖 `int8`、`int16`、`int32`。对于无符号整数,涵盖 `uint8`、`uint16`、`uint32`、`uint64`。除法情况专门检测有符号整数的 `MIN_INT / -1` 溢出条件。`int64` 和 `uintptr` 不进行算术运算检查。
**类型截断检测**:检测当目标类型的范围小于源类型时,整数类型转换是否会导致数据丢失。涵盖所有整数类型:`int8`、`int16`、`int32`、`int64`、`uint8`、`uint16`、`uint32`、`uint64`。由于平台相关的用法,排除 `uintptr`。默认**禁用**。
### 使用和安装:
```
# 克隆、更改目录并编译编译器
git clone https://github.com/trailofbits/go-panikint && cd go-panikint/src
# 启用所有功能编译
./make.bash
# 分叉编译器根目录的完整路径
export GOROOT=/path/to/go-panikint
# 编译并运行 Go 程序
./bin/go run test_simple_overflow.go
###### # 或者:构建启用截断检测的编译器
GOFLAGS="-gcflags=-truncationdetect=true" ./make.bash
# 仅 Fuzz
./bin/go test -fuzz=FuzzIntegerOverflow -v
```
### 它是如何工作的?
#### 具体做了什么?
我们基本上修改了 Go 编译器的中间表示 (IR) 部分,以便在每个数学操作数(即 `OADD`、`OMUL`、`OSUB`、`ODIV` 等)和类型转换上,编译器不仅添加执行操作的 IR 操作码,而且还**插入**一堆算术错误检查并插入带有详细错误信息的 panic 调用。Panic 消息包括具体的操作类型(例如,“integer overflow in int8 addition operation”、“integer truncation: uint16 cannot fit in uint8”)。这段代码最终会变成应用程序的二进制代码(汇编),因此请谨慎使用。
下面是一个 Ghidra 反编译加法 `+` 的例子:
```
if (*x_00 == '+') {
val = (uint32)*(undefined8 *)(puVar9 + 0x60);
sVar23 = val + sVar21;
puVar17 = puVar9 + 8;
if (((sdword)val < 0 && sVar21 < 0) && (sdword)val < sVar23 ||
((sdword)val >= 0 && sVar21 >= 0) && sVar23 < (sdword)val) {
runtime.panicoverflow(); // <-- panic if overflow caught
}
goto LAB_1000a10d4;
}
```
#### 为什么我们使用基于源代码位置的过滤?
正如在 `src/cmd/compile/internal/ssagen/ssa.go` 中实现的那样,我们对溢出检测应用了基于源代码位置的过滤。这确保了溢出检测仅应用于用户代码和目标应用程序(例如外部代码库的安全审计),同时排除标准库和第三方依赖。
每个算术运算(`intAdd`、`intSub`、`intMul`、`intDiv`)都使用 `n.Pos()` 和 `base.Ctxt.PosTable.Pos(pos).Filename()` 检查实际的源文件位置。来自包含 `/go-panikint/src/`、`/pkg/mod/`、`/vendor/` 的文件的操作会被自动排除,并且标准库包(`runtime`、`sync`、`os`、`syscall` 等)/ 内部包(`internal/*`)在编译器构建期间被排除。
### 抑制假阳性
在操作所在的同一行或紧邻的上一行添加注释标记,以将 bug 标记为假阳性,这样编译器就不会在算术或截断问题上发生 panic:
- 溢出/下溢:`overflow_false_positive`
- 截断:`truncation_false_positive`
示例:
```
// This is an overflow, but it's on purpose so we don't care flagging it
// overflow_false_positive
intentional_overflow := a + b
// Same for my buggy truncation
// truncation_false_positive
x := uint8(big)
// Also work on the same line
sum2 := a + b // overflow_false_positive
x2 := uint8(big) // truncation_false_positive
```
有时这可能不起作用,那是因为 Go 正在内联该函数。如果 `// overflow_false_poistive` 不起作用,请在函数签名前添加 `//go:noinline`。
### 测试
你可以使用以下命令运行 `tests/` 中的测试套件:
```
cd tests/;
GOROOT=/path/to/go-panikint /path/to/go-panikint/bin/go test -v .
```
CI 说明:
- Pull request 和推送会运行 workflow linting(actionlint + zizmor)、`go vet` 以及使用构建的工具链在 `tests/` 中进行类型检查(`go test -run=^$`)。
- Pull request 和推送会运行构建 + 测试(包括禁用/启用截断两种情况)。
- 编译器可能会生成许多相同的 panic 消息字符串;字符串符号发出是串行化的,以避免罕见的 `go:string."..." redeclared` 构建失败。
注意:`$GOROOT/test` 中的上游 Go 发行版测试有意依赖于整数回绕。
go-panikint 单元测试位于 `$GOROOT/tests`(复数)中,应在启用溢出检查的情况下运行。
当运行 `cmd/internal/testdir` 时(例如通过 `src/all.bash`),我们使用 `GOPANIKINT_DISABLE_OVERFLOW=1` 禁用溢出检测。
### Pre-commit (gofmt)
如果你使用 `pre-commit` 或 `prek`,此仓库包含一个最小的 `.pre-commit-config.yaml`,它会对暂存的 Go 文件运行 `gofmt`。该钩子跳过 `test/` 和任何 `testdata/` 路径,因为这些固件在 Go 仓库中故意不进行 gofmt 格式化。
示例:
```
prek run --all-files
```
### 示例
#### 示例 1(有符号整数溢出):
```
package main
import "fmt"
func main() {
fmt.Println("Testing signed integer overflow detection...")
// Test int8 addition overflow
var a int8 = 127
var b int8 = 1
fmt.Printf("Before: a=%d, b=%d\n", a, b)
result := a + b // Should panic with "integer overflow in int8 addition operation"
fmt.Printf("After: result=%d\n", result)
}
```
#### 示例 2(无符号整数溢出):
```
package main
import "fmt"
func main() {
fmt.Println("Testing unsigned integer overflow detection...")
// Test uint8 addition overflow
var a uint8 = 255
var b uint8 = 1
fmt.Printf("Before: a=%d, b=%d\n", a, b)
result := a + b // Should panic with "integer overflow in uint8 addition operation"
fmt.Printf("After: result=%d\n", result)
}
```
**预期输出(对于有符号和无符号溢出):**
```
bash-5.2$ GOROOT=/path/to/go-panikint && ./bin/go run test_overflow.go
Testing overflow detection...
Before: a=127, b=1
panic: runtime error: integer overflow in int8 addition operation
goroutine 1 [running]:
main.main()
/path/to/go-panikint/test_overflow.go:12 +0xfc
exit status 2
```
#### 示例 3(类型截断):
```
package main
import "fmt"
func main() {
fmt.Println("Testing type truncation detection...")
var u16 uint16 = 256
fmt.Printf("Before: u16=%d\n", u16)
result := uint8(u16) // Should panic with "integer truncation: uint16 cannot fit in uint8"
fmt.Printf("After: result=%d\n", result)
}
```
**预期输出:**
```
bash-5.2$ GOROOT=/path/to/go-panikint && ./bin/go run test_truncation.go
Testing type truncation detection...
Before: u16=256
panic: runtime error: integer truncation: uint16 cannot fit in uint8
goroutine 1 [running]:
main.main()
/path/to/go-panikint/test_truncation.go:10 +0xfc
exit status 2
```
#### 示例 4(fuzzing):
**Fuzzing harness:**
```
package fuzztest
import "testing"
func FuzzIntegerOverflow(f *testing.F) {
f.Fuzz(func(t *testing.T, a, b int8) {
result := a + b
t.Logf("%d + %d = %d", a, b, result)
})
}
```
**输出:**
```
GOROOT=/path/to/go-panikint/go-panikint ../bin/go test -fuzz=FuzzIntegerOverflow -v
=== RUN FuzzIntegerOverflow
fuzz: elapsed: 0s, gathering baseline coverage: 0/15 completed
fuzz: elapsed: 0s, gathering baseline coverage: 9/15 completed
--- FAIL: FuzzIntegerOverflow (0.03s)
--- FAIL: FuzzIntegerOverflow (0.00s)
testing.go:1822: panic: runtime error: integer overflow in int8 addition operation
goroutine 23 [running]:
runtime/debug.Stack()
/path/to/go-panikintgo-panikint/src/runtime/debug/stack.go:26 +0xc4
testing.tRunner.func1()
/path/to/go-panikintgo-panikint/src/testing/testing.go:1822 +0x220
panic({0x100e27b00?, 0x100fa4d60?})
/path/to/go-panikintgo-panikint/src/runtime/panic.go:783 +0x120
fuzztest.FuzzIntegerOverflow.func1(0x0?, 0x0?, 0x0?)
/path/to/go-panikintgo-panikint/fuzz_test/fuzz_test.go:10 +0xf8
reflect.Value.call({0x100e24ca0?, 0x100e5d5f8?, 0x14000093e28?}, {0x100dba042, 0x4}, {0x1400012a180, 0x3, 0x0?})
/path/to/go-panikintgo-panikint/src/reflect/value.go:581 +0x960
reflect.Value.Call({0x100e24ca0?, 0x100e5d5f8?, 0x14000154000?}, {0x1400012a180?, 0x100e5ce40?, 0x100d2abf3?})
/path/to/go-panikintgo-panikint/src/reflect/value.go:365 +0x94
testing.(*F).Fuzz.func1.1(0x140001028c0?)
/path/to/go-panikintgo-panikint/src/testing/fuzz.go:341 +0x258
testing.tRunner(0x140001028c0, 0x14000152000)
/path/to/go-panikintgo-panikint/src/testing/testing.go:1931 +0xc8
created by testing.(*F).Fuzz.func1 in goroutine 7
/path/to/go-panikintgo-panikint/src/testing/fuzz.go:328 +0x4a4
Failing input written to testdata/fuzz/FuzzIntegerOverflow/8a8466cc4de923f0
To re-run:
go test -run=FuzzIntegerOverflow/8a8466cc4de923f0
```
标签:DNS 反向解析, EVTX分析, EVTX分析, EVTX分析, Go编译器, Go语言, 云安全监控, 内存安全, 整数溢出检测, 日志审计, 程序插桩, 程序破解, 类型截断, 编译器修改, 运行时检查, 防御性编程, 静态分析