异构ROP
利用
对于异构的rop与x86下其实并没有多大差异
注意
在CTF
比赛中,绝大多数异架构的题都是在qemu
模拟出的环境中跑的
而qemu
有些不太安全的特性,比如它没有地址的随机化,也没有NX
保护,即使题目所给的二进制文件开了NX
和PIE
保护,也只是对真机环境奏效,而在qemu
中跑的时候,仍然相当于没有这些保护
也就是说,qemu
中所有地址都是有可执行权限的(包括堆栈,甚至bss
段等),然后libc_base
和elf_base
每次跑都是固定的,当然这个固定是指在同一个环境下,本地跑和远程跑的这个固定值极有可能不相同,因此有时候打远程仍需泄露libc_base
这些信息(当然也可以选择爆破,一般和本地也就差一两位的样子)。
不过在比较新的版本qemu似乎支持这些保护,也就导致之前的任意shellcode失效
arm
利用
异构pwn主要也是rop利用,利用手法和x86并无多大差异
和x86比较不同的是函数调用的指令:
- x86采用call和ret完成函数调用,原理是把返回地址压栈
- 而arm采用b系列指令完成跳转,pop pc的方式回到父函数调用处
- b系列指令中的bl指令把返回地址存到了lr寄存器中,函数返回时把原来的lr寄存器的值弄到pc里
- 所以其实换汤不换药,x86和arm的思路都是差不多,只不过arm多了个lr寄存器,在叶子函数里省的把返回地址压栈了
注意
在CTF
比赛中,绝大多数ARM
架构的题都是在qemu
模拟出的环境中跑的
而qemu
有些不太安全的特性,比如它没有地址的随机化,也没有NX
保护,即使题目所给的二进制文件开了NX
和PIE
保护,也只是对真机环境奏效,而在qemu
中跑的时候,仍然相当于没有这些保护
也就是说,qemu
中所有地址都是有可执行权限的(包括堆栈,甚至bss
段等),然后libc_base
和elf_base
每次跑都是固定的,当然这个固定是指在同一个环境下,本地跑和远程跑的这个固定值极有可能不相同,因此有时候打远程仍需泄露libc_base
这些信息(当然也可以选择爆破,一般和本地也就差一两位的样子)。
例题
jarvisoj - typo
checksec
1 | aichch@sword-shield:~/桌面/pwn$ checksec typo |
未开启canary和pie
又因为是静态链接,所以完全可以在程序中寻找system(‘/bin/sh’)
程序去除了符号表,要想读明白伪代码并不容易
对于复杂一些的题目,可能需要利用bindiff之类的工具来恢复符号表
qemu-arm-statically -g 1234 ./typo
启动程序
在按下回车键后,程序会读入
1 | Let's Do Some Typing Exercise~ |
猜测有溢出,用pwntools验证
1 | from pwn import* |
程序果然崩溃,并得出偏移112
1 | Invalid address 0x62616164 |
之后就是构造rop,首先要找到system和/bin/sh字符串
/bin/sh字符串好找,但system函数因为去除符号表就有些难找了,不过我们能够利用/bin/sh字符串是被system调用的,来找到system
1 | .rodata:0006C384 2F unk_6C384 DCB 0x2F ; / ; DATA XREF: sub_10BA8+468↑o |
有时可能不会显示这个,应该是程序还没加载完,等待一会并重新进入刷新就会有了
得到system函数地址10ba8
接下来就是找gadget了
1 | ropper -f ./typo --search 'pop' --quality 1 |
我们选择这一条0x00020904: pop {r0, r4, pc};
于是exp:
1 | #coding:utf-8 |
2018 上海市大学生网络安全大赛 - baby_arm
首先看一下,文件相关信息
1 | arm: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=e988eaee79fd41139699d813eac0c375dbddba43, stripped |
动态链接,没开pie和canary
ida静态分析一下
程序很简单,首先是向bss段读取0x200的字符,然后再向栈中变量读入0x200字符,显然存在栈溢出
不过可以发现,第二个read也并不能修改当前函数的返回地址,因为它的读入地址要比返回地址更高
发现程序中有调用 mprotect
的代码段
因此可以有如下思路:
- 第一次输入 name 时,在 bss 段写上 shellcode
- 通过 rop 调用 mprotect 改变 bss 的权限
- 返回到 bss 上的 shellcode
exp:
1 | #coding:utf-8 |
以上是常规做法
但如果能够确定程序是在qemu中运行的,那么程序就没有nx保护,任意地址可执行
1 | #coding:utf-8 |
ret2csu
通常,在调用了 libc.so 的程序中,都会用到 __libc_csu_init() 这个函数来对libc进行初始化
在init中可以找到它
1 | .text:00000000004008AC |
先分析下面的loc_4008cc的内容
1 | LDP X19, X20, [SP,#var_s10] |
第一句这个LDP X19, X20, [SP,#var_s10]就是说将SP+0x10所指向的内容给x19和x20寄存器(x19寄存器拿的是SP+0x10所指向的内容,而x20寄存器拿的是SP+0x18所指向的内容)
然后第四句这个LDP X29, X30, [SP+var_s0],#0x40的意思是将SP所指向的内容给x29和x30寄存器(x29寄存器拿的是SP所指向的内容,而x30寄存器拿的是SP+0x8所指向的内容),完成这句指令之后,再将SP指针增加0x40个字节。
然后ret,这个就是返回到x30寄存器所存储的值。
再结合着刚刚分析的内容,来看一下loc_4008ac的内容。
1 | LDR X3, [X21,X19,LSL#3] |
第一句就是说将x19的值逻辑左移3位,然后加上x21的值,将得到的这个值所指向内容给x3寄存器。(如果我们控制x19的值为0的话,就是说把x21寄存器的值所指向的内容给x3寄存器。
然后剩下的mov,add就没什么好说的了。
倒数第三行BLR指令是去跳转到X3寄存器的值,同时把下一个指令的地址存到x30里面。
然后下面的CMP和x86里面的一样了。
如此思路就出来了,几乎是跟ret2csu的利用方法一样。有两点需要注意一下。
第一点就是loc_4008cc中的
LDP X29, X30, [SP+var_s0],#0x40 这个指令,虽然它是在这个loc_4008cc函数的最后,但是它传给x29和x30寄存器的时候,拿的是栈顶的值。因此布置栈中数据的时候,栈顶的内容应该是存放的x29和x30的值。
第二点,是BLR X3的时候,这个X3的值溯源一下,它是由X21充当指针来指向的,而X21的值又是SP+0x20充当指针来指向的。意思就是说,最终跳转的目标是x21指向的指针
inctf2018_wARMup
文件信息
1 | aichch@sword-shield:~/桌面/pwn$ checksec ./wARMup |
32位动态链接
这题主要就是利用由qemu运行的arm程序,尽管程序开启了nx
但实际运行时依然是任意地址可执行
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
read调用时
1 | .text:00010530 78 20 A0 E3 MOV R2, #0x78 ; 'x' ; nbytes |
由R3决定第二个参数,恰好程序中存在这样一条gadget
1 | .fini:000105C0 08 80 BD E8 POP {R3,PC} |
于是我们可以直接在bss段上写shellcode并执行
exp:
1 | from pwn import* |
这里有一个比较奇怪的点是,如果在调试模式下read的第二参数必须是bss+4才能正常写入到bss处起始
但直接执行模式下,又必须是bss
MIPS
利用
mips下的利用需要注意几点
- MIPS32 架构中是没有 EBP 寄存器的,程序函数调用的时候是将当前栈指针向下移动 n 比特到该函数的 stack frame 存储组空间,函数返回的时候再加上偏移量恢复栈
- 传参过程中,前四个参数a0−a3,多余的会保存在调用函数的预留的栈顶空间内
- MIPS 调用函数时会把函数的返回地址直接存入 $RA 寄存器
- MIPS的特殊性,在函数体中
$fp
和$sp
是相同的,即都指向栈顶 - 由于mips的特殊性,在ROP过程中非常容易搞出来类似在x86上的
jmp esp
的指令 - mips本身不支持NX,与arm是因为qemu的关系不同
- mips跳转到完整函数,一定要使用
$t9
寄存器
最后两条使得ret2shellcode是十分有效的攻击方式
例题
HWS入营赛题mplogin
checksec没有任何保护机制
1 | [*] '/home/aichch/pwn/Mplogin/Mplogin' |
在sub_400840函数中
1 | int sub_400840() |
如果v1的长度填满的话那么%s就会把后面的栈地址一起打印出来
此时就可以泄露栈了
然后在sub_400978函数中
存在溢出,覆盖返回地址为栈,ret2shellcode
1 | rom pwn import* |
HWS结营赛题pwn
1 | ➜ file pwn |
MIPS大端,静态链接
因为这里没有地方泄露栈的地址,所以只能使用ROP来构造类似jmp esp
的指令
在0x004273C4处有一条gadget
addiu $a2,$sp,0x64; jalr $s0
这个gadget会将$sp寄存器的值加上0x64放到$a2寄存器中,然后跳转到$s0寄存器中的地址去执行。那么如果我们能控制$s0寄存器的值指向一个跳转$a2的gadget,然后在$sp+0x64
栈地址上布置shellcode即可利用成功。于是我们需要完成以下操作:
- 找到能跳转到$a2的gadget
- 控制$s0寄存器到如上gadget
- 在
$sp+0x64
的栈地址上布置shellcode
可以找到:
Address | Action | Control Jump |
---|---|---|
0x00421684 | move $t9,$a2 | jr $a2 |
现在还需要解决一个问题,如何控制$s0?
这个在前文的MIPS基础知识中提到过,在MIPS的复杂函数的序言和尾声中,会保存和恢复s组寄存器,我们可以下pwn()
函数尾声的汇编代码:
1 | .text:00400A2C move $sp, $fp |
故我们之前溢出时,在0x90
控制了$ra
,则我们在0x90-0x7c+0x58=0x6c
处,即可控制$s0
布置shellcode
因为在函数的尾声处会把栈空间收回:
1 | .text:00400A58 addiu $sp, 0x80 |
故我们控制栈地址到$s2寄存器的值也是回收之后的栈空间,故这个栈空间就是溢出返回地址之后的栈空间,故我们的gadget是$sp+0x64
,直接在溢出点后的0x64位置处拼接shellcode即可,故完整exp如下:
1 | from pwn import * |
PowerPC
例题
UTCTF2019 PPC
查看基本信息,发现程序是静态编译且没有任何保护机制
1 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) |
get_input向bss段上的全局变量buf读取1000个字节
前面都没有漏洞点,但在encrypt函数中
1 | void encrypt_0(char *block, int edflag) |
从buf处赋值内存到栈上上,显然发生溢出
因为没有开启nx,因此直接在buf上写入shellcode,查找溢出长度后直接返回到buf处
另外为了绕过异或检测,可以shellcode之前填上几个’\0’截断strlen函数
由于ppc结构没有类似push,pop的操作,所有栈都是由编译器直接指定,所以没办法直接生成getshell的shellcode,需要自己写
exp:
1 | #!/usr/bin/env python |
2021hws-ppppppc
1 | [*] '/home/aichch/pwn/PPPPPPC' |
静态编译无任何保护
去除了符号表,不过可以根据字符串查找到main函数
1 | int sub_10000464() |
发现就是就是栈溢出漏洞
远程环境一定是qemu,直接ret2shellcode,搜索内存发现两段内存里存着发过去的数据,注意要用栈上的shellcode,拷贝到数据段的shellcode会被截断。
当内存错误时,会打印当前状态,其中包含栈信息,由于是qemu,所以每次不变,故泄露一次,下一次攻击用即可
exp:
1 | from pwn import * |