circle2 发表于 2024-9-29 02:18

解决 redress 分析 gomobile so 失败的问题

# 前言

最近想分析一个 Android App,发现它的关键逻辑在 `libgojni.so` 中,推测是 go 相关的工具做的,查询资料后,发现了 `gomobile`。(https://go.dev/wiki/Mobile) 是一个可以将 go 程序打包为 android、iOS app/lib 的工具,它有两种模式

1. 直接构建 App
2. 构建一个 lib,供现有的 App 使用

第二种方式复用现有的go程序还是挺方便的,它会打包出一个 aar 文件,解压出来确实可以看到 `libgojni.so`,测试对比了一下,确实差不多。

找到生成的方式后,就是找现成的分析工具,(https://github.com/goretk/redress) 是一个比较好的分析工具,可以查看 `struct`, `interface` 等信息,查看 `gomobile` 生成的
`libgojni.so` 中的 `struct` 信息会报错。在使用 IDA 做了一些分析后,感觉有 `struct` 信息还是会对分析有帮助的,而且分析的过程中也了解了一些 go 的知识(不得不说,go 还是挺好上手的),
所以又转向了 `redress`,准备解决其报错。目前已经解决报错的问题,可以正常分析了。

下面分享一下解决报错的方式,下面的错误是我使用 `./redress types struct ./libgojni.so` 依次出现并解决的错误

# no goversion found

搜索错误信息,最终定位到相应的位置

``` text
gore/goversion.go
94-   }
95-   notfound := false
96-   for !notfound {
97-             version := matchGoVersionString(data)
98-             if version == "" {
99:                     return nil, ErrNoGoVersionFound
100-            }
101-            ver := ResolveGoVersion(version)
102-            // Go before 1.4 does not have the version string so if we have found
103-            // a version string below 1.4beta1 it is a false positive.
104-            if ver == nil || GoVersionCompare(ver.Name, "go1.4beta1") < 0 {
```

输出了一下 `version` ,发现实际是找到了的,只是在 `ResolveGoVersion` 的时候没有匹配到,它是在一堆 go version 信息中找的(`gore/goversion_gen.go`),样本中使用的是 1.20.14,它里面没有这么新的版本。

不过它是生成的,使用的是 `gore/gen.go`,执行下面命令即可,要先切换到gore目录下,因为它会将部分产物放在当前目录

``` bash
cd gore
go run gen.go
```

# failed to parse the module data: section does not exist

报错位置在这里,`parseModuleData` 出错

``` text
gore/type.go
62-
63-   md, err := parseModuledata(fileInfo, f)
64-   if err != nil {
65:             return nil, fmt.Errorf("failed to parse the module data: %w", err)
66-   }
67-
```

parseModuledata -> findModuledata,出问题的是 `getPCLNTABData`,这个 f 是根据分析的二进制文件不同,走的逻辑不同,这里走的是 `gore/elf.go` 中的

``` go
func findModuledata(f fileHandler) ([]byte, error) {
        _, secData, err := f.getSectionData(f.moduledataSection())
        if err != nil {
                return nil, err
        }
        tabAddr, _, err := f.getPCLNTABData()
        if err != nil {
                return nil, err
        }
    // ....
}
```

`gore/elf.go` 中,可以看到是要找 `.gopclntab` 或者是 `.data.rel.ro.gopclntab` section,gomobile生成的文件中确实没有这个段,但是在使用 IDA 分析的时候,我还见过 pclntab 这个字眼

``` go
func (e *elfFile) getPCLNTABData() (uint64, []byte, error) {
        start, data, err := e.getSectionData(".gopclntab")
        if err == ErrSectionDoesNotExist {
                // Try PIE location
                return e.getSectionData(".data.rel.ro.gopclntab")
        }
        return start, data, err
}
```

在 `redress` 中搜索 pclntab,其中120magic这个 `F1, FF, FF, FF` 就非常符合在 IDA 中看到的数据,而这些 magic 数据只在 `gore/pe.go` 中使用

``` text
gore/pclntab.go
25:// pclntab12magic is the magic bytes used for binaries compiled with Go
27:var pclntab12magic = []byte{0xfb, 0xff, 0xff, 0xff, 0x0, 0x0}
29:// pclntab116magic is the magic bytes used for binaries compiled with
31:var pclntab116magic = []byte{0xfa, 0xff, 0xff, 0xff, 0x0, 0x0}
33:// pclntab118magic is the magic bytes used for binaries compiled with
35:var pclntab118magic = []byte{0xf0, 0xff, 0xff, 0xff, 0x0, 0x0}
37:// pclntab120magic is the magic bytes used for binaries compiled with
39:var pclntab120magic = []byte{0xf1, 0xff, 0xff, 0xff, 0x0, 0x0}
70:   for _, magic := range [][]byte{pclntab120magic, pclntab118magic, pclntab116magic, pclntab12magic} {
```

所以解决办法是,模仿 pe,给 elf 写这样一处逻辑即可,新增了 `searchFileForPCLNTabElf` 这样一个函数

``` go
// gore/elf.go
func (e *elfFile) getPCLNTABData() (uint64, []byte, error) {
        start, data, err := e.getSectionData(".gopclntab")
        if err == ErrSectionDoesNotExist {
                // Try PIE location
                addr, pclndat, err := searchFileForPCLNTabElf(e.file)
                return uint64(addr), pclndat, err
                // return e.getSectionData(".data.rel.ro.gopclntab")
        }
        return start, data, err
}

// gore/pclntab.go
func searchFileForPCLNTabElf(f *elf.File) (uint32, []byte, error) {
        for _, v := range []string{".data.rel.ro"} {
                sec := f.Section(v)
                if sec == nil {
                        continue
                }
                secData, err := sec.Data()
                if err != nil {
                        continue
                }
                tab, err := searchSectionForTab(secData)
                if err == ErrNoPCLNTab {
                        continue
                }
                // TODO: Switch to returning a uint64 instead.
                addr := uint32(sec.Addr) + uint32(len(secData) - len(tab))
                return addr, tab, err
        }
        return 0, []byte{}, ErrNoPCLNTab
}
```

# failed to parse the module data: could not find moduledata.

``` text
218-func findModuledata(f fileHandler) ([]byte, error) {
219-    _, secData, err := f.getSectionData(f.moduledataSection())
220-    if err != nil {
221-            return nil, err
222-    }
223-    tabAddr, _, err := f.getPCLNTABData()
224-    if err != nil {
225-            return nil, err
226-    }
227-
228-    // Search for moduledata
229-    buf := new(bytes.Buffer)
230-    err = binary.Write(buf, binary.LittleEndian, &tabAddr)
231-    if err != nil {
232-            return nil, err
233-    }
234-    off := bytes.Index(secData, buf.Bytes()[:intSize32])
235-    if off == -1 {
236:            return nil, errors.New("could not find moduledata")
```

secData 是 .noptrdata 段的数据,在 IDA 确实是有 tabAddr 的数据的,不过在输出之后,我发现这里面相应的位置是 0,通过 rizin 搜索,也是可以找到 tabAddr 对应的数据。在查找资料后,确认出现问题的是重定位

查看重定位的数据,发现确实有在相应位置塞 tabAddr 的信息,那么在 getSectionData 里,修改获取的数据即可

``` go
// gore/elf.go

func relaSectionData(e *elfFile, targetSec []byte, targetSecBase uint64) {
        relaSection := e.file.Section(".rela.dyn")
        data, err := relaSection.Data()
        if err != nil {
                fmt.Printf("Error reading section data: %+v\n", err)
                return
        }

        fmt.Printf("rela section data 0x%X -- 0x%X\n", targetSecBase, targetSecBase + uint64(len(targetSec)))

        entrySize := 24
        for i := 0; i < len(data) ; i += entrySize {
                offset := binary.LittleEndian.Uint64(data)
                // info := binary.LittleEndian.Uint64(data)
                addend := binary.LittleEndian.Uint64(data)

                if offset < targetSecBase || offset + 8 > targetSecBase + uint64(len(targetSec)) {
                        continue
                }
                secOffset := offset - targetSecBase
                // fmt.Printf("rela offset: 0x%x, info: 0x%x, addend: 0x%x\n", offset, info, addend)
                // fmt.Printf("secOffset: %d\n", secOffset)

                binary.LittleEndian.PutUint64(targetSec, addend)
        }
}

func (e *elfFile) getSectionData(name string) (uint64, []byte, error) {
        section := e.file.Section(name)
        if section == nil {
                return 0, nil, ErrSectionDoesNotExist
        }
        data, err := section.Data()

        // 使用relaSectionData方法重定向 .noptrdata 中的数据
        fmt.Println("get section data: " + name)
        if name == e.moduledataSection() {
                relaSectionData(e, data, section.Addr)
        }

        return section.Addr, data, err
}
```


# bytes.Reader.Seek: negative position.

出现了这样的报错:failed to parse type at offset 0x2fc00: failed to parse resolved type for 0x67f4e0: bytes.Reader.Seek: negative position.

问题出在这块代码中,遍历typeLink中的偏移,去解析相应位置的type,`types` 会传入 `newTypeParser`,后续在 `parseType` 时,会读取它的数据。

通过中在 `parseType` 的分析,发现是在分析 `child` 时取到的数据不正确,也就是从 `types` 中某个偏移取数据的时候,拿到的是错误的(这部分就不详细展开了,里面也不复杂)

``` go
        types, err := md.Types().Data()
        if err != nil {
                return nil, fmt.Errorf("failed to get types data section: %w", err)
        }

        typeLink, err := md.TypeLink()
        if err != nil {
                return nil, fmt.Errorf("failed to get type link data: %w", err)
        }

        fmt.Printf("md info %+v\n", md)
        // New parser
        parser := newTypeParser(types, md.Types().Address, fileInfo)
        for _, off := range typeLink {
                typ, err := parser.parseType(uint64(off) + parser.base)
                if err != nil || typ == nil {
                        return nil, fmt.Errorf("failed to parse type at offset 0x%x: %w", uint64(off) + parser.base, err)
                }
        }
        return parser.parsedTypes(), nil
```

看下返回 `types` 的 `Data` 方法,会用 `getSectionDataFromOffset` 方法获取 sectionData,然后再取出需要的部分

``` go
func (m ModuleDataSection) Data() ([]byte, error) {
        // If we don't have any data, return an empty slice.
        if m.Length == 0 {
                return []byte{}, nil
        }
        base, data, err := m.fh.getSectionDataFromOffset(m.Address)
        if err != nil {
                return nil, fmt.Errorf("getting module data section failed: %w", err)
        }
        start := m.Address - base
        if uint64(len(data)) < start+m.Length {
                return nil, fmt.Errorf("the length of module data section is to big: address 0x%x, base 0x%x, length 0x%x", m.Address, base, m.Length)
        }
        buf := make([]byte, m.Length)
        copy(buf, data)
        return buf, nil
}
```

猜测还是有重定向的部分,在该方法中增加 `relaSectionData` 修改数据,再次运行,发现这个错误已经绕过了

``` go
func (e *elfFile) getSectionDataFromOffset(off uint64) (uint64, []byte, error) {
        for _, section := range e.file.Sections {
                if section.Offset == 0 {
                        // Only exist in memory
                        continue
                }

                if section.Addr <= off && off < (section.Addr+section.Size) {
                        data, err := section.Data()
                        fmt.Printf("section start: %x\n", section.Addr)
                        relaSectionData(e, data, section.Addr)   // 这里增加
                        return section.Addr, data, err
                }
        }
        return 0, nil, ErrSectionDoesNotExist
}
```

# no gopclntab section found

这部分就相对简单了,和前面的错误是一样的,只不过前面是 getPCLNTABData,其实都是

``` go
func (e *elfFile) getPCLNTab() (*gosym.Table, error) {
        pclnSection := e.file.Section(".gopclntab")
        if pclnSection == nil {
                // No section found. Check if the PIE section exist instead.
                pclnSection = e.file.Section(".data.rel.ro.gopclntab")
        }
        if pclnSection == nil {
                // 通过search获取pclntab
                _, pclndat, err := searchFileForPCLNTabElf(e.file)
                if err != nil {
                        return nil, err
                }
                pcln := gosym.NewLineTable(pclndat, e.file.Section(".text").Addr)
          return gosym.NewTable(make([]byte, 0), pcln)
                // 注释之前的返回
                // return nil, fmt.Errorf("no gopclntab section found")
        }

        pclndat, err := pclnSection.Data()
        if err != nil {
                return nil, fmt.Errorf("could not get the data for the pclntab: %w", err)
        }

        pcln := gosym.NewLineTable(pclndat, e.file.Section(".text").Addr)
        return gosym.NewTable(make([]byte, 0), pcln)
}
```

# 总结

目前已经解决分析的样本中的 struct, interface 等信息了。总的来说,改动的其实并不复杂

1. 更新 `redress` 中列出的 go 的版本
2. 通过搜索 magic 数据来定位 `gopclntab` 部分,通过 gomobile 生成的确实是没有这个字段
3. 根据重定向段,修改获取的 section 的数据(这个有可能是我所分析的样本中特殊的部分)

光影由心 发表于 2024-9-29 14:56

感谢分享!

小草草 发表于 2024-10-1 09:13

感谢楼主的分享
页: [1]
查看完整版本: 解决 redress 分析 gomobile so 失败的问题