从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
和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 |
- |
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 * |