我们尝试编写一些go的程序并逆向。
编写go的程序的原则呢就是要有一些基本的数据结构,然后编写逆向热门的接口。
在菜鸟教程上找了一点实例代码
package main
import "fmt"
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
fmt.Println("Hello, World!")
var numbers = make([]int,3,5)
fmt.Printf("len=%d cap=%d slice=%v\n",len(numbers),cap(numbers),numbers)
var n [10]int
var i,j int
for i = 0; i < 10; i++ {
n[i] = i + 100
}
for j = 0; j < 10; j++ {
fmt.Printf("Element[%d] = %d\n", j, n[j] )
}
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
fmt.Println("原始地图")
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}
delete(countryCapitalMap, "France")
fmt.Println("法国条目被删除")
fmt.Println("删除元素后地图")
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}
直接编译成二进制程序
我们之前在研究函数调用的时候用了这个
go build -gcflags "-N -l" 1.go
做了简化 禁止了优化、内联
我们在这里尝试直接编译
go build 1.go
右面这个是经过内联的
确实我们之前研究接口的时候为了观察调用规则
不内联才更好的观察
但是在咱这次的代码中
显然是内联过后去读汇编很多地方更直观
我们就读内联的。
当然用gdb也可以吧 gdb可以给出调用函数的时候各个内存其里的值
而且go的编译会自带-g选项
在本地编译的在本地调的时候可以自动载入源码
看着源码来。
当然其实gdb用dir命令载入源码也完全可以。
那正文开始
第一句话当然是输出hello_word
内联之后直接就调了Fprintln
我们直接去查看调用Fprintln时候进去的处理过程
就能判断参数是啥。
显然调用fmt_newPrinter用的就是这五个参数
看一眼寄存器
但是我们仍然不是很清楚逻辑
一路跟到write的时候才看出端倪
从一些参数上看的到是输出Hello,World
Fprintln函数里面前面调用的两个函数肯定是把我们的结构体首先转换成了一个结构体
传给write函数
然后实现的输出。
理论上Fprintln函数第二个参数应该是字符串
但是编译好之后我们实际去调的时候可以发现并不是这样。
我们只能通过定位最核心的函数
或者通过老道的经验去判断是干嘛
比如其实我们这里可以直接通过伪代码观察
这有个结构体
直接会发现是hello字符串然后还带着大小
这就是我们前面说过的虽然底层是通过type扩展实现
但逆向还是可以直接通过简化结构体来得到信息。
至于这里的第一个参数go_itab__os_File_io_Writer
就是第一个参数
就是源码中的一个io_write类型的参数
第二句的切片效果就比较明显
var numbers = make([]int,3,5)
直接看到就三个参数传参
第一个参数应该是int类型的结构体。
第三句是对切片的一个运用
fmt.Printf("len=%d cap=%d slice=%v\n",len(numbers),cap(numbers),numbers)
调用完makeslice初始化切片以后参数返回
然后把3 5当参数调用了一下convT64函数。
onvT64的作用就是分配这个存储值的内存空间
那么显然下面的convTslice的作用就是给切片申请内存空间
然后输出
但是这次你们又发现rcx是存放字符串的寄存器…
然后是数组
var n [10]int
var i,j int
for i = 0; i < 10; i++ {
n[i] = i + 100
}
for j = 0; j < 10; j++ {
fmt.Printf("Element[%d] = %d\n", j, n[j] )
}
这里将rsp那个栈上的地址作为参数调用了那个函数
函数做的就是把地址那个地方初始化一下
xmmword就是__int128
xmm15是128位寄存器
里面放着0
所以就是初始化一下栈上的临时变量
下面进入循环
不停的printf
黄色箭头是一个变量
红色箭头是一个变量
都通过convT64函数创造了内存空间
然后不停println
然后是map映射
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
fmt.Println("原始地图")
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}
delete(countryCapitalMap, "France")
fmt.Println("法国条目被删除")
fmt.Println("删除元素后地图")
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}
首先是创造map
伪代码看着是这样的
那个sub_45B7AB显然又是初始化栈临时变量的函数。
我们以初始化其中的一项为例
红色箭头是把字符串的地址填进bss
绿色箭头是做了一个判断
writeBarrier类似的函数都是gc垃圾回收那里的
我们可以直接不用管他。
然后输出了一下原始地图四个字
就初始化了map
可以看到通过栈里的变量来做到循环
通过这个变量找到键 并且用convTstring转换一下。
通过map access1_fastster来通过键找到值。
调用mapdelete_faststr将法国删除
然后又循环来了一次输出。
最后是接口部分
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
……
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
接口本来是跟复杂的东西
创建接口的时候会创建iface结构体
iface结构体里面会有itab表
接口转换的时候就是itab指针在变
最后通过itab指针找到要调用的函数
但是IDA里面我们清晰的可以看到它在干嘛他想干嘛
其他
我们现在只是能做到一些简单的go逆向的分析
还有很多深层次的原理也好,过程也好,还要在之后的学习过程中随着经验的积累慢慢的去掌握。