Arm从0开始
前言
记录一下异构的PWN题
🔺qemu 的 user 模式下,所以即便程序重启 libc 地址也不变,但是system模式,即加了内核的情况下就会改变了。
一、调试问题
1.ARM架构
pwndbg
对arm架构的支持还是挺好的
(1)无PIE
没有PIE的时候,直接使用patchelf将其链接器和搜索路径改一下即可
1 | patchelf --set-rpath /home/hacker/glibc/2.23/arm/ --set-interpreter /home/hacker/glibc/2.23/arm/lib/ld-linux.so.3 ./armNote |
然后通过qemu加载运行库启动
1 | qemu-arm -L /home/hacker/glibc/2.23/arm/ -g 12345 ./armNote |
之后通过gdb-multiarch连接上即可
1 | gdb-multiarch -q armNote |
这样就能调试了,包括pwndbg中的堆块heap,bins命令等也可以使用。
(2)有PIE
这个就比较复杂了,首先还是改掉用链接器和搜索路径,之后运行起来,查看vmmap
这里在stack的末尾,即图中蓝色框起来的就是elf基地址,我也不知道为什么,反正调试就是这样的,可能是QEMU和PIE的相关机制问题把。
之后通过elfBase来断点,再通过got表即可找到libc基地址了。
之后再通过add-symbol-file
即可得到地址了,地址由于qemu的关系,一直是不变的。
但是这里如果直接打印地址会出错,原因未知,需要重载符号表
重载符号表如下
可以看到之后就正确了。
不过还是没有办法进行bins,heap之类的命令。这个命令是pwndbg从__libc_malloc_initialized
这个全局符号获取的,但是添加了符号表之后这个全局符号还是没有被重新加载
导致无法识别,所以也就无法使用堆相关命令,不过可以从main_aren中简单看一下堆结构
2.MIPS架构
(1)无PIE
可以直接进行相应运行库加载即可,但是最好还是用gef插件。
(2)有PIE
pwndbg对mips架构的支持不太好,但是也由于gef关于qemu的vmmap命令不太准确,所以这里可以借助pwndbg的vmmap命令获取,如同之前的arm架构一样,获取到elf基地址之后即可查看对应的libc地址。
项目:
或者看项目,这个可以一键获取对应版本的glibc,在gdb调试中自动解析符号表和地址,方便做异构的pwn题。
mulArchAll
:https://github.com/PIG-007/mulArchAll.git
二、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
2.非叶子函数溢出
(1)栈模型
1 | #注释头 |
先压入LR,再压入FP,当前的FP指针指向的内容为LR保存的返回地址
(2)题目:
1 |
|
(3)利用:
一般而言都是不存在PIE的
A.后门
1 | backDoor = 0x10510 |
B.ROP链条
🔺注意:
不知道为啥,在我的GDB调试时,打印出来的函数地址和真实的函数地址不一样:
上面0x2101c是system的got表地址,可是和GDB打印的函数地址不一样,但是实际上libc.so文件没什么问题,因为可以正常getshell。之后咨询了lucky师傅之后,在室友的帮助下,编译了各个版本的arm架构下的glibc,再通过qemu加载运行库就正常了。
劫持栈模型:
1 | +-------------+ |
A.溢出字节不受限制
a.存在pop {r0,*, pc}
当有pop {r0,*, pc}
这种gadget,当然就是想怎么用就怎么用,但没有的时候可以用常见的CSU来替代。
b.利用csu
1 | #注释头 |
这里就是很正常的,通过①来为R4-R10赋值,以及控制PC跳转到②,再利用R5地址对应的值来赋值给R3对应跳转,期间也可通过R7-R8来控制R0-R2的参数。这里需要满足R5处保存的是got表地址,即将R5赋值为func_got_addr即可。
1 | def arm_csu(call,u_gadget1,u_gadget2,r0,r1,r2): |
这个是基于调用了system函数,所以system函数中的Got表中已经有了函数地址,同样的,当没有时需要泄露地址,通常调用puts函数或者printf函数即可泄露地址,如下相关脚本
1 | fake_FP = start_addr |
控制返回地址进入u_gadget1
然后控制R4~R10寄存器,跳转u_gadget2
然后跳转got表中函数
这里在调用CSU时也需要控制R6和R4,依据CMP R6, R4
,使得之后的BNE loc_10624
短跳转不成立,进入到csu中的最后的POP {R4-R10,PC}
语句,这时候再将栈控好,就能返回到start函数重新开始了。
现在的栈在我们最开始的时候已经通过以下语句控制好了,pop之后即可控制pc进入start函数重新开始。
1 | payload += p32(0)*7 |
泄露地址之后,如果程序中没有调用system,那么依然也无法通过ret2csu来getshell,但是可以通过下列的来代替:
1 | pop {r4, r5, r6, r7, r8, sb, sl, pc} #pop_R4_R10_PC |
而pop_R4_R10_PC
和movR0_R7_BLR3
都是csu中的,pop_r3_pc
则非常常用,基本都有。
这个就是先控R7,在控R3,最后R7赋给R0,跳转R3调用想调用的函数,这里就是system('/bin/sh')
。
1 | fake_FP = start_addr |
这里的system函数地址就得是泄露的真实地址了,因为赋值过程是直接寄存器赋值,而不是取寄存器值为地址再取值了。
这里的fake_FP基本没有什么用,但是下面介绍的就会有用了。
B.溢出字节受到限制
当溢出字节受到限制,比如只能溢出0x10个字节时。
参照inctf2018-warmup题目
a.利用read函数+shellcode
当调用了read函数时,且该arm架构使用qemu模拟,那么bss段基本都是可执行的。(具体原因未知)
那么调用read函数一般都是如下:
1 | .text:00010540 MOV R2, #0x100 ; nbytes |
这里就可以通过赋值给buf的语句来劫持R1,将shellcode读取到我们想放置的位置,当然,这之前还是得劫持R3寄存器,不过这个直接通过很常用的gadget即可。
1 | pop {r3, pc} #pop_r3_pc |
那么如下所示
1 | shellcode_addr = bss_addr + 0x4 |
这样即可读取shellcode到bss段了
不过这也需要原本的读取函数设置的buf长度足够放下我们的shellcode才行,已经成功读取
这里的fake_FP就起作用了,因为我们劫持了FP,所以在myFunc函数,即存在溢出函数返回时,通过
SUB SP, R11, #4
将FP减4之后赋值给SP,就能劫持栈了,
之后再通过POP {R11,PC}
,劫持PC,进入到shellcode执行。需要注意的是这里由于是pop给PC,所以bss段上还是需要放上对应shellcode处的地址,所以发送shellcode的语句为
1 | p.sendline(p32(shellcode_addr)+shellcode) |
最后成功getshell。
🔺存在PIE时
这种一般都是需要结合爆破来进行了,或者题目本身泄露地址。
3.叶子函数溢出
(1)栈模型
1 | #注释头 |
但是这样就不好利用,只能劫持到上一层函数的函数栈,那么通常会尝试劫持栈迁移一段距离,使得上一个非叶子函数的剩下的汇编代码所用到的栈上数据是我们伪造的栈中的数据,这样就能完成劫持上一个非叶子函数的返回地址。
(2)题目:
1 |
|
(3)利用:
直接劫持Last FP,然后利用上一层函数返回时劫持栈,在栈上保存shellcode地址,直接进入shellcode即可getshell。
1 | shellcode_addr = bss_addr + 0x20 |
pop {fp}之后已经劫持栈到bss段上了
返回main函数之后,利用main函数中的返回指令sub sp,fp,#4
完成栈劫持
然后就利用pop {fp, pc}
来劫持返回地址,进入到我们输入的bss段上shellcode的地方
注:
不在bss段上时,我们劫持完Last FP,返回上一层函数栈中时,当溢出长度足够,可直接修改上一层函数栈中的数据,相当于就是非叶子函数的溢出了。
至于溢出长度不够,只能劫持Last FP的时候,又无法泄露地址,感觉不太能搞,或者上上一层函数中有剩下可利用的数据,那么就部分劫持Last FP,利用上上一层函数中剩下的汇编来getshell,这个具体看题目。
4.格式化字符串
arm架构下的格式化字符串泄露的顺序是R1,R2,R3,栈,其他的相关利用方式也是类似,不再多说。
5.堆
堆的利用方法也是类似的。
参考:
ARM架构下的 Pwn 的一般解决思路 - 安全客,安全资讯平台 (anquanke.com)
Shellcode
1 |
|
三、MIPS
主要介绍mipsel架构的,小端序,大端的mips很少考到,而且也基本都是大同小异
1.流水线特点
以下指令的,strchr 函数的参数来自 $s0 而不是 $s2
1 | mov $a0, $s2; |
🔺注:流水线
即跳转之前会先一步加载下一条指令,所以在寻找ROP时,注意看下一步指令都是什么,不然跳转过去参数或者栈帧都有可能会出错的。
但是因为通常PWN题的MIPS通常是用qemu的user模式运行的,这个可能导致指令流水有时候表现不出来,所以不没有调用函数sleep(1)去将数据区刷新到指令区域也是可以拿到shell的;但是如果题目使用system模式部署,添加调用函数刷新数据区域再跳转到shellcode就很必要了,但如果是ROP来getshell倒是不用刷新数据。
2.栈机制
函数调用
1 | .text:00409A14 la $t9, sobj_del |
常见于库函数
通过
$t9
来跳转跳转指令为
jalr
,保存返回地址到$ra
,这里的返回地址即为1
.text:00409A24 lw $gp, 0x4C0+var_4B0($sp)
由于五级流水线,所以通常在跳转指令下一条指令加载参数,这里即为
1
.text:00409A20 move $a0, $s5
当然如果涉及到多个参数就会提前加载了
函数栈加载
叶子函数
叶子函数不需要将返回地址保存到栈上,所以一般只需要将参数放入栈中
1 | //栈空间开辟 |
有的也会有关于$gp
指针的设置,一般是用到了全局变量,常量什么的
1 | .text:00409480 lui $gp, 0x43 # 'C' |
非叶子函数
非叶子函数需要将返回地址保存到栈上
1 | .text:00409480 lui $gp, 0x43 # 'C' |
函数返回
叶子函数
叶子函数直接通过$ra
寄存器返回
1 | //....一些参数加载 |
非叶子函数
非叶子函数需要从栈中获取返回地址到$ra
寄存器,然后也是通过$ra
寄存器返回
1 | //一些处理工作.... |
总结:
总的来说,和x86也是有点相似,不同的就是关于$ra
寄存器的使用,以及叶子函数和非叶子函数的区别,还有一些五级流水线机制。
3.寄存器关系
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 | 暂时的(或随便用的),$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
返回。
4.指令
(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是一个Labelli:将立即数存入通用寄存器 eg:
li $t1, 40
$t1 = 40lw:从指定的地址加载一个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;复制返回地址到$ra中,即当前PC+8的值,程序跳转到sub_routine_label |
⑦子函数的返回
1 | jr $ra;如果子函数重复嵌套,则将$ra的值保存在堆栈中,因为$ra总是保存当前执行的子函数的返回地址 |
以上大多参考如下
MIPS汇编语言入门 - Sylvain’s Blog (valeeraz.github.io)
2021第四届强网拟态防御积分赛工控pwn eserver WP - 安全客,安全资讯平台 (anquanke.com)
5.常用ROP寻址
IDA中
1 | #mipsrop |
很多好用ROP的可在libc的scandir_tail类别函数下可以找到
Shellcode
三部曲
(1)gadget1
从sp控制寄存器
1 | lw ra,0x3c(sp); |
这个通常可以在scandir_tail
函数中找,或者如下
1 | mipsrop.find('jr $ra') |
(2)gadget2
从栈取地址给寄存器,然后跳转之前控制住的寄存器
1 | addiu a1,0x18(sp);move t9,s3;jalr t9 |
addiu指令即是取栈地址到a1寄存器,方便之后跳转,如下寻找指令
1 | mipsrop.find("addiu $a1,$sp") |
(3)gadget3
跳转保存栈地址的寄存器,进入shellcode
1 | move t9,a1;move a1,a2;jr t9 |
如下寻址指令
1 | mipsrop.find("move $t9, $s3") |
因为一般上基本不存在直接取栈地址然后跳转的,所以需要多重跳转来完成。
🔺可用shellcode:
(1)直接可用的
1 | shellcode = b"" |
(2)从栈上取数据的:
1 | shellcode1 = "\x1c\xfe\xa4\x8f" + \ |
对应的如下汇编代码:
1 | rasm2 -a mips -b 32 -C "lw a0,-0x1e4(sp)" #a |
6.调试
1 | set architecture mips |
下载库
异构下载库
1 | apt install crossbuild-essential-* |