CTF/해외CTF

Hack the Box CTF 2022

Ron Weasley 2022. 10. 23. 08:46

2022년 10월 23일 참가했던 CTF이다. 바로 풀이를 시작하겠습니다.

[Wrong Spooky Season]

문제를 한번 보겠습니다. (사진이 없어 글로 대체하겠습니다)

"I told them it was too soon and in the wrong season to deploy such a website, but they assured me that theming it properly would be enough to stop the ghosts from haunting us. I was wrong." Now there is an internal breach in the `Spooky Network` and you need to find out what happened. Analyze the the network traffic and find how the scary ghosts got in and what they did.

문제를 해석을 해보면, 네트워크 패킷 파일에서 수상한 패킷을 찾고 플래그를 찾는 그런 문제이다.

패킷을 보면, 3 - way -handshake 형태로 정상적인 패킷으로 보이고, GET으로 서버 리소스를 요청하는 아주 정상적인 패킷인 것을 확인할 수 있다.
HTTP 프로토콜이 있어, 가장 먼저 분석을 진행한 것은 HTTP List이다.

HTTP Object list를 보니 할로윈과 연관된 유령 jpg 파일들이 엄청 많이 있었고 전부다 카빙을 진행 했다.

하지만 사진에서는 정답과 관련된 내용들이 일절 없고 그냥...할로윈인 것을 보여주는 사진들 밖에 없었기 때문에 다른 패킷을 분석하였다.

TCP Stream의 10번 패킷을 보면, root로 된 데이터를 하나 볼 수 있는데 뭔가 수상해서 다른 TCP Stream도 분석을 진행 했다.

루트의 uid, gid, groups과 관련된 정보들도 있다.

그리고 업그레이드를 하는 패킷도 볼 수 있는데, 이 패킷을 보면 네트워크 패킷을 잡은 운영체제가 데비안 기반으로 된 칼리 리눅스임을 알 수 있었다.

하나씩 따라가던 중, TCP Stream 14번에서 passwd 파일을 출력하는 명령어를 실행하는 것을 볼 수 있는데, 조금더 내려보면 다음과 같은 패킷들이 잡혀있다.

find로 된 명령어를 해석해보면, 퍼미션이 setuid로 된 파일을 찾는 것이다.그리고 하단에 echo로 명령어를 실행하는데, socat으로 192.168.1.180 아이피에 데이터를 전송하는것을 볼 수 있다.그래서 데이터 중 ==gC9~~~~로 된 부분이 base64로 인코딩된 문자열이 reserved된 것을 확인할 수 있었고 다음 코드를 짜서 뒤집어 주면 된다.

a = "==gC9FSI5tGMwA3cfRjd0o2Xz0GNjNjYfR3c1p2Xn5WMyBXNfRjd0o2eCRFS"
b = "".join(reversed(a))

print(b)

뒤집어서 나온 값을 가지고 base64 디코딩을 하면 플래그를 찾을 수 있다.

 

[Trick or Breach]

문제를 한번 보겠습니다.

woods invented a potion to bring pumpkins to life, but in a more up-to-date approach. Unfortunately, we learned that malicious actors accessed our network in a massive cyber attack. Our security team found that the hack had occurred when a group of children came into the office's security external room for trick or treat. One of the children was found to be a paid actor and managed to insert a USB into one of the security personnel's computers, which allowed the hackers to gain access to the company's systems. We only have a network capture during the time of the incident. Can you find out if they stole the secret project?

공격을 당했고, 네트워크 패킷으로 어떤 프로젝트를 훔쳤는지 알아봐달라고 하면서 패킷 파일을 하나 던져줍니다.
패킷을 한번 보겠습니다.

DNS 패킷들이 엄청 많이 잡혀있는 것을 볼 수 있고, 쿼리로 찍힌 데이터가 504b0304인 것으로 보아 pkzip 형식의 파일임을 알 수 있습니다.
데이터를 추출하기 위해 tshark를 사용했습니다.

tshark -r capture_1.pcap -T fields -e dns.qry.name > out.txt

위 코드를 입력하면 out.txt가 나오는데, 안에 데이터를 살펴보면 2개씩 찍혀있는 것을 볼 수 있습니다.

1개씩 다 지우고, pumpkincorp.com을 지우고 zip 확장자로 바꿔주면 다음과 같은 파일들이 보입니다.

안에 있는 데이터들 중, 하나씩 hxd값으로 열어보니 플래그를 찾을 수 있었습니다.

 

[POOF]

문제를 보겠습니다.

In my company, we are developing a new python game for Halloween. I'm the leader of this project; thus, I want it to be unique. So I researched the most cutting-edge python libraries for game development until I stumbled upon a private game-dev discord server. One member suggested I try a new python library that provides enhanced game development capabilities. I was excited about it until I tried it. Quite simply, all my files are encrypted now. Thankfully I manage to capture the memory and the network traffic of my Linux server during the incident. Can you analyze it and help me recover my files? To get the flag, connect to the docker service and answer the questions.

이 문제는, 게임을 개발하려고 라이브러리를 설치했다가 랜섬에 감염되는 시나리오입니다.

그리고 문제 풀이를 위해서 도커도 생성된 것을 볼 수 있습니다. 한번 들어가볼까요?

nc 139.59.167.169 31208

이렇게 하나씩 질문에 대한 답을 해결하면서 플래그를 찾는 문제 유형임을 알 수 있습니다. 이제 풀이를 시작하도록 하겠습니다.

주어진 문제 파일들을 살펴보면, 다음과 같습니다.

랜섬에 감염된 pdf파일, 메모리 덤프 파일, pcap 파일, 리눅스 generic 파일 총 4개가 들어있습니다.
일단 메모리 덤프 파일에 리눅스 generic 파일을 준 것으로 보아, 리눅스 메모리 포렌식임을 알 수 있습니다.
해당 zip 파일을 /volatility/volatility/plugins/overlays/linux/ 경로에 넣어주시고 다음 명령어를 입력하시면 됩니다.

python vol.py --info

입력을 하시면 volatility가 인식을 하게 되어, 리눅스 메모리 포렌식의 준비가 되었습니다.

이렇게 나오시면 정상적으로 인식을 한 겁니다.
프로세스를 보기 위해서 linux_pslist 플러그인을 사용하겠습니다.

python vol.py -f mem2.dmp --profile=LinuxUbuntu_4_15_0-184-generic_profilex64 linux_pslist

 

cofigure이라는 프로세스 명을 가진 친구가 bash로 인해서 실행이 된 것으로 보아, 터미널에서 입력했던 명령어 히스토리를 보겠습니다.

python vol.py -f mem2.dmp --profile=LinuxUbuntu_4_15_0-184-generic_profilex64 linux_bash

딱 느낌이 바로 왔습니다. wget으로 pygaming-dev-13.37.tar.gz을 다운 받아서 압축을 풀고 configure을 실행시킨 후 랜섬에 감염된 시나리오입니다.
문제를 풀기위해 아까 접속했던 도커를 보면, 랜섬웨어를 다운 받은 url 경로를 묻는 문제가 있습니다. 거기에다가 bash로 나온 http ~~를 입력해주시면 됩니다.

문제를 풀고나면 또 다음 문제가 주어지는 형태이기 때문에 다음 문제를 한번 보겠습니다.

malware의 프로세스 이름을 묻는거기 때문에 아까 configure를 실행했을 때, 감염이 되었으니까 configure이 됩니다.

다음 문제를 보겠습니다.

랜섬웨어 파일의 md5를 찾는 문제인데, 여기서부터는 제가 헤맸던 부분까지 적을 것이여서 조금 내용이 길 수 있음을 알려드립니다.
랜섬웨어를 찾기 위해서, linux_find_file 명령어로 파일의 메모리 주소를 파악했습니다.

python vol.py -f mem2.dmp --profile=LinuxUbuntu_4_15_0-184-generic_profilex64 linux_find_file -L > filescan.txt

-L 옵션은 모든 리스트를 출력하라는 것이고, txt로 파일을 받아서 configure을 검색했습니다.

검색이 된 로그들을 살펴보면, 누가봐도 시나리오와 동일한...친구들이 보이네요..!!
그래서 일단, configure을 추출했습니다.

추출이 완료되어 md5sum을 체크했습니다.

md5sum configure

이제 저 md5 해쉬값을 답으로 제출을 넣어봤지만, 정답이 아니였습니다.

왜냐하면, 해당 파일을 hxd 열어봤을 때, elf 파일이긴 하지만, 80프로 이상이 NULL 값으로 추출이 되었기 때문입니다.

그래서 문제를 만든 제작자가 wget으로 gz을 다운받아서 압축을 해제했으니까, 같은 명령어로 gz을 받고 압축을 해제하면 될 것이라는 생각이 떠올라 바로 해봤습니다.

wget http://files.pypi-install.com/packages/a5/61/caf3af6d893b5cb8eae9a90a3054f370a92130863450e3299d742c7a65329d94/pygaming-dev-13.37.tar.gz

 

gz파일이 다운 받아지는 것을 볼 수 있습니다.

확인 완료 후, 동일한 명령어로 압축을 해제하겠습니다.

tar -xf pygaming-dev-13.37.tar.gz

tar 파일이 아니라고...되어있네요...진짜 file로 확인을 해보니 아니라고 뜹니다.

file pygaming-dev-13.37.tar.gz

여기서부터 이제 멘붕의 시작이였으나...해결을 했으니, 바로 풀어야죠?? ㅎㅎㅎ
아까 처음에 zip파일을 풀었던 파일중, pcap파일이 하나 있는데, 이걸 열면 원본 파일을 구할 수 있습니다.

여기서 파일을 카빙 후, gz으로 완성을 시켜야 하기 때문에, raw로 변경하여 가져오겠습니다.
gz의 시그니처는 1F 8B 08이기 때문에 이렇게 하시면 됩니다.

파일 이름을 .gz으로 바꾸고 저장을 하면 됩니다.

제대로 카빙을 했으면, 이렇게 나옵니다!
안에 있는 파일을 추출하면, 이제 원래 랜섬웨어 파일이 나오게됩니다. 속성을 확인하여 md5를 구하고 넣어주면??

또 틀렸다고 뜹니다... 도대체 뭐가 문제일까...해보니 아까 우리가 리눅스에서 추출했던 configure파일이랑 지금 추출한 파일의 크기를 비교해야됩니다.

보시면, 왼쪽이 pcap파일에서 추출한 파일 오른쪽이 리눅스에서 추출한 파일입니다.
파일의 크기가 다른것을 확인할 수 있습니다. 그래서...구조적으로 맞춰줘야 하기 때문에 수정을 조금 했습니다.

수정하는 방법이 조금 까다롭기 때문에, 이해가 어려우실 수 있지만 하나씩 해보겠습니다.
먼저, pcap에서 추출한 파일 시그니처를 보면 elf가 아니기 때문에 elf가 시작되는 위치까지 데이터를 날려줘야합니다.

다음은 제일 하단에, NULL값이 되어있는 놈들 지워야 하는데 본 파일인 configure의 총 오프셋을 한번 보겠습니다.

0x727317이니까, 똑같이 추출한 파일도 맞춰줍시다.

그리고 저장하고 md5를 확인하겠습니다.

저 값을 집어넣으면 Corrcet가 뜨는것을 확인할 수 있습니다.

다음 문제를 보겠습니다.

어떤 프로그래밍 언어로 되어있냐...라고 적혀있네요??
아까 복원을 했던 랜섬웨어 파일을 IDA로 까봅시다.

main 함수(sub_403DD0) 를 보면, PYI_PROCNAME이라는 파라미터도 볼 수 있고, 조금 더 들어가보면 sub_403B00이 있는데, 안에 들어있는 로직을 보면 다음과 같습니다.

.py로 된 파일을 가지고 어떠한 행동을 하는 로직을 볼 수 있습니다. (코드 분석을 안했기 때문에 정확히 어떤 로직인지는 다음에 작성)
결국, py를 건든다는 것을 보면 파이썬 코드로 된 랜섬웨어임을 알 수 있습니다. 그래서 답은 파이썬이 됩니다.

다음 문제를 한번 보겠습니다.

랜섬웨어 파일을 디컴파일 하여 암호화를 시키는 함수를 찾는건데, 원래 이 문제는 기능만 찾는 문제였습니다.
하지만 제가 어드민에게 신고를 하여 문제 수정이 이루어진 문제입니다.

이렇게 말이죠...ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 해프닝..!
파이썬으로 짜여진 실행파일이라 했으니 pyinstaller로 만들어진 파일이고, 디컴파일을 할려고 오픈소스를 좀 가지고 왔습니다.

from __future__ import print_function
import os
import struct
import marshal
import zlib
import sys
from uuid import uuid4 as uniquename


class CTOCEntry:
    def __init__(self, position, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name):
        self.position = position
        self.cmprsdDataSize = cmprsdDataSize
        self.uncmprsdDataSize = uncmprsdDataSize
        self.cmprsFlag = cmprsFlag
        self.typeCmprsData = typeCmprsData
        self.name = name


class PyInstArchive:
    PYINST20_COOKIE_SIZE = 24           # For pyinstaller 2.0
    PYINST21_COOKIE_SIZE = 24 + 64      # For pyinstaller 2.1+
    MAGIC = b'MEI\014\013\012\013\016'  # Magic number which identifies pyinstaller

    def __init__(self, path):
        self.filePath = path
        self.pycMagic = b'\0' * 4
        self.barePycList = [] # List of pyc's whose headers have to be fixed


    def open(self):
        try:
            self.fPtr = open(self.filePath, 'rb')
            self.fileSize = os.stat(self.filePath).st_size
        except:
            print('[!] Error: Could not open {0}'.format(self.filePath))
            return False
        return True


    def close(self):
        try:
            self.fPtr.close()
        except:
            pass


    def checkFile(self):
        print('[+] Processing {0}'.format(self.filePath))

        searchChunkSize = 8192
        endPos = self.fileSize
        self.cookiePos = -1

        if endPos < len(self.MAGIC):
            print('[!] Error : File is too short or truncated')
            return False

        while True:
            startPos = endPos - searchChunkSize if endPos >= searchChunkSize else 0
            chunkSize = endPos - startPos

            if chunkSize < len(self.MAGIC):
                break

            self.fPtr.seek(startPos, os.SEEK_SET)
            data = self.fPtr.read(chunkSize)

            offs = data.rfind(self.MAGIC)

            if offs != -1:
                self.cookiePos = startPos + offs
                break

            endPos = startPos + len(self.MAGIC) - 1

            if startPos == 0:
                break

        if self.cookiePos == -1:
            print('[!] Error : Missing cookie, unsupported pyinstaller version or not a pyinstaller archive')
            return False

        self.fPtr.seek(self.cookiePos + self.PYINST20_COOKIE_SIZE, os.SEEK_SET)

        if b'python' in self.fPtr.read(64).lower():
            print('[+] Pyinstaller version: 2.1+')
            self.pyinstVer = 21     # pyinstaller 2.1+
        else:
            self.pyinstVer = 20     # pyinstaller 2.0
            print('[+] Pyinstaller version: 2.0')

        return True


    def getCArchiveInfo(self):
        try:
            if self.pyinstVer == 20:
                self.fPtr.seek(self.cookiePos, os.SEEK_SET)

                # Read CArchive cookie
                (magic, lengthofPackage, toc, tocLen, pyver) = \
                struct.unpack('!8siiii', self.fPtr.read(self.PYINST20_COOKIE_SIZE))

            elif self.pyinstVer == 21:
                self.fPtr.seek(self.cookiePos, os.SEEK_SET)

                # Read CArchive cookie
                (magic, lengthofPackage, toc, tocLen, pyver, pylibname) = \
                struct.unpack('!8siiii64s', self.fPtr.read(self.PYINST21_COOKIE_SIZE))

        except:
            print('[!] Error : The file is not a pyinstaller archive')
            return False

        self.pymaj, self.pymin = (pyver//100, pyver%100) if pyver >= 100 else (pyver//10, pyver%10)
        print('[+] Python version: {0}.{1}'.format(self.pymaj, self.pymin))

        # Additional data after the cookie
        tailBytes = self.fileSize - self.cookiePos - (self.PYINST20_COOKIE_SIZE if self.pyinstVer == 20 else self.PYINST21_COOKIE_SIZE)

        # Overlay is the data appended at the end of the PE
        self.overlaySize = lengthofPackage + tailBytes
        self.overlayPos = self.fileSize - self.overlaySize
        self.tableOfContentsPos = self.overlayPos + toc
        self.tableOfContentsSize = tocLen

        print('[+] Length of package: {0} bytes'.format(lengthofPackage))
        return True


    def parseTOC(self):
        # Go to the table of contents
        self.fPtr.seek(self.tableOfContentsPos, os.SEEK_SET)

        self.tocList = []
        parsedLen = 0

        # Parse table of contents
        while parsedLen < self.tableOfContentsSize:
            (entrySize, ) = struct.unpack('!i', self.fPtr.read(4))
            nameLen = struct.calcsize('!iiiiBc')

            (entryPos, cmprsdDataSize, uncmprsdDataSize, cmprsFlag, typeCmprsData, name) = \
            struct.unpack( \
                '!iiiBc{0}s'.format(entrySize - nameLen), \
                self.fPtr.read(entrySize - 4))

            name = name.decode('utf-8').rstrip('\0')
            if len(name) == 0:
                name = str(uniquename())
                print('[!] Warning: Found an unamed file in CArchive. Using random name {0}'.format(name))

            self.tocList.append( \
                                CTOCEntry(                      \
                                    self.overlayPos + entryPos, \
                                    cmprsdDataSize,             \
                                    uncmprsdDataSize,           \
                                    cmprsFlag,                  \
                                    typeCmprsData,              \
                                    name                        \
                                ))

            parsedLen += entrySize
        print('[+] Found {0} files in CArchive'.format(len(self.tocList)))


    def _writeRawData(self, filepath, data):
        nm = filepath.replace('\\', os.path.sep).replace('/', os.path.sep).replace('..', '__')
        nmDir = os.path.dirname(nm)
        if nmDir != '' and not os.path.exists(nmDir): # Check if path exists, create if not
            os.makedirs(nmDir)

        with open(nm, 'wb') as f:
            f.write(data)


    def extractFiles(self):
        print('[+] Beginning extraction...please standby')
        extractionDir = os.path.join(os.getcwd(), os.path.basename(self.filePath) + '_extracted')

        if not os.path.exists(extractionDir):
            os.mkdir(extractionDir)

        os.chdir(extractionDir)

        for entry in self.tocList:
            self.fPtr.seek(entry.position, os.SEEK_SET)
            data = self.fPtr.read(entry.cmprsdDataSize)

            if entry.cmprsFlag == 1:
                data = zlib.decompress(data)
                # Malware may tamper with the uncompressed size
                # Comment out the assertion in such a case
                assert len(data) == entry.uncmprsdDataSize # Sanity Check

            if entry.typeCmprsData == b'd' or entry.typeCmprsData == b'o':
                # d -> ARCHIVE_ITEM_DEPENDENCY
                # o -> ARCHIVE_ITEM_RUNTIME_OPTION
                # These are runtime options, not files
                continue

            basePath = os.path.dirname(entry.name)
            if basePath != '':
                # Check if path exists, create if not
                if not os.path.exists(basePath):
                    os.makedirs(basePath)

            if entry.typeCmprsData == b's':
                # s -> ARCHIVE_ITEM_PYSOURCE
                # Entry point are expected to be python scripts
                print('[+] Possible entry point: {0}.pyc'.format(entry.name))

                if self.pycMagic == b'\0' * 4:
                    # if we don't have the pyc header yet, fix them in a later pass
                    self.barePycList.append(entry.name + '.pyc')
                self._writePyc(entry.name + '.pyc', data)

            elif entry.typeCmprsData == b'M' or entry.typeCmprsData == b'm':
                # M -> ARCHIVE_ITEM_PYPACKAGE
                # m -> ARCHIVE_ITEM_PYMODULE
                # packages and modules are pyc files with their header intact

                # From PyInstaller 5.3 and above pyc headers are no longer stored
                # https://github.com/pyinstaller/pyinstaller/commit/a97fdf
                if data[2:4] == b'\r\n':
                    # < pyinstaller 5.3
                    if self.pycMagic == b'\0' * 4: 
                        self.pycMagic = data[0:4]
                    self._writeRawData(entry.name + '.pyc', data)

                else:
                    # >= pyinstaller 5.3
                    if self.pycMagic == b'\0' * 4:
                        # if we don't have the pyc header yet, fix them in a later pass
                        self.barePycList.append(entry.name + '.pyc')

                    self._writePyc(entry.name + '.pyc', data)

            else:
                self._writeRawData(entry.name, data)

                if entry.typeCmprsData == b'z' or entry.typeCmprsData == b'Z':
                    self._extractPyz(entry.name)

        # Fix bare pyc's if any
        self._fixBarePycs()


    def _fixBarePycs(self):
        for pycFile in self.barePycList:
            with open(pycFile, 'r+b') as pycFile:
                # Overwrite the first four bytes
                pycFile.write(self.pycMagic)


    def _writePyc(self, filename, data):
        with open(filename, 'wb') as pycFile:
            pycFile.write(self.pycMagic)            # pyc magic

            if self.pymaj >= 3 and self.pymin >= 7:                # PEP 552 -- Deterministic pycs
                pycFile.write(b'\0' * 4)        # Bitfield
                pycFile.write(b'\0' * 8)        # (Timestamp + size) || hash 

            else:
                pycFile.write(b'\0' * 4)      # Timestamp
                if self.pymaj >= 3 and self.pymin >= 3:
                    pycFile.write(b'\0' * 4)  # Size parameter added in Python 3.3

            pycFile.write(data)


    def _extractPyz(self, name):
        dirName =  name + '_extracted'
        # Create a directory for the contents of the pyz
        if not os.path.exists(dirName):
            os.mkdir(dirName)

        with open(name, 'rb') as f:
            pyzMagic = f.read(4)
            assert pyzMagic == b'PYZ\0' # Sanity Check

            pyzPycMagic = f.read(4) # Python magic value

            if self.pycMagic == b'\0' * 4:
                self.pycMagic = pyzPycMagic

            elif self.pycMagic != pyzPycMagic:
                self.pycMagic = pyzPycMagic
                print('[!] Warning: pyc magic of files inside PYZ archive are different from those in CArchive')

            # Skip PYZ extraction if not running under the same python version
            if self.pymaj != sys.version_info.major or self.pymin != sys.version_info.minor:
                print('[!] Warning: This script is running in a different Python version than the one used to build the executable.')
                print('[!] Please run this script in Python {0}.{1} to prevent extraction errors during unmarshalling'.format(self.pymaj, self.pymin))
                print('[!] Skipping pyz extraction')
                return

            (tocPosition, ) = struct.unpack('!i', f.read(4))
            f.seek(tocPosition, os.SEEK_SET)

            try:
                toc = marshal.load(f)
            except:
                print('[!] Unmarshalling FAILED. Cannot extract {0}. Extracting remaining files.'.format(name))
                return

            print('[+] Found {0} files in PYZ archive'.format(len(toc)))

            # From pyinstaller 3.1+ toc is a list of tuples
            if type(toc) == list:
                toc = dict(toc)

            for key in toc.keys():
                (ispkg, pos, length) = toc[key]
                f.seek(pos, os.SEEK_SET)
                fileName = key

                try:
                    # for Python > 3.3 some keys are bytes object some are str object
                    fileName = fileName.decode('utf-8')
                except:
                    pass

                # Prevent writing outside dirName
                fileName = fileName.replace('..', '__').replace('.', os.path.sep)

                if ispkg == 1:
                    filePath = os.path.join(dirName, fileName, '__init__.pyc')

                else:
                    filePath = os.path.join(dirName, fileName + '.pyc')

                fileDir = os.path.dirname(filePath)
                if not os.path.exists(fileDir):
                    os.makedirs(fileDir)

                try:
                    data = f.read(length)
                    data = zlib.decompress(data)
                except:
                    print('[!] Error: Failed to decompress {0}, probably encrypted. Extracting as is.'.format(filePath))
                    open(filePath + '.encrypted', 'wb').write(data)
                else:
                    self._writePyc(filePath, data)


def main():
    if len(sys.argv) < 2:
        print('[+] Usage: pyinstxtractor.py <filename>')

    else:
        arch = PyInstArchive(sys.argv[1])
        if arch.open():
            if arch.checkFile():
                if arch.getCArchiveInfo():
                    arch.parseTOC()
                    arch.extractFiles()
                    arch.close()
                    print('[+] Successfully extracted pyinstaller archive: {0}'.format(sys.argv[1]))
                    print('')
                    print('You can now use a python decompiler on the pyc files within the extracted directory')
                    return

            arch.close()


if __name__ == '__main__':
    main()

https://github.com/extremecoders-re/pyinstxtractor/blob/master/pyinstxtractor.py
여기 사이트를 참고하면 됩니다!!!
그래서 코드를 돌리면, 다음과 같은 파일들이 추출됩니다.

pyc 파일을 또 디컴파일해서 보려면 리눅스에서 uncompyle6 도구를 사용합니다.
위에 추출된 파일 중 configure.pyc 파일이 있는데, 이 파일을 툴을 사용해서 열면 다음과 같이 소스코드를 볼 수 있습니다.

uncompyle6 configure.pyc

이렇게 까지 했으면, 이제 아까 그 문제를 풀 수 있습니다.
암호화를 시키는 함수명은 mv18jiVh6TJI9lzY입니다.

다음 질문을 보겠습니다.

아까 처음에 줬던 .pdf.boo 파일인 친구를 해독해서 md5 값을 달라.
즉, 저기 코딩 되어있는 key 값과, iv 값을 이용해서 aes 복호화를 하는데 aes 모드는 CFB로 해라. 이말입니다.
여기서 진짜 10시간 이상 허비했습니다...하지만 풀이 자체는 엄청 단순하기에 올리도록 하겠습니다.

#!/usr/bin/python3
from Crypto.Cipher import AES
import random, string, time, os

def decrypte(filename: str) -> None:
    data = open(filename, 'rb').read() # candy_dungeon.pdf.boo 파일 불러옴
    key = 'vN0nb7ZshjAWiCzv'
    iv = b'ffTC776Wt59Qawe1'
    cipher = AES.new(key.encode('utf-8'), AES.MODE_CFB, iv)
    ct = cipher.decrypt(data) 
    # 원래는 여기서 encrypt로 암호화를 시키지만
    # 복호화를 하기 위해 decrypt로 변환
    Pkrr1fe0qmDD9nKx(filename, ct)

def Pkrr1fe0qmDD9nKx(filename: str, data: bytes) -> None:
    open(filename, 'wb').write(data)
    os.rename(filename, f"{filename}.pdf")

if __name__ == '__main__':
    filename = "candy_dungeon.pdf.boo" # 복호화 할 파일 이름
    decrypte(filename) # 복호화 함수 실행

코드를 실행시키면 pdf 파일이 하나 생성되는데, 그 파일의 MD5를 입력하시면 됩니다.

 

[Downgrade]

문제를 보겠습니다.

During recent auditing, we noticed that network authentication is not forced upon remote connections to our Windows 2012 server. That led us to investigate our system for suspicious logins further. Provided the server's event logs, can you find any suspicious successful login?

문제를 보면, Windows 2012 서버에 원격 연결 시 네트워크 인증이 강제 되지 않으며, 의심스로운 로그인이 있는지 추가로 조사를 하는데, 서버의 이벤트 로그를 제공하면 의심스럽게 로그인을 했지만 성공한 기록을 찾을 수 있는지 물어보는 시나리오 입니다.
그래서 다시 이번 문제도 위 문제처럼 nc로 들어가서 푸는 문제이고 주어진 파일을 다운로드 하면 이벤트로그가 많이 있습니다.

그리고 nc로 문제에 접근을 하여 첫번째 문제를 보겠습니다.

음...로그온 및 로그오프 이벤트에 대한 정보를 포함하는 이벤트 로그를 찾는 문제인데, 사실상 이건 Security파일로 답이 바로 나옵니다.
왜냐면, 이벤트 로그의 파일들이 Application, Security, System 이렇게 크게 3개로 나뉘는데, 크게 다음과 같다.
Application 이벤트 로그파일에는 응용 프로그램이 기록한 다양한 이벤트가 저장하며, System 이벤트 로그는 Windows 시스템 구성요소가 기록하는 이벤트로 시스템 부팅 시, 드라이버가 로드되지 않는 경우와 같이 구성요소의 오류를 기록하는 파일이기 때문이다. 나머지 Security는 유효하거나 유효하지 않은 로그온 시도 및 파일 생성, 열람, 삭제 등 리소스 사용에 관련된 이벤트를 기록합니다.

구구절절 설명을 좀 해봤는데, 아무튼 로그온과 관련된 이벤트 로그는 Security입니다. 이벤트를 한번 열어보면 바로 알 수 있습니다.

Logon이라고 적혀있는 이벤트를 볼 수 있습니다.
다음 문제를 보겠습니다.

로컬 컴퓨터에 성공적으로 로그온 하기위한 로그 이벤트 ID라면, 127.0.0.1에 접속한 로그온 이벤트를 보면 됩니다.

4624번 이벤트 ID가 127.0.0.1에 접속을 하고 있는 것을 볼 수 있습니다.

다음 문제를 보겠습니다.

기본 Active Directory 인증 프로토콜을 찾는 문제입니다.
먼저, 액티브 디렉터리는 윈도우 도메인 네트워크 용으로 마이크로소프트에서 개발한 디렉터리 서비스이며, 대부분의 Windows Server 운영체제에 일련의 프로세스 및 서비스로 포함되어 있습니다.

분석을 진행하기 앞서 이벤트 로그에 남는 로그의 로그온 유형을 살펴보도록 하겠습니다.

문제를 대조하지 않고 하나씩 읽어보기에는 이벤트 로그가 19000개 가량 되기 때문에 분석을 진행하기 어려웠고, 문제와 대조 했을 때 네트워크를 검사하지 않고 로그온, 원격 이라는 키워드를 봤을 때 유형 3과 유형 10이 유력하다고 판단을 하여 분석을 진행하였다.

그래서 유형 3번을 찾아서 보다가 로그온 프로세스가 Kerberos라는 이름으로 하나 잡히는 것이 있었다.

그래서 수상해서 더 분석을 진행한 결과 Kerberos 프로토콜을 가진 Logon 이벤트 기록들이 로그인 유형 3을 가지고 있음을 확인할 수 있었고, 이번 질문에 대한답은 Kerberos이다.

다음 문제를 보겠습니다.

이번 문제는 모든 로그온 이벤트를 살펴보면 다른 모든 이벤트와 다른 인증 패키지가 무엇인지 묻는 문제이다.

인증 세부 정보에 보면, NTLM 전용이라고 적혀있기 때문에 답은 NTLM이 된다.

다음 문제를 보겠습니다.

최종적으로 의심스러운 로그인을 했던 시간을 찾고, UTC를 기준으로 타임스탬프를 입력하는 문제이다.
로그들을 보다가 딱 하고 의심스러운게 하나 있었고 데이터들을 한번 살펴보자.

다른 애들을 살펴보면, 워크스테이션 이름이 SRV01로 된 로그들을 많이 볼 수 있는데 kali가 있는것으로 보아 수상하다고 느꼈다.
뿐만 아니라 로그온 프로세스가 ntlmssp가 적혀있는 것을 볼 수 있는데, 간단하게 말해서 ntlmssp는 NTLMSSP는 NTLM 챌린지 응답 인증을 용이하게 하고 무결성 및 기밀성 옵션을 협상하기 위해 Microsoft 보안 지원 공급자 인터페이스에서 사용하는 이진 메시징 프로토콜입니다.

그리고 키 길이가 있는것으로 보아 칼리에서 해당 윈도우 서버로 로그인을 했던 로그가 잡힌것으로 추정되며, 어떤 정보들을 빼돌렸는지는 알 수 없으나 로그인에 성공을 했기 때문에 로그가 남은 것으로 추정된다. 그래서 해당 시간인 2022-09-28 22:10:57을 UTC로 2022-09-28T13:10:57을 입력하면 플래그를 얻을 수 있었다.

사실 이번 문제는, 너무 많은 이벤트 로그가 있었으나 문제에서 키워드를 조금씩 던져줬기 때문에 쉽게 해결할 수 있었던 문제입니다.

[Halloween Invitation]

문제를 보겠습니다.

An email notification pops up. It's from your theater group. Someone decided to throw a party. The invitation looks awesome, but there is something suspicious about this document. Maybe you should take a look before you rent your banana costume.

문서 파일에 수상한 점이 있으니 분석을 요청한 시나리오 입니다.

주어진 파일을 다운받으면 docm 확장자를 가진 워드파일을 하나 받을 수 있는데, 실행을 시켜보겠습니다.

실행을 시키면, 왠지 모르게 언패킹? 된 스크립트가 짜져있는 것을 볼 수 있습니다.

vba 파일을 분석하기 위해서 구글링을 좀 했더니 olevba 라는 툴을 사용하는 것을 볼 수 있었고, 설치를 진행했습니다.

sudo apt-get update && apt-get upgrade -y
sudo -H pip install -U oletoos

https://github.com/decalage2/oletools/wiki/olevba

설치가 완료되었으면 분석을 진행하도록 하겠습니다.

우리가 문서 파일에서 볼 때는 뭔가 너저분 했기 때문에 우분투에서 파일을 한번 보겠습니다.

olevba [파일명].docm

docm 파일안에 있는 vba 매크로의 스크립트 코드를 다 출력해줍니다.

출력을 다 했으나 변수명이 좀 알아볼 수 없는? 형태가 많기 때문에 --deobf 옵션과 --reveal 옵션으로 해독을 한번 해보도록 하겠습니다.

olevba --deobf --reveal invitation.docm

해독을 하고나면 볼 수 있는 코드중에 아까 암호화된 코드에 대한 원본 코드를 볼 수 있습니다.

해독된 코드를 조금 살펴보면, 다음과 같은 코드를 볼 수 있습니다.

코드를 살짝 해석 해보면,  어떤 역할을 하는지는 잘 모르겠으나 fxnrfzsdxmcvranp 변수에 문자를 더해주는 코드가 보입니다.

이 코드가 아까 위에서 봤던 코드인데, 74 65 66 122 ~~ 로 된 것으로 보아 10진수임을 알 수 있었고, 아스키 코드로 변환을 좀 시켜보겠습니다.

a = [74,65,66,122,65,68,48,65,74,119,65,51,65,68,99,65,76,103,65,51,65,68,81,65,76,103,
65,120,65,68,107,65,79,65,65,117,65,68,85,65,77,103,65,54,65,68,103,65,77,65,65,52,
65,68,65,65,74,119,65,55,65,67,81,65,97,81,65,57,65,67,99,65,90,65,65,48,65,68,77,
65,89,103,66,106,65,71,77,65,78,103,66,107,65,67,48,65,77,65,65,48,65,68,77,65,90,
103,65,121,65,68,81,65,77,65,65,53,65,67,48,65,78,119,66,108,65,71,69,65,77,103,65,
122,65,71,69,65,77,103,66,106,65,67,99,65,79,119,65,107,65,72,65,65,80,81,65,110,65,
71,103,65,100,65,66,48,65,72,65,65,79,103,65,118,65,67,56,65,74,119,65,55,65,67,81,
65,100,103,65,57,65,69,107,65,98,103,66,50,65,71,56,65,97,119,66,108,65,67,48,65,85,
103,66,108,65,72,77,65,100,65,66,78,65,71,85,65,100,65,66,111,65,71,56,65,90,65,65,
103,65,67,48,65,86,81,66,122,65,71,85,65,81,103,66,104,65,72,77,65,97,81,66,106,65,
70,65,65,89,81,66,121,65,72,77,65,97,81,66,117,65,71,99,65,73,65,65,116,65,70,85,65,
99,103,66,112,65,67,65,65,74,65,66,119,65,67,81,65,99,119,65,118,65,71,81,65,78,65,
65,122,65,71,73,65,89,119,66,106,65,68,89,65,90,65,65,103,65,67,48,65,83,65,66,108,
65,71,69,65,90,65,66,108,65,72,73,65,99,119,65,103,65,69,65,65,101,119,65,105,65,69,
69,65,100,81,66,48,65,71,103,65,98,119,66,121,65,71,107,65,101,103,66,104,65,72,81,
65,97,81,66,118,65,71,52,65,73,103,65,57,65,67,81,65,97,81,66,57,65,68,115,65,100,
119,66,111,65,71,107,65,98,65,66,108,65,67,65,65,75,65,65,107,65,72,81,65,99,103,66,
49,65,71,85,65,75,81,66,55,65,67,81,65,89,119,65,57,65,67,103,65,83,81,66,117,65,72,
89,65,98,119,66,114,65,71,85,65,76,81,66,83,65,71,85,65,99,119,66,48,65,69,48,65,90,
81,66,48,65,71,103,65,98,119,66,107,65,67,65,65,76,81,66,86,65,72,77,65,90,81,66,67,
65,71,69,65,99,119,66,112,65,71,77,65,85,65,66,104,65,72,73,65,99,119,66,112,65,71,
52,65,90,119,65,103,65,67,48,65,86,81,66,121,65,71,107,65,73,65,65,107,65,72,65,65,
74,65,66,122,65,67,56,65,77,65,65,48,65,68,77,65,90,103,65,121,65,68,81,65,77,65,65,
53,65,67,65,65,76,81,66,73,65,71,85,65,89,81,66,107,65,71,85,65,99,103,66,122,65,67,
65,65,81,65,66,55,65,67,73,65,81,81,66,49,65,72,81,65,97,65,66,118,65,72,73,65,97,
81,66,54,65,71,69,65,100,65,66,112,65,71,56,65,98,103,65,105,65,68,48,65,74,65,66,
112,65,72,48,65,75,81,65,55,65,71,107,65,90,103,65,103,65,67,103,65,74,65,66,106,65,
67,65,65,76,81,66,117,65,71,85,65,73,65,65,110,65,69,52,65,98,119,66,117,65,71,85,
65,74,119,65,112,65,67,65,65,101,119,65,107,65,72,73,65,80,81,66,112,65,71,85,65,101,
65,65,103,65,67,81,65,89,119,65,103,65,67,48,65,82,81,66,121,65,72,73,65,98,119,66,
121,65,69,69,65,89,119,66,48,65,71,107,65,98,119,66,117,65,67,65,65,85,119,66,48,65,
71,56,65,99,65,65,103,65,67,48,65,82,81,66,121,65,72,73,65,98,119,66,121,65,70,89,
65,89,81,66,121,65,71,107,65,89,81,66,105,65,71,119,65,90,81,65,103,65,71,85,65,79,
119,65,107,65,72,73,65,80,81,66,80,65,72,85,65,100,65,65,116,65,70,77,65,100,65,66,
121,65,71,107,65,98,103,66,110,65,67,65,65,76,81,66,74,65,71,52,65,99,65,66,49,65,
72,81,65,84,119,66,105,65,71,111,65,90,81,66,106,65,72,81,65,73,65,65,107,65,72,73,
65,79,119,65,107,65,72,81,65,80,81,66,74,65,71,52,65,100,103,66,118,65,71,115,65,90,
81,65,116,65,70,73,65,90,81,66,122,65,72,81,65,84,81,66,108,65,72,81,65,97,65,66,118,
65,71,81,65,73,65,65,116,65,70,85,65,99,103,66,112,65,67,65,65,74,65,66,119,65,67,
81,65,99,119,65,118,65,68,99,65,90,81,66,104,65,68,73,65,77,119,66,104,65,68,73,65,
89,119,65,103,65,67,48,65,84,81,66,108,65,72,81,65,97,65,66,118,65,71,81,65,73,65,
66,81,65,69,56,65,85,119,66,85,65,67,65,65,76,81,66,73,65,71,85,65,89,81,66,107,65,
71,85,65,99,103,66,122,65,67,65,65,81,65,66,55,65,67,73,65,81,81,66,49,65,72,81,65,
97,65,66,118,65,72,73,65,97,81,66,54,65,71,69,65,100,65,66,112,65,71,56,65,98,103,
65,105,65,68,48,65,74,65,66,112,65,72,48,65,73,65,65,116,65,69,73,65,98,119,66,107,
65,72,107,65,73,65,65,111,65,70,115,65,85,119,66,53,65,72,77,65,100,65,66,108,65,71,
48,65,76,103,66,85,65,71,85,65,101,65,66,48,65,67,52,65,82,81,66,117,65,71,77,65,98,
119,66,107,65,71,107,65,98,103,66,110,65,70,48,65,79,103,65,54,65,70,85,65,86,65,66,
71,65,68,103,65,76,103,66,72,65,71,85,65,100,65,66,67,65,72,107,65,100,65,66,108,65,
72,77,65,75,65,65,107,65,71,85,65,75,119,65,107,65,72,73,65,75,81,65,103,65,67,48,
65,97,103,66,118,65,71,107,65,98,103,65,103,65,67,99,65,73,65,65,110,65,67,107,65,
102,81,65,103,65,72,77,65,98,65,66,108,65,71,85,65,99,65,65,103,65,68,65,65,76,103,
65,52,65,72,48,65,83,65,66,85,65,69,73,65,101,119,65,49,65,72,85,65,99,65,65,122,65,
72,73,65,88,119,65,122,65,68,81,65,78,81,66,53,65,70,56,65,98,81,65,48,65,71,77,65,
99,103,65,119,65,68,85,65,102,81,65,61]

for i in a:
    print(chr(i), end='')

코드를 실행 시키면 base64로 인코딩 된 문자열을 볼 수 있습니다.

디코딩을 하다보면 플래그 값을 찾을 수 있습니다.

이렇게 Hack the Boo 포렌식 카테고리를 모두 풀어봤습니다.

피드백 언제나 환영입니다!!