GO FUZZING入手

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 导致越界索引。


文章作者: hh
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hh !
  目录