从0开始的iot学习记录-arm篇
前言
这篇博客会持续更新,作为我入坑iot的异架构学习记录
会记录刷题记录和arm汇编指令集
感谢winmt师傅,孤鸿师傅和爱师傅的帮助指导和🍐
环境配置和启动,调试
配置环境
主要需要的是gdbserver,gdb-multiarch和qemu
可以参考孤鸿师傅的博客配置:Blog
启动,调试
启动直接使用
1 | qemu-aarch64 ./pwn |
调试分为直接调试和加载进程调试
直接调试
在shell中输入
1 | qemu-aarch64 -g 1234 ./pwn |
qemu设置架构,-g指定端口为1234,运行的文件为./pwn
此时在另一个端口输入
1 | gdb-multiarch ./pwn |
此时进入gdb

在 gdb中输入
1 | target remote localhost:2234 |
连接2234端口,此时就可以正常使用gdb了

加载进程调试
我们如果想看exp的效果,可以使用这个调试模式
在exp创建进程对象时使用
1 | io = process(["qemu-aarch64", "-g", "4234","./pwn"]) |
然后运行exp时exp会被卡住,等待远程连接,此时重复直接调试的步骤

exp进程被卡住

远程连接
汇编指令集介绍
这段会持续更新,直到把所有指令搞得差不多懂了(精简指令集这个应该不难吧)
arm属于精简指令集,每条指令都是8/4字节对齐
AARCH64
运算
指令
含义
示例
说明
MOV
移动数据MOV X0, X1
把X1的值赋给X0,X1还可以是16位立即数
ADD
加法ADD X0, X1, X2X0 = X1 + X2
SUB
减法SUB X0, X1, X2X0 = X1 - X2
MUL
乘法MUL X0, X1, X2X0 = X1 * X2
AND
按位与AND X0, X1, X2
位运算
ORR
按位或ORR X0, X1, X2
位运算
EOR
异或EOR X0, X1, X2
位运算
LSL
左移LSL X0, X1, #3
`X0 = X1
LSR
右移LSR X0, X1, #2X0 = X1 >> 2
CMP
比较CMP X0, X1
实际是SUB,不存结果,仅影响标志位
数据加载与存储
指令
含义
示例
说明
LDR
加载LDR X0, [X1]
从X1指向的地址读取值存入X0
STR
存储STR X0, [X1]
把X0的值写入X1指向的地址
LDP
成对加载LDP X0, X1, [SP]
加载两个值
STP
成对存储STP X0, X1, [SP, #-16]!
一次存两个,常用于保存栈帧
跳转
指令
含义
示例
说明
B
无条件跳转B label
跳转到 label
BL
跳转并链接BL func
调用函数(保存返回地址到X30)
BR
跳转寄存器BR X30
类似RET
RET
返回,跳转到X30RET
相当于BR X30
条件跳转
配合CMP使用
指令
含义
条件
示例
B.EQ/BEQ
相等
Z=1BEQ label
B.NE/BNE
不相等
Z=0BNE label
B.LT/BLT
小于
N≠VBLT label
B.LE/BLE
小于等于
Z=1 or N≠VBLE label
B.GT/BGT
大于
Z=0 and N=VBGT label
B.GE/BGE
大于等于
N=VBGE label
系统调用
指令
含义
示例
NOP
空操作NOP
BRK
触发断点BRK #0
SVC
触发系统调用SVC #0
HINT
优化提示(用于处理器)
adr可以将label标签的地址加载到寄存器中,比如adr x0,binsh
此时在汇编处的ascii前加上binsh标签,就可以直接将/bin/sh的地址赋值给x0
这样可以完成在shellcode内部的字符串地址转化
其中LDP的扩展使用
LDP X19, X20, [SP,#0x10]从SP+0x10开始弹出两个8字节数据给X19和X20
LDP X29, X30, [SP+var_s0],#0x40从SP+var_s0弹出数据后,将SP自增0x40
STP的扩展使用
STP X29, X30, [SP,#var_s0]!将X29和X30存放在SP+var_s0后将SP+var_s0回写给SP(!的作用)相当于先进行偏移,后进行存储操作
STP X29, X30, [SP, #-0x50]则只是将X29和X30存放在SP+var_s0,不会改变SP
ARM
运算
指令
含义
示例
说明
MOV
数据传送mov r0, #1
将立即数1放入r0
MVN
取反mvn r0, #0
r0 =~0
ADD
加法add r1, r0, #2
r1 = r0 + 2
SUB
减法sub r2, r1, r0
r2 = r1 - r0
RSB
反向减法rsb r3, r0, #5
r3 = 5 - r0
AND
按位与and r4, r0, r1
r4 = r0 & r1
ORR
按位或orr r5, r0, r1
r5 = r0
EOR
异或eor r6, r0, r1
r6 = r0 ^ r1
CMP
比较cmp r0, #1
设置标志位
TST
测试tst r0, r1
按位与,不改变寄存器,只设置标志位
数据加载与储存
指令
含义
示例
说明
LDR
加载字ldr r0, [r1]
从内存中加载值到 r0
STR
存储字str r0, [r1]
将 r0 存入内存
LDRB
加载字节ldrb r0, [r1]
只加载 1 字节
STRB
存储字节strb r0, [r1]
存入 1 字节
LDMFD
加载多寄存器ldmfd sp!, {r0-r3}
从栈中加载多个寄存器
STMFD
存储多寄存器stmfd sp!, {r0-r3}
将多个寄存器入栈
指令流控制
指令
含义
示例
说明
B
跳转b loop
跳转到 loop
BL
跳转并保存bl func
调用函数,返回地址存在 LR
BX
跳转寄存器bx lr
跳转到 LR
BLX
跳转切换模式blx r3
跳转并切换 Thumb/ARM 模式
栈操作
指令
含义
示例
说明
PUSH
入栈push {r4, r5, lr}
保存寄存器
POP
出栈pop {r4, r5, pc}
恢复寄存器,跳转
STMFD
存储多寄存器(Full Descending)stmfd sp!, {r0-r3}
等价于 push
LDMFD
加载多寄存器ldmfd sp!, {r0-r3}
等价于 pop
其他
指令
说明
ADR
计算当前 PC 相对地址,将其写入一个寄存器
ADRP
类似ADR,但对齐到4KB页(Page)边界,只获取页地址部分,常用于构建绝对地址
题目练习
baby_arm
简要分析

只开启了NX保护


允许我们先向bss上写入0x200个字节,然后调用了一个可以溢出的函数
而且可以看到elf中包含mprotect和init函数,那我们就可以使用mprotect对抗NX保护,然后执行写在bss上的shellcode
shellcode书写
如果想使用pwntools库的shellcode,直接使用
1 | context(os = 'linux', arch = 'aarch64') |
当然了,手搓shellcode是每个pwn手必备的技能,通过手写汇编会加强我们对指令集的熟悉和理解
贴一张aarch64常用系统调用表
编号
系统调用名称
描述
64
write
写入文件
63
read
读取文件
56
openat
打开文件
57
close
关闭文件
93
exit
退出进程
94
exit_group
退出线程组
80
fstat
获取文件状态信息
221
execve
执行程序
调用execve
1 | sc = asm(" |
我们是通过x30跳转到shellcode上的,所有可以通过0x30+offset寻址/bin/sh
xzr寄存器是arm架构下一个值恒为0的寄存器,我们用这个清空寄存器
x8用于设置系统调用号,其中execve的系统调用号是221
svc用于触发系统调用
此时会执行execve(“/bin/sh”,0,0)
orw调用
在aarch64中,open已经被废弃,只剩下了openat
如果想打开名为flag的文件,应该调用openat(-100,”./flag”,0,0)
1 | sc = asm(""" |
对抗NX保护
我们想要调用mprotect,参数设置还是很简单的,和x86中的ret2csu几乎相同
但是ret2csu中的调用部分:此时的got表中还没有mprotect的真实函数地址😵感谢winmt师傅教学
我们可以先将mprotect的plt表写在bss上
然后ret2csu中的call的[地址]写为bss的地址,这样就可以正常调用mportect
调用后直接跳转到shellcode就成了
EXP
1 | from pwn import * |
stack_buffer_overflow_basic


栈有可执行权限,且程序会打印出栈地址,我们可以在栈上写shellcode,然后跳转到shellcode上
arm架构的shellcode书写
execve
仍然是执行execve(“/bin/sh”,0,0)
其中arm结构的参数通过寄存器+栈传递
寄存器
用途说明
r0~`r3`
前四个参数使用这四个寄存器传递(依次排布)
r4~`r10`
用于保存局部变量或 callee-saved 寄存器(函数内部可用,但调用者期望其不变)
r11
栈底寄存器,临时变量的地址由r11+offset寻址
r13
栈指针(SP)
r14
链接寄存器(LR)
r15
程序计数器(PC)
超过 4 个参数
从第 5 个参数开始,压入栈中由 SP 引用
首先我们要把r0-r3清空,然后把r0填入/bin/sh的地址
然后向r7填入execve的系统调用号#11,执行svc #0
1 | shellcode=asm(""" |
orw
常用系统调用号表
编号
系统调用名
参数说明
0
restart_syscall
1exitint error_code
2
fork
3readunsigned int fd,char *buf,size_t count
4writeunsigned int fd,const char *buf,size_t count
5openconst char *filename,int flags,umode_t mode
6closeunsigned int fd
7waitpidpid_t pid,int *status,int options
8creatconst char *pathname,umode_t mode
9linkconst char *oldname,const char *newname
10unlinkconst char *pathname
11execve
…
shellcode
1 | shellcode=asm(''' |
EXP
1 | from pwn import * |
stack_spraying


拥有一个明显的栈溢出,还有system函数,可以rce
因为elf中没有现成的rce指令,我们需要将栈迁移到bss上,然后注入命令后再次溢出到system附近

system的参数是由r11+offset确定的,而我们已知bss的地址
所以
EXP
1 | from pwn import * |
要点
scanf会对\x0c进行截断,\x0c 被当成控制字符 Form Feed,类似于换页符、换行符,会被终端解释为“输入结束”
arm中调用system时要保持SP指针8字节对齐
所以我们要在栈迁移时注意地址的对齐和不能出现\x0c
typo

符号表被剔除干净了,什么都找不到
即使使用gdb也很难找到程序的逻辑

判断溢出长度
根据程序的运行可以看到我们能输入一些内容,猜测是栈溢出
溢出长度我们可以手动写个脚本简单fuzz一下
1 | from pwn import * |
当达到溢出长度时程序会崩溃,经过测试得到len=0x70

由于程序的静态编译的,我们可以在elf中找到足够的gadget调用system(“/bin/sh”)

所以简单的栈溢出
EXP
1 | from pwn import * |