# DEFCON CTF 2022 - teedium-wallet (2/3) ###### tags: `trustzone` `ctf` `pwn` `ARM` # リンク集 - Part1/3 (事前準備/TIPS) - https://hackmd.io/@bata24/BJ3nuVEu5 - Part2/3 (解析) - https://hackmd.io/@bata24/ry1_YS-c5 - Part3/3 (攻略) - https://hackmd.io/@bata24/HkZkcrW95 # 解析 今回の解析対象は,以下である. - Trusted App - `extracted.elf` - Trusted OS - `bl32_extra1.bin` - ただしリバースはほぼ不要 ## シンボル付与 `extracted.elf`はstrippedなバイナリだが,OPTEE-OSのフレームワークを用いて作っていると考えられる.従って,頑張ればシンボルを付与することが可能だ. https://github.com/OP-TEE/optee_os をローカルでビルドし,ARM32向けにビルドする. その際,makeコマンドに`-V`オプションを渡す(`make run -V`)とobjcopyでstripするようなコマンドがあることがわかる.当然元になるELFが存在するので,それらを持ってきて`extracted.elf`と比較すれば良い. 尚,今回はIDAのFLIRT(`pelf`+`sigmake`)や`idb2pat.py`を使ってシンボルを移植しようとしたが,いずれもあまり芳しい結果は得られなかったため,地道に各ファイルを比較しながら特定していった. やり方は愚直で,bindiffに同梱されるIDAプラグインや,diaphoraなどのプラグインを使って,ローカルビルドしたELFと`extracted.elf`で確実に一致する大きめの関数の位置をいくつか特定する. その前後の関数は,ビルド元のソースコードの並び順と一致しているはずなので(差分の発生はプリプロセッサによって消されるくらいしか考えにくい),OPTEE-OSの各種ソースコードと`extracted.elf`のデコンパイル結果を見比べながらシンボルを特定していけば良い. 関数自体は600個程度なので,数時間もあればほぼ完全に復元できる. ## 構造体定義 OPTEEのTAの解析で,よく使われる構造体や列挙体をまとめておいた.IDAのLocal types画面に食わせれば使うことができる. https://gist.github.com/bata24/a6265af40f889eaa87f575801021e87a 一部の構造体は`#ifdef`をよくありそうな設定で決め打ちしているため,メンバーの有無が実際とは異なるかもしれない.その時は臨機応変に修正して欲しい. ## ゴールの設定 `extracted.elf`には以下のコードがある. ![](https://i.imgur.com/taMXKRe.png) 31行目の呼び出しには`it's a flag syscall`と書かれており,`call_flag_system_call()`にバッファとサイズが渡される.ただしフラグが書かれるであろうバッファは,システムコールから返ってくるとゼロクリアされてしまう. `call_flag_system_call()`は単にシステムコール0x47を呼ぶ関数となっている. ![](https://i.imgur.com/GwMHyLm.png) `bl32_extra1.bin`(Trusted OS)には以下のコードがあるので,システムコール0x47を呼ぶことができればまずは一旦ゴールだと考えられる. ![](https://i.imgur.com/OSjFzg3.png) ただしその結果はセキュアワールドにしか返らないので,exploitではノーマルワールドに転送するところまで実装しなければならない. ## コマンド概観 ![](https://i.imgur.com/J4t8IES.png) :::spoiler 入出力パラメータの仕様 {%gist bata24/680504dbfd88385f3e4fdd2ea5206a6f %} ::: :::spoiler 入出力サンプル {%gist bata24/2f5d0069a36a74e61f0b6d8e90aa3324 %} ::: ## btcのアドレス仕様 以下がとてもわかり易く,非常に参考になる. https://qiita.com/ryo0301/items/0bc9ccfb3291cabd50d5 # 脆弱性 Write-upやソースによれば,`cmd3`から呼ばれる`serialize_tx()`内の`serialize_sighash()`にヒープBOFがあるとのことだ. 該当箇所を見てみると,全体的に`serialize_tx()`は以下のようになっている.最初に`0x100`バイトの領域を確保し,シリアライズした結果をそこに書き込んでいる. ```c= TEE_Result __cdecl serialize_tx(tx_t *tx, uint8_t **out, size_t *out_sz) { unsigned int v3; // r5 unsigned int v4; // r6 unsigned int v5; // r7 unsigned int v6; // r8 int v7; // lr uint8_t tx_hash[12]; // [sp+1Ch] [bp-8h] BYREF uint8_t *tx_bytes; // [sp+3Ch] [bp+18h] BYREF size_t capacity; // [sp+40h] [bp+1Ch] BYREF uint32_t pos; // [sp+44h] [bp+20h] BYREF uint64_t n_output_entries; // [sp+48h] [bp+24h] output_t *cur_output; // [sp+54h] [bp+30h] int end; // [sp+58h] [bp+34h] int start; // [sp+5Ch] [bp+38h] uint64_t n_input_entries; // [sp+60h] [bp+3Ch] input_t *cur_input; // [sp+68h] [bp+44h] TEE_Result res; // [sp+6Ch] [bp+48h] unsigned __int64 v24; // [sp+78h] [bp+54h] unsigned __int64 v25; // [sp+80h] [bp+5Ch] int v26; // [sp+8Ch] [bp+68h] v24 = __PAIR64__(v4, v3); v25 = __PAIR64__(v6, v5); v26 = v7; res = 0; pos = 0; capacity = 0x100; tx_bytes = (uint8_t *)TEE_Malloc(0x100, 0); // 最初の確保 if ( !tx_bytes ) return TEE_ERROR_OUT_OF_MEMORY; if ( serialize_int(1u, &tx_bytes, &pos, &capacity) ) // 最初の書き込み { res = TEE_ERROR_GENERIC; } else { cur_input = tx->inputs; n_input_entries = 0LL; while ( cur_input ) { cur_input = (input_t *)cur_input->ll.next; ++n_input_entries; } if ( serialize_var_int(n_input_entries, &tx_bytes, &pos, &capacity) ) // 次の書き込み(以下省略) { res = TEE_ERROR_GENERIC; } else { for ( cur_input = tx->inputs; cur_input; cur_input = (input_t *)cur_input->ll.next ) { TEE_MemMove(tx_hash, cur_input->tx_hash, 0x20); start = 0; for ( end = 0x1F; start < end; --end ) { tx_hash[start] ^= tx_hash[end]; tx_hash[end] ^= tx_hash[start]; tx_hash[start++] ^= tx_hash[end]; } if ( serialize_bytes(tx_hash, 0x20u, &tx_bytes, &pos, &capacity) ) { res = TEE_ERROR_GENERIC; goto LABEL_41; } if ( serialize_int(cur_input->index, &tx_bytes, &pos, &capacity) ) { res = TEE_ERROR_GENERIC; goto LABEL_41; } if ( serialize_var_string(cur_input->scriptSig, cur_input->scriptSig_len, &tx_bytes, &pos, &capacity) ) { res = TEE_ERROR_GENERIC; goto LABEL_41; } if ( serialize_int(cur_input->sequence, &tx_bytes, &pos, &capacity) ) { res = TEE_ERROR_GENERIC; goto LABEL_41; } } cur_output = tx->outputs; n_output_entries = 0LL; while ( cur_output ) { cur_output = (output_t *)cur_output->ll.next; ++n_output_entries; } if ( serialize_var_int(n_output_entries, &tx_bytes, &pos, &capacity) ) { res = TEE_ERROR_GENERIC; } else { for ( cur_output = tx->outputs; cur_output; cur_output = (output_t *)cur_output->ll.next ) { if ( serialize_int64(cur_output->value, &tx_bytes, &pos, &capacity) ) { res = TEE_ERROR_GENERIC; goto LABEL_41; } if ( serialize_var_string(cur_output->lockScript, cur_output->lockScript_len, &tx_bytes, &pos, &capacity) ) { res = TEE_ERROR_GENERIC; goto LABEL_41; } } if ( serialize_int(tx->locktime, &tx_bytes, &pos, &capacity) ) { res = TEE_ERROR_GENERIC; } else { if ( !serialize_sighash(&tx_bytes, &pos, &capacity) ) // バグはこの内部 { *out = tx_bytes; *out_sz = pos; return res; } res = TEE_ERROR_GENERIC; } } } } LABEL_41: TEE_Free(tx_bytes); return res; } ``` 随所で`serialize_int()`, `serialize_var_int()`, `serialize_bytes()`, `serialize_int64()`が呼ばれているのに気づくだろう. これらの関数は全て,内部で実際に値をバッファに書き込む前に,以下の`serialize_ensure_size()`を呼んでいる.これはさらに`TEE_Realloc()`を呼んでいる.つまり,最初に確保した`0x100`バイトでは足りない場合,自動的に`TEE_Realloc()`が呼ばれてサイズが調整されるというわけだ. ```c= uint8_t *__cdecl serialize_ensure_size(uint8_t **s, size_t n, uint32_t *pos, size_t *cap) { while ( *pos + n > *cap ) { *cap += 0x100; *s = (uint8_t *)TEE_Realloc(*s, *cap); } return *s; } ``` しかし`serialize_sighash()`はそうではない. ```c= int __cdecl serialize_sighash(uint8_t **s, uint32_t *pos, size_t *cap) { int v3; // lr int sighash; // [sp+14h] [bp+Ch] BYREF int v7; // [sp+1Ch] [bp+14h] v7 = v3; sighash = 1; TEE_MemMove(&(*s)[*pos], &sighash, 4); *pos += 4; return 0; } ``` `TEE_Realloc()`は呼ばれていないため,バッファが足りない場合はヒープBOFにつながることになる. ただし`TEE_MemMove()`でコピーされる値`sighash`は,`0x00000001`という値で固定されているため,ヒープのメタデータを`\x01\x00\x00\x00`というデータで4バイト破壊することしか出来ないことに注意しよう. # ヒープの構造 後続領域のメタデータを破壊したことで,どのような問題が起きるだろうか.unlinkなどのバグが起こせるのかもしれない.確認すべきはヒープ系関数の実装とチャンクの構造だ. さてTAが使っているヒープ系関数は,`optee_os/lib/libutee/tee_api.c`で定義されている.以下の通りだ. ```c= #define TEE_NULL_SIZED_VA ((void *)1) #define TEE_MALLOC_FILL_ZERO 0x00000000 #define TEE_USER_MEM_HINT_NO_FILL_ZERO 0x80000000 void *TEE_Malloc(uint32_t len, uint32_t hint) { if (!len) return TEE_NULL_SIZED_VA; if (hint == TEE_MALLOC_FILL_ZERO) return calloc(1, len); else if (hint == TEE_USER_MEM_HINT_NO_FILL_ZERO) return malloc(len); EMSG("Invalid hint %#" PRIx32, hint); return NULL; } void *TEE_Realloc(void *buffer, uint32_t newSize) { if (!newSize) { TEE_Free(buffer); return TEE_NULL_SIZED_VA; } if (buffer == TEE_NULL_SIZED_VA) return calloc(1, newSize); return realloc(buffer, newSize); } void TEE_Free(void *buffer) { if (buffer != TEE_NULL_SIZED_VA) free(buffer); } ``` 最も読みやすかった`free()`の実装を見ていこう.`lib/libutils/isoc/bget_malloc.c`にその定義がある. ```c= static void free_helper(void *ptr, bool wipe) { uint32_t exceptions = malloc_lock(&malloc_ctx); raw_free(ptr, &malloc_ctx, wipe); malloc_unlock(&malloc_ctx, exceptions); } void free(void *ptr) { free_helper(ptr, false); } void raw_free(void *ptr, struct malloc_ctx *ctx, bool wipe) { raw_malloc_validate_pools(ctx); // 今回の環境では何もしない if (ptr) brel(maybe_untag_buf(ptr), &ctx->poolset, wipe); } ``` `free()`は`free_helper()`を呼び,そこから`raw_free()`を呼び出し,更に`brel()`を呼び出しているので,実体は`brel()`だ. また重要な構造体として`malloc_ctx`も調べる必要がある.まずは`malloc_ctx`の構造体から見てみよう. ```c= typedef long bufsize; struct qlinks { struct bfhead *flink; /* Forward link */ struct bfhead *blink; /* Backward link */ }; struct bhead { bufsize prevfree; /* Relative link back to previous free buffer in memory or 0 if previous buffer is allocated. */ bufsize bsize; /* Buffer size: positive if free, negative if allocated. */ }; struct bfhead { struct bhead bh; /* Common allocated/free header */ struct qlinks ql; /* Links on free list */ }; struct bpoolset { struct bfhead freelist; #ifdef BufStats bufsize totalloc; /* Total space currently allocated */ long numget; /* Number of bget() calls */ long numrel; /* Number of brel() calls */ #ifdef BECtl long numpblk; /* Number of pool blocks */ long numpget; /* Number of block gets and rels */ long numprel; long numdget; /* Number of direct gets and rels */ long numdrel; #endif /* BECtl */ #endif /* BufStats */ #ifdef BECtl /* Automatic expansion block management functions */ int (*compfcn) _((bufsize sizereq, int sequence)); void *(*acqfcn) _((bufsize size)); void (*relfcn) _((void *buf)); bufsize exp_incr; /* Expansion block size */ bufsize pool_len; /* 0: no bpool calls have been made -1: not all pool blocks are the same size >0: (common) block size for all bpool calls made so far */ #endif }; struct malloc_pool { void *buf; size_t len; }; struct malloc_ctx { struct bpoolset poolset; struct malloc_pool *pool; size_t pool_len; #ifdef BufStats struct malloc_stats mstats; #endif #ifdef __KERNEL__ unsigned int spinlock; #endif }; ``` 続いて`brel()`の実装は以下の通りだ. ```c= struct bhead { bufsize prevfree; /* Relative link back to previous free buffer in memory or 0 if previous buffer is allocated. */ bufsize bsize; /* Buffer size: positive if free, negative if allocated. */ }; #define BH(p) ((struct bhead *) (p)) /* Header in directly allocated buffers (by acqfcn) */ struct bdhead { bufsize tsize; /* Total size, including overhead */ bufsize offs; /* Offset from allocated buffer */ struct bhead bh; /* Common header */ }; #define BDH(p) ((struct bdhead *) (p)) /* Header in free buffers */ struct bfhead { struct bhead bh; /* Common allocated/free header */ struct qlinks ql; /* Links on free list */ }; #define BFH(p) ((struct bfhead *) (p)) /* BREL -- Release a buffer. */ void brel(buf, poolset, wipe) void *buf; struct bpoolset *poolset; int wipe; { struct bfhead *b, *bn; b = BFH(((char *) buf) - sizeof(struct bhead)); #ifdef BufStats poolset->numrel++; /* Increment number of brel() calls */ #endif assert(buf != NULL); #ifdef FreeWipe wipe = true; #endif #ifdef BECtl if (b->bh.bsize == 0) { /* Directly-acquired buffer? */ struct bdhead *bdh; bdh = BDH(((char *) buf) - sizeof(struct bdhead)); assert(b->bh.prevfree == 0); #ifdef BufStats poolset->totalloc -= bdh->tsize; assert(poolset->totalloc >= 0); poolset->numdrel++; /* Number of direct releases */ #endif /* BufStats */ if (wipe) { V memset_unchecked((char *) buf, 0x55, (MemSize) (bdh->tsize - sizeof(struct bdhead))); } assert(poolset->relfcn != NULL); poolset->relfcn((char *)buf - sizeof(struct bdhead) - bdh->offs); /* Release it directly. */ return; } #endif /* BECtl */ /* Buffer size must be negative, indicating that the buffer is allocated. */ if (b->bh.bsize >= 0) { bn = NULL; } assert(b->bh.bsize < 0); /* Back pointer in next buffer must be zero, indicating the same thing: */ assert(BH((char *) b - b->bh.bsize)->prevfree == 0); #ifdef BufStats poolset->totalloc += b->bh.bsize; assert(poolset->totalloc >= 0); #endif /* If the back link is nonzero, the previous buffer is free. */ if (b->bh.prevfree != 0) { /* The previous buffer is free. Consolidate this buffer with it by adding the length of this buffer to the previous free buffer. Note that we subtract the size in the buffer being released, since it's negative to indicate that the buffer is allocated. */ register bufsize size = b->bh.bsize; /* Make the previous buffer the one we're working on. */ assert(BH((char *) b - b->bh.prevfree)->bsize == b->bh.prevfree); b = BFH(((char *) b) - b->bh.prevfree); b->bh.bsize -= size; } else { /* The previous buffer isn't allocated. Insert this buffer on the free list as an isolated free block. */ assert(poolset->freelist.ql.blink->ql.flink == &poolset->freelist); assert(poolset->freelist.ql.flink->ql.blink == &poolset->freelist); b->ql.flink = &poolset->freelist; b->ql.blink = poolset->freelist.ql.blink; poolset->freelist.ql.blink = b; b->ql.blink->ql.flink = b; b->bh.bsize = -b->bh.bsize; } /* Now we look at the next buffer in memory, located by advancing from the start of this buffer by its size, to see if that buffer is free. If it is, we combine this buffer with the next one in memory, dechaining the second buffer from the free list. */ bn = BFH(((char *) b) + b->bh.bsize); if (bn->bh.bsize > 0) { /* The buffer is free. Remove it from the free list and add its size to that of our buffer. */ assert(BH((char *) bn + bn->bh.bsize)->prevfree == bn->bh.bsize); assert(bn->ql.blink->ql.flink == bn); assert(bn->ql.flink->ql.blink == bn); bn->ql.blink->ql.flink = bn->ql.flink; bn->ql.flink->ql.blink = bn->ql.blink; b->bh.bsize += bn->bh.bsize; /* Finally, advance to the buffer that follows the newly consolidated free block. We must set its backpointer to the head of the consolidated free block. We know the next block must be an allocated block because the process of recombination guarantees that two free blocks will never be contiguous in memory. */ bn = BFH(((char *) b) + b->bh.bsize); } if (wipe) { V memset_unchecked(((char *) b) + sizeof(struct bfhead), 0x55, (MemSize) (b->bh.bsize - sizeof(struct bfhead))); } assert(bn->bh.bsize < 0); /* The next buffer is allocated. Set the backpointer in it to point to this buffer; the previous free buffer in memory. */ bn->bh.prevfree = b->bh.bsize; #ifdef BECtl /* If a block-release function is defined, and this free buffer constitutes the entire block, release it. Note that pool_len is defined in such a way that the test will fail unless all pool blocks are the same size. */ if (poolset->relfcn != NULL && ((bufsize) b->bh.bsize) == (pool_len - sizeof(struct bhead))) { assert(b->bh.prevfree == 0); assert(BH((char *) b + b->bh.bsize)->bsize == ESent); assert(BH((char *) b + b->bh.bsize)->prevfree == b->bh.bsize); /* Unlink the buffer from the free list */ b->ql.blink->ql.flink = b->ql.flink; b->ql.flink->ql.blink = b->ql.blink; poolset->relfcn(b); #ifdef BufStats poolset->numprel++; /* Nr of expansion block releases */ poolset->numpblk--; /* Total number of blocks */ assert(numpblk == numpget - numprel); #endif /* BufStats */ } #endif /* BECtl */ } ``` `#ifdef`のせいで不要なコードが混じって見辛いため,コメント付きの解析結果も貼っておく.こちらの方が読みやすいと思う.尚ソース中の`assert()`は,アセンブリ上では消えているため,検証は非常に甘いことに注意しよう. ![](https://i.imgur.com/aEUg4w7.png) また解析の結果分かったbget allocatorのメモリ構造(主にフリーリストやチャンク周り)は以下の通りだ.ただし`malloc_ctx`のメンバは,`#ifdef`によって有無が決まるので,今回の環境とは異なる.尚知っておくべきは先頭の4つのメンバ程度なので,それ以外は気にしなくて良い. ```= +-malloc_ctx-------------------+ +-free-ed chunk----------+ | bufsize prevfree |<--+ +-->| bufsize prevfree |= 0 (if upper chunk is used) +--> ... | bufsize bsize | | | | bufsize bsize |= the size of this chunk | | struct bfhead *flink |-----+ | struct bfhead *flink |------------------------------+ | struct bfhead *blink | +-----| struct bfhead *blink | | (bufsize totalloc) | | | | (long numget) | | | | (long numrel) | | | | (long numpblk) | +-used chunk-------------+ | (long numpget) | | bufsize prevfree |= the size of upper chunk (if upper chunk is free-ed) | (long numprel) | | bufsize bsize |= the size of this chunk (negative number) | (long numdget) | | uchar user_data[bsize] | | (long numdrel) | | | | (func_ptr compfcn) | | | | (func_ptr acqfcn) | +------------------------+ | (func_ptr relfcn) | | (bufsize exp_incr) | | (bufsize pool_len) | | struct malloc_pool* pool | | size_t pool_len | | (struct malloc_stats mstats) | +------------------------------+ ``` 今後のため,`malloc_ctx`とフリーリストをダンプするコマンド(`optee-bget-dump`)も https://github.com/bata24/gef に実装しておいた. ![](https://i.imgur.com/n7VqP7O.png) # 次へ - Part3/3 (攻略) - https://hackmd.io/@bata24/HkZkcrW95