汇编基础
前言
复习一下汇编
一、X86
1.汇编指令
(1)跳转
依据ZF(Zero)、SF(Symbol))、OF(Overflow)、PF(Parity)、CF(Carry)五种状态标志位有对应的跳转JZ(JNZ)/JS(JNS)等五种跳转
状态标志位除了以上五种还有AF(Assistant)辅助进位
此外还有Jmp无条件跳转
(2)标志位
以上的六种状态标志位外,还有以下三种标志位
TF(trap):跟踪标答志位:单步标志
IF(interregnum):中断标志位
DF(direction):方向标志位
2.寄存器
(1)传参
64位:rdi rsi rdx rcx r8 r9 栈
32位:栈传参
(2)栈指针
RSP、RBP
二、ARM
1.寄存器关系
R0
R3:函数调用参数,代表第04个参数,剩下的参数从右向左依次入栈,函数返回值保存在R0中。(对应在aarch64中为R0R7,但是gdb调试或者IDA中一般显示X0X30,同时还有低32位的W0~W30)SP:类似rsp,esp,栈指针
FP:类似ebp,栈指针
LR:当发生函数调用时,会保存调用函数处的地址,退出函数时赋值给PC。
PC:类似eip,rip,存储下一条指令的地址。
R11:类似ebp,栈指针,其实就差不多是FP
2.汇编指令
(1)寄存器传送MOV
寄存器之间还是MOV指令,比如MOV R2,R0;但是如果涉及到内存上的,则需要先加载到寄存器中,或者把寄存器中的值存储到内存中
(2)寄存器-内存传送
STR(store reg)/LDR(load reg)以及STM(store multiple)/LDM(load multiple)
①STR/LDR模式
STR Ra [Rb]:
Ra中的数据存储到Rb指向的内存中
LDR Ra [Rb]
Rb指向的内存中的值加载到Ra中
此外还有如下情形
1 | STR R0,[R1, #12] // R0 --> [R1+12] |
②STM/LDM模式
STM R0, {R4,R5}
R4的值传送给R0+x对应的内存单元,然后R5的值传给R0+x+x对应的内存单元
这个x即由后缀决定,分别有以下几种
DB(Decrease Before):每次传送前R0加上x,x为负数
DA(Decrease After):每次传送后R0加上x,x为负数
IB(Increase Before):每次传送前R0加上x,x为正数
IA(Increase After):每次传送后R0加上x,x为正数
而X则在不同的CPU位数下不同,32为4,64则为8。
此外堆栈的增长方向可以不同,也可以在不同情形下作为后缀,效果一样的
- FD(Full Descent):满递减堆栈
- FA(Full Ascent):满递增堆栈
- ED(Empty Descent):空递减堆栈
- EA(Empty Ascent):空递增堆栈
满则代表栈指针指向栈顶,空则代表栈指针不指向栈顶
(3)跳转指令
①分支跳转(Branch)
B
无条件跳转
BX <Rm>
由寄存器给出地址
若 Rm 的 bit[0] 为1,切换到 Thumb 指令执行;
若 Rm 的 bit[0] 为0,切换到 ARM 指令执行。
BL
类似于Call,会存入返回地址到LR寄存器
BLX/BLR
即类似对应组合
②条件跳转
依据CPSR寄存器下的ALU状态标志位
CPSR寄存器包含下面的ALU状态标志:
N Set when the result of the operation was Negative(结果为负数)
Z Set when the result of the operation was Zero(结果为0)
C Set when the operation result in a Carry(发生进位,或借位)
V Set when the operation caused oVerflow(操作造成溢出)
Q ARM architecture v5E only sticky flag
还有BEQ、BNE
三、MIPS
主要介绍mipsel架构的,小端序,大端的mips很少考到,而且也基本都是大同小异
1.寄存器关系
REGISTER | NAME | USAGE |
---|---|---|
$0 | $zero | 常量0(constant value 0) |
$1 | $at | 保留给汇编器(Reserved for assembler) |
$2-$3 | $v0 - $v1 | 函数调用返回值(values for results and expression evaluation) |
$4-$7 | $a0-$a3 | 函数调用参数(arguments) |
$8-$15 | $t0-$t7 | 暂时的(或随便用的) |
$16-$23 | $s0-$s7 | 保存的(或如果用,需要SAVE/RESTORE的)(saved) |
$24-$25 | $t8-$t9 | 暂时的(或随便用的) |
$28 | $gp | 全局指针(Global Pointer) |
$29 | $sp | 堆栈指针(Stack Pointer) |
$30 | $fp | 帧指针(Frame Pointer) |
说明
- $0
即$zero,该寄存器总是返回零,为0这个有用常数提供了一个简洁的编码形式。
1 | move $t0,$t1 |
使用伪指令可以简化任务,汇编程序提供了比硬件更丰富的指令集。
- $1
即 $at,该寄存器为汇编保留,由于I型指令的立即数字段只有16位,在加载大常数时,编译器或汇编程序需要把大常数拆开,然后重新组合到寄存器里。比如加载一个32位立即数需要 lui
(装入高位立即数)和addi
两条指令。像MIPS程序拆散和重装大常数由汇编程序来完成,汇编程序必需一个临时寄存器来重组大常数,这也是为汇编保留$at的原因之一。
- $2..$3:($v0-$v1)
用于子程序的非浮点结果或返回值,对于子程序如何传递参数及如何返回,MIPS范围有一套约定,堆栈中少数几个位置处的内容装入CPU寄存器,其相应内存位置保留未做定义,当这两个寄存器不够存放返回值时,编译器通过内存来完成。
- $4..$7:($a0-$a3)
用来传递前四个参数给子程序,不够的用堆栈。a0-a3和v0-v1以及ra一起来支持子程序/过程调用,分别用以传递参数,返回结果和存放返回地址。当需要使用更多的寄存器时,就需要堆栈(stack)了,MIPS编译器总是为参数在堆栈中留有空间以防有参数需要存储。
- $8..$15:($t0-$t7)
临时寄存器,子程序可以使用它们而不用保留。
- $16..$23:($s0-$s7)
保存寄存器,在过程调用过程中需要保留(被调用者保存和恢复,还包括$fp和$ra),MIPS提供了临时寄存器和保存寄存器,这样就减少了寄存器溢出(spilling,即将不常用的变量放到存储器的过程),编译器在编译一个叶(leaf)过程(不调用其它过程的过程)的时候,总是在临时寄存器分配完了才使用需要保存的寄存器。
- $24..$25:($t8-$t9)
同($t0-$t7) $26..$27:($k0,$k1)为操作系统/异常处理保留,至少要预留一个。 异常(或中断)是一种不需要在程序中显示调用的过程。MIPS有个叫异常程序计数器(exception program counter,EPC)的寄存器,属于CP0寄存器,用于保存造成异常的那条指令的地址。查看控制寄存器的唯一方法是把它复制到通用寄存器里,指令mfc0
(move from system control)可以将EPC中的地址复制到某个通用寄存器中,通过跳转语句(jr
),程序可以返回到造成异常的那条指令处继续执行。MIPS程序员都必须保留两个寄存器$k0和$k1,供操作系统使用。
发生异常时,这两个寄存器的值不会被恢复,编译器也不使用k0和k1,异常处理函数可以将返回地址放到这两个中的任何一个,然后使用jr跳转到造成异常的指令处继续执行。
- $28:($gp)
为了简化静态数据的访问,MIPS软件保留了一个寄存器:全局指针gp(global pointer,$gp),全局指针指向静态数据区中的运行时决定的地址,在存取位于gp值上下32KB范围内的数据时,只需要一条以gp为基指针的指令即可。在编译时,数据须在以gp为基指针的64KB范围内。
- $29:($sp)
MIPS硬件并不直接支持堆栈,你可以把它用于别的目的,但为了使用别人的程序或让别人使用你的程序,还是要遵守这个约定的,但这和硬件没有关系。
- $30:($fp)
GNU MIPS C编译器使用了帧指针(frame pointer),而SGI的C编译器没有使用,而把这个寄存器当作保存寄存器使用($s8),这节省了调用和返回开销,但增加了代码生成的复杂性。
- $31:($ra)
存放返回地址,MIPS有个jal
(jump-and-link,跳转并 链接)指令,在跳转到某个地址时,把下一条指令的地址放到$ra中。用于支持子程序,例如调用程序把参数放到$a0~$a3,然后jal X
跳到X过程,被调过程完成后把结果放到$v0,$v1,然后使用jr $ra
返回。
2.汇编指令
(1)不同类型的指令
R型指令:操作寄存器
常见指令:add
, sub
, and
, or
, nor
, slt
, sll
, srl
, jr
- opcod:操作码
- rs:源寄存器的数量
- rt:源寄存器的数量
- rd:目标寄存器的编号
- sham:移位量(操作移位的位数)
- Func: 函数(操作码扩展)
Sham仅用于操作偏移量的指令(例如stl
)
I型指令:操作常量
常见指令:addi
, lw
, sw
, lh
, sh
, lb
, lbu
, sb
, ll
, sc
, lui
, andi
, ori
, beq
, bne
, slti
, sltiu
- opcod:操作码
- rs:源寄存器的数量
- rd:源或目标寄存器的数量
- imd: 立即值
J型指令:跳跃寻址
处理器直接跳跃到给定的地址,执行所在地址的指令
常见指令:j
, jal
- Opcod:操作码
- Imd:立即值
(2)常用指令
其中i
表示立即数相关,u
表示无符号相关。
①load/store 指令
la
将地址或者标签存入一个寄存器 eg:la $t0,val_1
复制val_l的地址到$t0中,val_1是一个Label
li
将立即数存入通用寄存器 eg:li $t1, 40
$t1 = 40
lw
从指定的地址加载一个word类型的值到一个寄存器 eg:lw $s0, 0($sp) $s0=MEM[$sp+0]
sw
将寄存器的值,存于指定的地址word类型 eg:sw $a0, 0($sp) MEM[$sp+0] = $a0
move
寄存器传值 eg:move $t5, $t2 $t5 = $t2
②算数指令
算数指令的所有操作都是寄存器,不能是 RAM 地址或间接寻址。且操作数大小都是 word(4byte)
1 | add $t0, $t1, $t2; $t0=$t1+$t2; 带符号数相加 |
③syscall
产生一个软化中断,实现系统调用;系统调用号存放在 $v0 中,参数在 $a0~$a3 中;
返回值在 $v0 中,如果出错,在 $a3 中返回错误号;在编写 shellcode 时,用到该指令机制
Write(1, “ABCn”, 5) 实现如下
1 | addiu $sp, $sp, -32; |
④分支跳转指令
分支跳转指令本身可以通过比较两个寄存器决定如何跳转;如果想要实现与立即数的比较跳转,需要结合类跳转指令实现
1 | b target; 无条件跳转到target处 |
⑤跳转指令
1 | j target; 无条件跳转target |
⑥子函数的调用
1 | jal sub_routine_label;复制当前PC的值到$ra中,(当前PC值就是返回地址)程序跳转到sub_routine_label |
⑦子函数的返回
1 | jr $ra;如果子函数重复嵌套,则将$ra的值保存在堆栈中,因为$ra总是保存当前执行的子函数的返回地址 |
以上大多参考如下
MIPS汇编语言入门 - Sylvain’s Blog (valeeraz.github.io)
2021第四届强网拟态防御积分赛工控pwn eserver WP - 安全客,安全资讯平台 (anquanke.com)
3.流水线特点
以下指令的,strchr 函数的参数来自 $s0 而不是 $s2
1 | mov $a0, $s2; |
🔺注:流水线
即跳转之前会先一步加载下一条指令,所以在寻找ROP时,注意看下一步指令都是什么,不然跳转过去参数或者栈帧都有可能会出错的。
但是因为通常PWN题的MIPS通常是用qemu的user模式运行的,这个可能导致指令流水有时候表现不出来,所以不没有调用函数sleep(1)去将数据区刷新到指令区域也是可以拿到shell的;但是如果题目使用system模式部署,添加调用函数刷新数据区域再跳转到shellcode就很必要了,但如果是ROP来getshell倒是不用刷新数据。