文档地址
ARMv7-A和 ARMv7-R的文档地址:
https://developer.arm.com/documentation/ddi0406/cd/?lang=en
ARMv8-A 的文档地址:
https://developer.arm.com/documentation/ddi0487/ka/?lang=en
ARM64因为指令更长,hook起来更稳定,所以有时候逆向反而更容易
参数和返回值传递
Arm32参数传递
前4个参数,使用r0~r3传递
后面的参数,使用栈传递
#include <stdio.h>
int main(int argc, char const *argv[])
{
printf("hello arm:%d %d %d %d %d\r\n",1,2,3,4,5);
return 0;
}
export PATH=/home/tom/kanxue/Chapter09/android-ndk-r21e/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH
clang -target arm-linux-android21 -S hello-multiarg.c -o hello-multiarg.s
hello-multiarg.s:
@ %bb.0:
.save {r11, lr}
push {r11, lr}
.setfp r11, sp
mov r11, sp
.pad #32
sub sp, sp, #32
mov r2, #0
str r2, [r11, #-4]
str r0, [r11, #-8]
str r1, [r11, #-12]
mov r0, sp
mov r1, #5
str r1, [r0, #4]
mov r1, #4
str r1, [r0]
ldr r0, .LCPI0_0
.LPC0_0:
add r0, pc, r0
mov r1, #1
mov r3, #2
mov r12, #3
str r2, [sp, #16] @ 4-byte Spill
mov r2, r3
mov r3, r12
bl printf
Arm64参数和返回值传递
前8个参数:R0~R7
返回值:
- 不超过64位:X0
- 超过64位:将返回值的地址存在X8,函数返回后 从X8存储的地址取出返回值
常用寄存器
ARM32中主要寄存器
(armeabi-v7a,armeabi)
13+3个寄存器
- R0-R12
- SP LR PC
ARM64中主要寄存器
(arm64-v8a)
31+2+32+2个寄存器
- R0~R30 31个通用寄存器,每个64位
- 64bit:X0~X30
- 32bit:W0~W30
- X30被用作链接寄存器(Link Register,LR),存储函数调用的返回地址
- SP
- PC
- V0-V31:SIMD&FP registers, 浮点寄存器
- FPCR, FPSR
LR寄存器(Link Register)
在ARM处理器中使用 lr实现对调用点的记录
ARM处理器执行BL时,会自动将 当前PC减去4的结果 保存到LR寄存器
参考https://www.cnblogs.com/FightingChen/p/12411753.html
流水线
注意 PC值 = 当前程序执行位置 + 8字节,因为ARM是三级流水线
三级流水线:
取指、译码、执行
所以PC值 = 当前程序执行位置 + 8字节
参考:https://blog.youkuaiyun.com/u012351051/article/details/80898085
负整数的十六进制
执行前
r0是0xffffeedc,也就是-4388
参见https://www.toolhelper.cn/Digit/BaseConvertNegative
pc是0x0040149c,也就是4,199,580
处理器上pc的实际值是4,199,580+8=4,199,588
所以add r0, pc, r0之后,r0是4,199,588-4388=4,195,200=0x400380
符号
井号# :代表立即数
立即数要以“#”为前缀
例如:
LDR X8, [X21,#0x28]
直接跳转与间接跳转
直接跳转:跳转目标是作为指令的一部分编码的
间接跳转:跳转目标是从寄存器或内存位置中读出的
存储器访问指令
LDR和STR
ldr r0, [pc, #20] ;load to register
;r0 = *(pc+20)
str r2, [r11, #-4] ;store register
;*(r11-4) = r2
STP
STP: 入栈指令(str 的变种指令,可以同时操作两个寄存器)
如:
stp x29, x30, [sp, #0x10] ; 将 x29, x30 的值存入 sp + 16个字节的地址
stp x29, x30, [sp, #-32]! ; 将 x29, x30 的值存入 sp - 32个字节的地址后,sp=sp-32
CSET
CSET: 比较指令(满足条件,则置 1,否则置 0 )
如:
cmp w8, #2 ; 将寄存器 w8 的值和常量 2 进行比较
cset w8, gt ; 如果是大于(grater than),则将寄存器 w8 的值设置为 1,否则设置为 0
TBNZ
TBNZ: 特殊判断指令(满足条件,则置 1,否则置 0 )
如:
TBNZ W28, #3, loc_D553E8 ; 如果W28寄存器的第3位不等于0,则跳转到loc_D553E8
push
push {r11, lr} 从右往左依次把寄存器压栈
在函数刚进来时 会保存寄存器
r11,lr → r11
lr
pop
pop {r11, lr} 从栈中取值放入寄存器
在函数结尾时 会恢复寄存器
r11 → r11,lr
lr
执行前:
执行后:
常用指令
MOV
MOV 目的寄存器,源寄存器
mov r11, sp ;r11=sp
BLX(Branch with Link and eXchange)
ARM指令:每条指令都是4字节
Thumb指令:每条指令大多是2字节
从Thumb指令 调用ARM指令,或者从ARM指令 调用 Thumb指令时,
使用BLX,X代表切换
BLX指令会将当前指令的下一条指令地址存入LR寄存器,并跳转到指定位置(更新PC),同时切换处理器状态(ARM ↔ Thumb)
BIC(Bit Clear)
BIC <Rd>, <Rn>, <Operand2> ; Rd = Rn AND (NOT Operand2)
例如 BIC R1, LR, #1
的功能是:
清除 LR(Link Register)的最低位(bit 0),并将结果存入 R1。
从指令到字节码
比如将B #0x40转换为字节码的时候:
27~24 = 1010
关于cond:
A8.3 Conditional execution
所以cond = 1110
31~28 = cond = 1110
27~24 = 1010
imm24之后要被SignExtend转化为imm32
关于SignExtend:
SignExtend(x, i) = Replicate(TopBit(x), i-Len(x)) : x
SignExtend(imm24:‘00’, 32)
= Replicate(TopBit(imm24:‘00’), 32-Len(imm24:‘00’)) : imm24:‘00’
= Replicate(0, 32-26) : imm24:‘00’
= ‘0000 00’ : imm24 : ‘00’
我们希望跳转到0x40,由于三级流水线,当我们执行到B #0x40这条指令的时候,我们希望跳转到的偏移量应该是0x40-0x08=0x38(其实也没太理解,这里#0x40应该是绝对地址呀)
所以希望imm32是0x38,也就是 0000 0000 0000 0000 0000 0000 0011 1000
所以imm24应该是00 0000 0000 0000 0000 0011 10
最终B #0x40对应的字节码应该是
31~28 = cond = 1110
27~24 = 1010
imm24 = 00 0000 0000 0000 0000 0011 10
合起来:1110 1010 0000 0000 0000 0000 0000 1110
可以用keystone验证
pip install capstone
pip install keystone-engine
import keystone
import capstone
def ins2bcode(arm_ins):
ks = keystone.Ks(keystone.KS_ARCH_ARM, keystone.KS_MODE_ARM)
arm_bytecode=ks.asm(arm_ins,as_bytes=True)
bytecode =int.from_bytes(arm_bytecode[0],"little") # 将汇编后的字节码转换为整数,使用小端序
bytecode_bin =format(bytecode, "032b") # 将整数转换为32位的二进制字符串
print("arm ins: \t",arm_ins)
print("bytecode hex: \t", hex(bytecode))
print("bytecode bin: \t", bytecode_bin)
return arm_bytecode[0]
def bcode2ins(arm_bytecode):
cs=capstone.Cs(capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM)
for ins in cs.disasm(arm_bytecode, 0):
print("ins.address: \t",ins.address) # 指令的地址
print("ins.mnemonic: \t",ins.mnemonic) # 指令的助记符(指令名称)
print("ins.op_str: \t",ins.op_str) # 指令的操作数
arm_ins ="B #0x40"
bytecode = ins2bcode(arm_ins)
bcode2ins(bytecode)
输出:
arm ins: B #0x40
bytecode hex: 0xea00000e
bytecode bin: 11101010000000000000000000001110
ins.address: 0
ins.mnemonic: b
ins.op_str: #0x40
c++filt
可以使用c++filt 命令解析 C++中被修饰的函数名
例如:
c++filt _ZN7CNumber7setNumlEi
输出:
CNumber::setNuml(int)
每个编译器都有一套自己内部的名字,比如对于linux下g++而言。以下是基本的方法: 每个方法都是以_Z开头,对于嵌套的名字(比如名字空间中的名字或者是类中间的名字,比如Class::Func)后面紧跟N , 然后是各个名字空间和类的名字,每个名字前是名字字符的长度,再以E结尾。(如果不是嵌套名字则不需要以E结尾)。