go逆向首先了解他的函数调用规则。
环境搭建在go 环境搭建
代码这样
package main
func val(c, d int) (a int, b int) {
e := 5
f := 6
a = c + d + e + f
b = d * 2
return
}
func test() {
i, j := val(1, 2)
i = i + 3
j = j + 4
}
func main() {
test()
}
然后编译成二进制文件
go build -gcflags "-N -l" 1.go
gcflags可以向go编译器传递参数。
-N参数代表禁止优化, -l参数代表禁止内联,
go在编译目标程序的时候会嵌入运行时(runtime)的二进制,
禁止优化和内联可以让运行时(runtime)中的函数变得更容易调试.
然后就好多了。
先看main函数的汇编
sub rsp, 8
mov [rsp+8+var_8], rbp
lea rbp, [rsp+8+var_8]
call main_test
mov rbp, [rsp+8+var_8]
add rsp, 8
nop
retn
显然我们看到因为main没有参数
就是把栈抬高八个字节
然后rbp放进去
然后放的这个地方是rbp新地址。
函数返回的时候正常返回就可以了。
单从这里来看
效果跟C语言函数调用的效果是一样的。
都是压一个rbp进去
只不过是具体的汇编不一样。
再来看test函数
sub rsp, 38h #两个参数此时rsp指向main函数中call的时候压进去的返回地址
mov [rsp+38h+var_8], rbp #0x38的栈最下面八个字节是rbp
lea rbp, [rsp+38h+var_8]
mov eax, 1 #两个参数乱入
mov ebx, 2
xchg ax, ax #xchg是交换,ax交换ax,啥也不干,cherest师傅说是废话文学
call main_val #又把返回地址压进去之后先不看下面的先去看看val函数
#经过下面的函数调用回来之后我们现在知道
#开了0x38的栈 最下面8个字节是栈底
#然后最上面两个是函数调用时候的传参
#rax rbx里面放着返回值
mov [rsp+38h+var_10], rax #把返回值放进临时变量i j
mov [rsp+38h+var_18], rbx
mov rcx, [rsp+38h+var_10] #剩下的四个qword就是两个i 两个j
mov [rsp+38h+var_20], rcx #两个的目的是进行了i = i + 3这样的操作
mov rcx, [rsp+38h+var_18] #可能因为关了优化 导致有点空间上的浪费
mov [rsp+38h+var_28], rcx #但是并不影响我们研究函数的调用规则
mov rcx, [rsp+38h+var_20]
add rcx, 3
mov [rsp+38h+var_20], rcx
mov rcx, [rsp+38h+var_28]
add rcx, 4
mov [rsp+38h+var_28], rcx
mov rbp, [rsp+38h+var_8]
add rsp, 38h
retn #最后返回
val
var_28= qword ptr -28h
var_20= qword ptr -20h
var_18= qword ptr -18h
var_10= qword ptr -10h
var_8= qword ptr -8
arg_0= qword ptr 8
arg_8= qword ptr 10h
sub rsp, 28h #这个函数栈空间开了0x28
mov [rsp+28h+var_8], rbp #然后又是rbp放进去这个操作
lea rbp, [rsp+28h+var_8]
mov [rsp+28h+arg_0], rax #rax是第一个参数 放在了返回地址下面第一个qword
mov [rsp+28h+arg_8], rbx #rbx是第二个参数 放在返回地址下面第二个qword
mov [rsp+28h+var_10], 0 #这连着的四个显然就是两个返回值跟两个临时变量
mov [rsp+28h+var_18], 0
mov [rsp+28h+var_20], 5
mov [rsp+28h+var_28], 6
mov rcx, [rsp+28h+arg_0] #这里就是做那个加法,都加在rcx里面
add rcx, [rsp+28h+arg_8] #参数就是用之前rax rbx加进去的。
add rcx, [rsp+28h+var_20]
add rcx, 6
mov [rsp+28h+var_10], rcx #最后的结果放进返回值那里
mov rbx, [rsp+28h+arg_8]
shl rbx, 1
mov [rsp+28h+var_18], rbx #对临时变量b进行处理
mov rax, [rsp+28h+var_10] #然后rax rbx又分别是两个返回值 rax是第一个返回值 rbx是第二个返回值
mov rbp, [rsp+28h+var_8]
add rsp, 28h
retn
我们总结一下。
其实单论栈布局来讲这与C语言的栈布局并没有什么区别。
栈指针在栈底
后面跟着返回地址
再后面跟着参数
不一样的只不过是整体实现过程中的具体汇编代码,或者说整个的过程有差别。
函数调用方面传参用的是rax rbx 返回值也是rax rbx。
参数方面是stdcall的调用规则。从右往左压参。
说白了就是咱平常说的返回地址下面是第一个参数,然后是第二个第三个。
然后临时变量的分布也有规律这里的话是先压返回值,然后压临时变量
但是我们不关心,平常可能也会被优化掉。
那么问题来了
我们的传参参数多了剩下的寄存器应该是啥?
如果我们一个函数中调用了好几个函数,栈又是怎样的分布?
带着问题我们接着实验。