# 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`には以下のコードがある.

31行目の呼び出しには`it's a flag syscall`と書かれており,`call_flag_system_call()`にバッファとサイズが渡される.ただしフラグが書かれるであろうバッファは,システムコールから返ってくるとゼロクリアされてしまう.
`call_flag_system_call()`は単にシステムコール0x47を呼ぶ関数となっている.

`bl32_extra1.bin`(Trusted OS)には以下のコードがあるので,システムコール0x47を呼ぶことができればまずは一旦ゴールだと考えられる.

ただしその結果はセキュアワールドにしか返らないので,exploitではノーマルワールドに転送するところまで実装しなければならない.
## コマンド概観

:::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()`は,アセンブリ上では消えているため,検証は非常に甘いことに注意しよう.

また解析の結果分かった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 に実装しておいた.

# 次へ
- Part3/3 (攻略)
- https://hackmd.io/@bata24/HkZkcrW95