从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, X2 |
X0 = X1 + X2 |
SUB |
减法 | SUB X0, X1, X2 |
X0 = X1 - X2 |
MUL |
乘法 | MUL X0, X1, X2 |
X0 = X1 * X2 |
AND |
按位与 | AND X0, X1, X2 |
位运算 |
ORR |
按位或 | ORR X0, X1, X2 |
位运算 |
EOR |
异或 | EOR X0, X1, X2 |
位运算 |
LSL |
左移 | LSL X0, X1, #3 |
X0 = X1 << 3 |
LSR |
右移 | LSR X0, X1, #2 |
X0 = 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 |
返回,跳转到X30 |
RET |
相当于BR X30 |
条件跳转
配合CMP使用
| 指令 | 含义 | 条件 | 示例 |
|---|---|---|---|
B.EQ/BEQ |
相等 | Z=1 | BEQ label |
B.NE/BNE |
不相等 | Z=0 | BNE label |
B.LT/BLT |
小于 | N≠V | BLT label |
B.LE/BLE |
小于等于 | Z=1 or N≠V | BLE label |
B.GT/BGT |
大于 | Z=0 and N=V | BGT label |
B.GE/BGE |
大于等于 | N=V | BGE 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和X20LDP 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 |
- |
| 1 | exit |
int error_code |
| 2 | fork |
- |
| 3 | read |
unsigned int fd,char *buf,size_t count |
| 4 | write |
unsigned int fd,const char *buf,size_t count |
| 5 | open |
const char *filename,int flags,umode_t mode |
| 6 | close |
unsigned int fd |
| 7 | waitpid |
pid_t pid,int *status,int options |
| 8 | creat |
const char *pathname,umode_t mode |
| 9 | link |
const char *oldname,const char *newname |
| 10 | unlink |
const char *pathname |
| 11 | execve |
… |
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 * |