前言
go 經曆了幾次大的更新,自從去年 1.18 版本之後 go 更是官方釋出了 fuzz 功能。已經有人寫了go語言原生模糊測試:源碼分析和實戰,并且把源碼都講解的很明白。官方又出了一些新功能,比如已經原生支援生成語料庫,将檔案轉成适合 go fuzz 的格式。
昨天跑了一下,找到了兩處 bug :第一個是官方的webp,第二個是第三方庫iprange
webp
自生成語料庫
在官方文檔中,提供了 file2fuzz 的方法,通過該方法來讓語料庫适配 go 原生 fuzz:
$ go install golang.org/x/tools/cmd/[email protected]
$ file2fuzz
可以轉單個檔案,也可以批量轉檔案夾:
go-fuzz-corpus 這個語料庫裡涵蓋非常全面。非常容易找到自己想 fuzz 的檔案格式,而且這個庫給的語料相當多樣(原本是專門為 go-fuzz 提供的語料庫:
比如到它的一個具體檔案格式的檔案夾下:❯ file2fuzz -o Fuzz*** corpus/*,其中這個Fuzz***是 fuzz 函數具體名稱,之後要把這個檔案夾丢到 go fuzz 的工作目錄:比如我的就是這樣的:mv Fuzz*** /Users/yourusername/Desktop/justfu/testdata/fuzz,其中testdata/fuzz格式固定,相當于justfu是真正的工作目錄
找個檔案看下檔案格式:
可以官方已經把我們需要的檔案轉成了 byte 格式,之後在我們的 fuzz 邏輯裡邊就可以自由發揮了:
package test
import (
"bytes"
// "strings"
// "fmt"
"golang.org/x/image/webp"
"testing"
)
func FuzzReverse(f *testing.F) {
f.Fuzz(func(a *testing.T, data []byte) {//接收參數
_, err := webp.DecodeConfig(bytes.NewReader(data))//解析參數
if err != nil {//沒錯就正常運作,有錯誤就直接退出
return
}
if _, err := webp.Decode(bytes.NewReader(data)); err != nil {//解析參數
return
}
})
}
❯ go test -fuzz=Fuzz
fuzz: elapsed: 1s, gathering baseline coverage: 0/1603 completed
fuzz: elapsed: 4s, gathering baseline coverage: 1602/1603 completed
fuzz: elapsed: 7s, gathering baseline coverage: 1602/1603 completed
fuzz: elapsed: 10s, gathering baseline coverage: 1602/1603 completed
這裡 fuzz 就看人品了,我試過有時候 fuzz 幾個小時沒有東西,有時候一個小時就直接 crash 了。我直接把讓程式 hang 的檔案提取出來了,寫了個驗證程式:
package main
//main.go
import (
"bytes"
"fmt"
"golang.org/x/image/webp"
)
func main() {
data := []byte("RIFF0000WEBPVP8X\n\x00\x00\x000000000000ALPH\xf0\xffx\x00D")
cfg, err := webp.DecodeConfig(bytes.NewReader(data))
if err != nil {
return
}
fmt.Println(cfg.Width)
fmt.Println(cfg.Height)
if _, err := webp.Decode(bytes.NewReader(data)); err != nil {
return
}
}
❯ go run main.go
3158065
3158065
3158064//這塊是我改的源碼,就是底下的 Println
3158064
fatal error: out of memory allocating heap arena metadata
研究了半天 webp 檔案格式,發現 fuzz 出來的檔案一堆 0 那塊,代表了 heightMinusOne、widthMinusOne 的大小,然後 alpha 沒有進行驗證,直接 make 這麼大的記憶體空間。
修複該 bug 的話:我想法是給個限制範圍,通過 runtime 包把記憶體資訊拿到,然後做判斷。官方沒有類似的讨論,我就去翻了翻 go 的 image 庫,發現已經在2月份 PR 了:https://github.com/golang/image/pull/14。該想法是:加個傳資料的邏輯,把直接 make 改成檔案流的形式,如果 chunkdata 就是假的(損壞的檔案,那就會失敗。不然就正常進行。
iprange
語料庫寫死到代碼中
這個是當時寫掃描器的時候,用到的一個庫,它能夠從 nmap 格式的字元串中解析出 IPv4 位址,作者給的這個用法寫的很明白:
我們稍微修改下提供的用法,就可以結合 Fuzz 把 bug 給掃出來:看下整體 fuzz 代碼:
package main//ip_test.go,必須以test結尾
import (
"github.com/malfunkt/iprange"
"testing"
)
func FuzzReverse(f *testing.F) {
testcases := []string{"10.0.0.1", "10.0.0.5-10", "192.168.1.*", "192.168.10.0/24"}
for _, tc := range testcases {//直接進語料庫
f.Add(tc)
}
f.Fuzz(func(t *testing.T, orig string) {//接收參數
_, err1 := iprange.ParseList(orig)//解析參數
if err1 != nil {
return//如果報錯的話讓他直接傳回
}
})
}
逐行掃一眼代碼:
- import 的包,需要包含項目所必須的包,還有個 testing
- 就是一個 fuzz 代碼:将10.0.0.1..進語料庫Fuzz 的參數,在這裡就是一個 string ,保證 iprange.ParseList 接收到正确參數即可
直接運作,相信一下就能跑出結果:
❯ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/4 completed
fuzz: elapsed: 0s, gathering baseline coverage: 4/4 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 136078 (45344/sec), new interesting: 48 (total: 52)
fuzz: elapsed: 6s, execs: 136078 (0/sec), new interesting: 48 (total: 52)
fuzz: elapsed: 9s, execs: 168410 (10777/sec), new interesting: 50 (total: 54)
fuzz: elapsed: 12s, execs: 304898 (45509/sec), new interesting: 51 (total: 55)
fuzz: minimizing 37-byte failing input file
fuzz: elapsed: 15s, minimizing
fuzz: elapsed: 15s, minimizing
--- FAIL: FuzzReverse (15.30s)
--- FAIL: FuzzReverse (0.00s)
testing.go:1485: panic: runtime error: index out of range [3] with length 0
goroutine 43099 [running]:
runtime/debug.Stack()
/usr/local/go/src/runtime/debug/stack.go:24 +0xbc
testing.tRunner.func1()
/usr/local/go/src/testing/testing.go:1485 +0x264
panic({0x100253da0, 0x1400672cd08})
/usr/local/go/src/runtime/panic.go:884 +0x204
encoding/binary.bigEndian.Uint32(...)
/usr/local/go/src/encoding/binary/binary.go:157
github.com/malfunkt/iprange.(*ipParserImpl).Parse(0x14007881200, {0x100265128?, 0x14007878640?})
yaccpar:351 +0x2888
github.com/malfunkt/iprange.ipParse(...)
yaccpar:153
github.com/malfunkt/iprange.ParseList({0x14007807a66?, 0x0?})
ip.y:93 +0xcc
m.FuzzReverse.func1(0x1400c291718?, {0x14007807a66?, 0x0?})
/Users/housihan/Desktop/sensor/bigdata/waf2/m_test.go:14 +0x54
reflect.Value.call({0x100230ba0?, 0x1002637a0?, 0x1400008ee38?}, {0x1001da54a, 0x4}, {0x140078840c0, 0x2, 0x0?})
/usr/local/go/src/reflect/value.go:586 +0x87c
reflect.Value.Call({0x100230ba0?, 0x1002637a0?, 0x140000100c0?}, {0x140078840c0?, 0x100262e20?, 0x10032e670?})
/usr/local/go/src/reflect/value.go:370 +0x90
testing.(*F).Fuzz.func1.1(0x14000010360?)
/usr/local/go/src/testing/fuzz.go:335 +0x360
testing.tRunner(0x14007882340, 0x14007863320)
/usr/local/go/src/testing/testing.go:1576 +0x10c
created by testing.(*F).Fuzz.func1
/usr/local/go/src/testing/fuzz.go:322 +0x4c4
Failing input written to testdata/fuzz/FuzzReverse/f5de87321fd8b751
To re-run:
go test -run=FuzzReverse/f5de87321fd8b751
FAIL
exit status 1
FAIL m 16.590s
然後我們檢視一下引發崩潰的字元串(也就是上邊的testdata/fuzz/FuzzReverse/f5de87321fd8b751):
go test fuzz v1
string("0.0.0.0/70")
其将掩碼改成了 70,我們的 fuzz 邏輯已經把 err 給判斷了,發現程式沒有報錯反而正常運作,之後直接引發了一個 panic。
順着調用棧到源碼裡一探究竟。我在這把/usr/local/go/src/encoding/binary/binary.go源碼改了下,檢視具體變量。這塊代碼很不好跟,非常容易跑飛:
func (bigEndian) Uint32(b []byte) uint32 {
fmt.Println(b)//列印一下
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24
}
❯ go run main.go
[]//求他的b[3]必然直接索引越界
panic: runtime error: index out of range [3] with length 0
goroutine 1 [running]:
encoding/binary.bigEndian.Uint32({}, {0x0, 0x0, 0x10423dda8?})
/usr/local/go/src/encoding/binary/binary.go:159 +0x88
github.com/malfunkt/iprange.(*ipParserImpl).Parse(0x140000c6000, {0x1041d03c8?, 0x140000bc050?})
yaccpar:351 +0x12f8
github.com/malfunkt/iprange.ipParse(...)
yaccpar:153
github.com/malfunkt/iprange.ParseList({0x10418ced9?, 0x140000021a0?})
ip.y:93 +0xa0
main.main()
/Users/housihan/Desktop/sensor/bigdata/waf2/main.go:10 +0x28
exit status 2
倒着推發現它用到了 net 的 CIDRMask 方法,去自己的/usr/local/go/src/看看 ones 和 bits:
// CIDRMask returns an IPMask consisting of 'ones' 1 bits
// followed by 0s up to a total length of 'bits' bits.
// For a mask of this form, CIDRMask is the inverse of IPMask.Size.
func CIDRMask(ones, bits int) IPMask {
fmt.Println(ones)
fmt.Println(bits)
if bits != 8*IPv4len && bits != 8*IPv6len {
return nil
}
if ones < 0 || ones > bits {//直接return nil
return nil
}
l := bits / 8
m := make(IPMask, l)
n := uint(ones)
for i := 0; i < l; i++ {
if n >= 8 {
m[i] = 0xff
n -= 8
continue
}
m[i] = ^byte(0xff >> n)
n = 0
}
return m
}
❯ go run main.go
70
32
panic: runtime error: index out of range [3] with length 0
可以看到,隻要是大于 32 事實上是直接傳回 nil 的,是以會失敗。我看了看寫的 test 代碼,沒考慮掩碼大于 32 的情況,比如直接把 10.0.0.1/33 送進去就會報錯:
package main
import (
"log"
"github.com/malfunkt/iprange"
)
func main() {
list, err := iprange.ParseList("10.0.0.1/33")
if err != nil {
log.Printf("error: %s", err)
}
log.Printf("%+v", list)
rng := list.Expand()
log.Printf("%s", rng)
}
❯ go run main.go
panic: runtime error: index out of range [3] with length 0
goroutine 1 [running]:
encoding/binary.bigEndian.Uint32(...)
/usr/local/go/src/encoding/binary/binary.go:157
github.com/malfunkt/iprange.(*ipParserImpl).Parse(0x140001a4000, {0x102adc3c8?, 0x14000198050?})
yaccpar:351 +0x14bc
github.com/malfunkt/iprange.ipParse(...)
yaccpar:153
github.com/malfunkt/iprange.ParseList({0x102a98b79?, 0x140000021a0?})
ip.y:93 +0xa0
main.main()
/Users/housihan/Desktop/sensor/bigdata/waf2/main.go:10 +0x28
exit status 2
這個問題在18年就提出來了:https://github.com/malfunkt/iprange/issues/1。改起來可以先判斷掩碼,如果大于的話直接就報錯然後 return ,或者強行限制到 32。
我給 y.go 的 502 行加了個判斷:
總結
go 原生的 fuzz 在快速發展,目前 github 上的讨論也十分活躍。但仍存在諸多限制,比如一個測試程式跑出 panic 會導緻所有運作的測試程式一起掉。之後想重新跑還得先把造成 crash 的檔案提取出來,再繼續不然每次開始測試的時候因為每次都會去掃一遍 testdata 裡的東西,還是會掉。
from: https://xz.aliyun.com/t/12611