GO FUZZING
安装 go 环境
建议在服务器上搭建
docker pull fuzzitdev/golang:1.12.7-buster
FROM fuzzitdev/golang:1.12.7-buster
RUN apt-get update&&\
echo y|apt-get install openssh-server
RUN rm -f /etc/service/sshd/down&&\
sed -ri 's/^#?PermitRootLogin\s+.*/PermitRootLogin yes/' /etc/ssh/sshd_config&&\
groupadd test && \
useradd -g test test -m &&\
echo 'test:test' | chpasswd
ADD ./start.sh /etc/my_init.d/start.sh
RUN chmod +x /etc/my_init.d/start.sh
RUN /etc/my_init.d/start.sh
ADD ./heart_beat.sh /heart_beat.sh
RUN chmod +x /heart_beat.sh
EXPOSE 80
EXPOSE 22
CMD /etc/init.d/ssh start
CMD [ "/heart_beat.sh" ]
#heart_beat.sh
#!/bin/bash
sleep infinity;
/etc/profile
配置 golang 环境变量
export GOROOT=/usr/local/go
export GOBIN=$GOROOT/bin
#工作目录
export GOPATH=/go
export GOPROXY=https://goproxy.io
export PATH=$PATH:$GOPATH:$GOBIN:$GOROOT
升级 go 版本
从官方地址:https://golang.org/dl/ 下载
wget https://dl.google.com/go/go1.xx.tar.gz
tar -xzf go1.xx.tar.gz -C /usr/local
ln -s /usr/local/go/bin/* /usr/bin/
安装 go-fuzz
下载 https://github.com/dvyukov/go-fuzz
放在 $GOPATH/src/github.com/dvyukov/go-fuzz
go-fuzz提供的语料库 https://github.com/dvyukov/go-fuzz-corpus
放在 $GOPATH/src/github.com/dvyukov/go-fuzz-corpus
go install $GOPATH/src/github.com/dvyukov/go-fuzz/go-fuzz
go install $GOPATH/src/github.com/dvyukov/go-fuzz/go-fuzz-build
install完成后生成的可执行文件在$GOBIN 路径下
Fuzz 实战
要测试的是 iprange (https://github.com/malfunkt/iprange)
iprange是一个库,可用于从nmap格式的字符串中解析IPv4地址。它接收一个字符串,并返回一个“Min-Max”格式的列表。iprange支持以下格式:
10.0.0.1
10.0.0.0/24
10.0.0.*
10.0.0.1-10
10.0.0.1, 10.0.0.5-10, 192.168.1.*, 192.168.10.0/24
代码示例
iprange.ParseList 可以将 ip 格式的字符串转换成 AddressRangeList
package main
import (
"log"
"github.com/malfunkt/iprange"
)
func main() {
list, err := iprange.ParseList("10.0.0.1, 10.0.0.5-10, 192.168.1.*, 192.168.10.0/24")
if err != nil {
log.Printf("error: %s", err)
}
log.Printf("%+v", list)
rng := list.Expand()
log.Printf("%s", rng)
}
安装的库文件在 $GOPATH\src\github.com\malfunkt\iprange
目录下,
有漏洞版本的 commit id 如下
git reset --hard 3a31f5ed42d2d8a1fc46f1be91fd693bdef2dd52
编写 Fuzz 函数
在 $GOPATH/src/github.com/malfunkt/iprange
下创建 iprange_fuzz.go 文件,内容如下:
package iprange
func Fuzz(data []byte) int {
_, err := ParseList(string(data))
if err != nil {
return 0
}
return 1
}
我们把 go-fuzz随机生成的数据data转换为字符串,并传递给ParseList() 函数。如果解析器返回一个错误,那么就说明输入存在问题,将会 return 0。而如果它通过了检查,将会 return 1,这个正确输入也将被添加到原始语料库中。
执行 Fuzz
- 使用 go-fuzz-build 来生成 fuzzing zip 文件
运行如下命令,在当前文件夹生成 iprange-fuzz.zip 文件
$GOBIN/go-fuzz-build $GOPATH/src/github.com/malfunkt/iprange/
- 准备语料
为了进行有意义的 fuzz,我们需要尽可能提供格式正确的样本。
在文件夹 ./corpus
下创建语料文件,写入以下内容,文件名随意
提供 iprange 输入的格式数据可以更快的找到 crash 数据( fuzz 会通过你提供的语料以及 fuzz 函数返回的状态,变异输入的数据)
10.0.0.1
10.0.0.0/24
10.0.0.*
10.0.0.1-10
10.0.0.1, 10.0.0.5-10, 192.168.1.*, 192.168.10.0/24
- 运行 fuzzer
$GOBIN/go-fuzz -bin=./iprange-fuzz.zip -workdir=.
运行 fuzzer 后,看到 crashers 的数量出现1
workers: 1, corpus: 37 (0s ago), crashers: 1, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s
workers: 1, corpus: 38 (2s ago), crashers: 1, restarts: 1/0, execs: 0 (0/sec), cover: 384, uptime: 6s
workers: 1, corpus: 38 (5s ago), crashers: 1, restarts: 1/981, execs: 7849 (872/sec), cover: 393, uptime: 9s
在 crashers 目录下查看文件,发现导致崩溃的原因
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:112
github.com/malfunkt/iprange.(*ipParserImpl).Parse(0xc000097800, 0x53fde0, 0xc0000581e0, 0x0)
/go/src/github.com/malfunkt/iprange/y.go:504 +0x29d5
github.com/malfunkt/iprange.ipParse(...)
/go/src/github.com/malfunkt/iprange/y.go:306
github.com/malfunkt/iprange.ParseList(0xc000041e78, 0xa, 0xa, 0xa, 0xc000041e78, 0xa, 0xc000041e98)
/go/src/github.com/malfunkt/iprange/y.go:61 +0x127
github.com/malfunkt/iprange.Fuzz(0x7f9f0e6f5000, 0xa, 0xa, 0x3)
/go/src/github.com/malfunkt/iprange/iprange_fuzz.go:4 +0x7d
go-fuzz-dep.Main(0xc000041f70, 0x1, 0x1)
go-fuzz-dep/main.go:36 +0x1ad
main.main()
github.com/malfunkt/iprange/go.fuzz.main/main.go:15 +0x52
exit status 2
分析Crash
综合以上 crash 文件可以知道,问题是程序存在超过索引范围读取数据的情况,接下来就依次看 dump 中提到的函数。
- encoding/binary.bigEndian.Uint32
- Parse
bigEndian.Uint32
首先是encoding/binary/binary.bigEndian.Uint32,它是 go 的标准库。源码在这里
https://github.com/golang/go/blob/master/src/encoding/binary/binary.go#L110
func (bigEndian) Uint32(b []byte) uint32 {
_ = 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
}
可以看出函数传入的 b 参数,如果没有满足四位的长度,它将在字节被访问时发生 panic 异常
Parse
而 iprange.Parse 调用了 bigEndian.Uint32 的函数,找到对应的调用地方
case 5:
ipDollar = ipS[ippt-3 : ippt+1]
//line ip.y:54
{
mask := net.CIDRMask(int(ipDollar[3].num), 32)
min := ipDollar[1].addrRange.Min.Mask(mask)
maxInt := binary.BigEndian.Uint32([]byte(min)) +
0xffffffff -
binary.BigEndian.Uint32([]byte(mask))
maxBytes := make([]byte, 4)
binary.BigEndian.PutUint32(maxBytes, maxInt)
maxBytes = maxBytes[len(maxBytes)-4:]
max := net.IP(maxBytes)
ipVAL.addrRange = AddressRange{
Min: min.To4(),
Max: max.To4(),
}
}
传入 bigEndian.Uint32 的参数是 min,min 来自于 mask,而mask 又来自于 net.CIDRMask。
查看官方文档 https://golang.org/pkg/net/#CIDRMask ,net.CIDRMask 返回的 IPMask 格式的数据,前面有 “ones” 个1,后面跟0,类似子网掩码的二进制数值,
查看源码
func CIDRMask(ones, bits int) IPMask {
if bits != 8*IPv4len && bits != 8*IPv6len {
return nil
}
if ones < 0 || ones > bits {
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
}
如果如果传入参数 ones 无效,函数将会返回nil,从而使得 bigEndian.Uint32 函数 panic,所以当 ones 大于 32 的时候,满足条件。
结论
输入 ip 数据的子网掩码大于最大的32位时,会导致程序出现 crash
最后用 fuzz 时导致 crash 的输入复现调试一下
package main
import "github.com/malfunkt/iprange"
func main() {
_ = Fuzz([]byte("0.0.0.0/70"))
}
func Fuzz(data []byte) int {
_, err := iprange.ParseList(string(data))
if err != nil {
return 0
}
return 1
}
输入数据中的 70 传递给 net.CIDRMask ,会导致 mask 为 nil ,进而导致 min 也为 nil,min 参数传入bigEndian.Uint32 导致越界索引。