Skip to content

Commit

Permalink
Merge branch '4-dll-files-arent-supported'
Browse files Browse the repository at this point in the history
  • Loading branch information
ergrelet committed May 31, 2022
2 parents d86f89b + 12aeda2 commit dd12f2e
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 90 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Changelog

## [Unreleased]

## [0.2.0] - 2022-05-31
### Added
- Handle unpacking of 32-bit and 64-bit DLLs
- Handle unpacking of 32-bit and 64-bit .NET assembly PEs (EXE only)
- OEP detection times out after 10 seconds by default. The duration can be
changed through the CLI.
Expand Down
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Unlicense [![](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/) ![CI status x64](https://github.com/ergrelet/unlicense/actions/workflows/win64-ci.yml/badge.svg?branch=main) ![CI status x86](https://github.com/ergrelet/unlicense/actions/workflows/win32-ci.yml/badge.svg?branch=main)

A Python 3 tool to dynamically unpack executables protected with
WinLicense/Themida 2.x and 3.x.
Themida/WinLicense 2.x and 3.x.

Warning: This tool will execute the target executable. Make sure to use this
tool in a VM if you're unsure about what the target executable does.
Expand All @@ -11,38 +11,46 @@ Note: You need to use a 32-bit Python interpreter to dump 32-bit executables.
## Features

* Handles Themida/Winlicense 2.x and 3.x
* Handles 32-bit and 64-bit executables (including .NET assemblies)
* Handles 32-bit and 64-bit PEs (EXEs and DLLs)
* Handles 32-bit and 64-bit .NET assemblies (EXEs only)
* Recovers the original entry point (OEP) automatically
* Recovers the (obfuscated) import table automatically

## Known Limitations

* Doesn't handle .NET assembly DLLs
* Doesn't automatically recover OEPs for executables with virtualized entry points
* Doesn't produce runnable dumps in most cases
* Resolving imports for 32-bit executables packed with Themida 2.x is pretty slow
* Doesn't handle DLL files

## How To

### Install
### Download

You can either download the PyInstaller-generated executables from the "Releases"
section or fetch the project with `git` and install it with `pip`:
```
$ git clone https://github.com/ergrelet/unlicense.git
$ pip install unlicense/
```

### Use

If you don't want to deal the command-line interface (CLI) you can simply
drag-and-drop the target binary on the appropriate (32-bit or 64-bit) `unlicense`
executable (which is available in the "Releases" section).

Otherwise here's what the CLI looks like:
```
$ unlicense --help
NAME
unlicense - Unpack executables protected with WinLicense/Themida.
unlicense - Unpack executables protected with Themida/WinLicense 2.x and 3.x
SYNOPSIS
unlicense EXE_TO_DUMP <flags>
DESCRIPTION
Unpack executables protected with WinLicense/Themida.
Unpack executables protected with Themida/WinLicense 2.x and 3.x
POSITIONAL ARGUMENTS
EXE_TO_DUMP
Expand All @@ -61,6 +69,9 @@ FLAGS
--target_version=TARGET_VERSION
Type: Optional[typing.Optional[int]]
Default: None
--timeout=TIMEOUT
Type: int
Default: 10
NOTES
You can also use flags syntax for POSITIONAL ARGUMENTS
Expand Down
62 changes: 31 additions & 31 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[tool.poetry]
name = "unlicense"
version = "0.1.1"
description = "Unpack executables protected with WinLicense/Themida"
version = "0.2.0"
description = "Unpack executables protected with Themida/WinLicense 2.x and 3.x"
authors = ["Erwan Grelet"]
license = "GPL-3.0-or-later"

Expand All @@ -13,7 +13,7 @@ lief = "^0.11"
fire = "^0.4"
capstone = "^4.0"
xxhash = "^2.0"
pyscylla = "^0.10.0"
pyscylla = "^0.11.0"

[tool.poetry.dev-dependencies]
mypy = "^0.910"
Expand Down
31 changes: 13 additions & 18 deletions unlicense/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from pathlib import Path
from typing import Optional

import lief # type: ignore
import fire # type: ignore

from . import frida_exec, winlicense2, winlicense3
from .dump_utils import dump_dotnet_assembly, interpreter_can_dump_pe
from .logger import setup_logger
from .version_detection import detect_winlicense_version

# Supported Themida/WinLicense major versions
Expand All @@ -21,31 +21,26 @@ def main() -> None:


def run_unlicense(
exe_to_dump: str,
pe_to_dump: str,
verbose: bool = False,
pause_on_oep: bool = False,
force_oep: Optional[int] = None,
target_version: Optional[int] = None,
timeout: int = 10,
) -> None:
"""
Unpack executables protected with WinLicense/Themida 2.x and 3.x
Unpack executables protected with Themida/WinLicense 2.x and 3.x
"""
if verbose:
log_level = logging.DEBUG
else:
log_level = logging.INFO
logging.basicConfig(level=log_level)
lief.logging.disable()

exe_path = Path(exe_to_dump)
if not exe_path.is_file():
LOG.error("'%s' isn't a file or doesn't exist", exe_path)
setup_logger(LOG, verbose)

pe_path = Path(pe_to_dump)
if not pe_path.is_file():
LOG.error("'%s' isn't a file or doesn't exist", pe_path)
sys.exit(1)

# Detect Themida/Winlicense version if needed
if target_version is None:
target_version = detect_winlicense_version(exe_to_dump)
target_version = detect_winlicense_version(pe_to_dump)
if target_version is None:
LOG.error("Failed to automatically detect packer version")
sys.exit(2)
Expand All @@ -55,7 +50,7 @@ def run_unlicense(
LOG.info("Detected packer version: %d.x", target_version)

# Check PE architecture and bitness
if not interpreter_can_dump_pe(exe_to_dump):
if not interpreter_can_dump_pe(pe_to_dump):
LOG.error("Target PE cannot be dumped with this interpreter. "
"This is most likely a 32 vs 64 bit mismatch.")
sys.exit(3)
Expand All @@ -76,7 +71,7 @@ def notify_oep_reached(image_base: int, oep: int, dotnet: bool) -> None:

# Spawn the packed executable and instrument it to find its OEP
process_controller = frida_exec.spawn_and_instrument(
exe_path, notify_oep_reached)
pe_path, notify_oep_reached)
try:
# Block until OEP is reached
if not oep_reached.wait(float(timeout)):
Expand All @@ -99,10 +94,10 @@ def notify_oep_reached(image_base: int, oep: int, dotnet: bool) -> None:
LOG.error(".NET assembly dump failed")
# Fix imports and dump the executable
elif target_version == 2:
winlicense2.fix_and_dump_pe(process_controller, exe_to_dump,
winlicense2.fix_and_dump_pe(process_controller, pe_to_dump,
dumped_image_base, dumped_oep)
elif target_version == 3:
winlicense3.fix_and_dump_pe(process_controller, exe_to_dump,
winlicense3.fix_and_dump_pe(process_controller, pe_to_dump,
dumped_image_base, dumped_oep)
finally:
# Try to kill the process on exit
Expand Down
24 changes: 13 additions & 11 deletions unlicense/dump_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,27 @@ def dump_pe(

with TemporaryDirectory() as tmp_dir:
TMP_FILE_PATH = os.path.join(tmp_dir, "unlicense.tmp")
dump_success = pyscylla.dump_pe(process_controller.pid, image_base,
oep, TMP_FILE_PATH, pe_file_path)
if not dump_success:
LOG.error("Failed to dump PE")
try:
pyscylla.dump_pe(process_controller.pid, image_base, oep,
TMP_FILE_PATH, pe_file_path)
except pyscylla.ScyllaException as e:
LOG.error("Failed to dump PE: %s", str(e))
return False

LOG.info("Fixing dump ...")
output_file_name = f"unpacked_{process_controller.main_module_name}"
try:
pyscylla.fix_iat(process_controller.pid, iat_addr, iat_size,
add_new_iat, TMP_FILE_PATH, output_file_name)
pyscylla.fix_iat(process_controller.pid, image_base, iat_addr,
iat_size, add_new_iat, TMP_FILE_PATH,
output_file_name)
except pyscylla.ScyllaException as e:
LOG.error("Failed to fix IAT: %s", str(e))
return False

rebuild_success = pyscylla.rebuild_pe(output_file_name, False, True,
False)
if not rebuild_success:
LOG.error("Failed to rebuild PE (with Scylla)")
try:
pyscylla.rebuild_pe(output_file_name, False, True, False)
except pyscylla.ScyllaException as e:
LOG.error("Failed to rebuild PE: %s", str(e))
return False

_rebuild_pe(output_file_name)
Expand Down Expand Up @@ -143,4 +145,4 @@ def interpreter_can_dump_pe(pe_file_path: str) -> bool:
# Only 32-bit PEs are supported
return bool(pe_architecture == lief.PE.MACHINE_TYPES.I386)

return False
return False
15 changes: 11 additions & 4 deletions unlicense/frida_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,17 @@ def _str_to_architecture(frida_arch: str) -> Architecture:


def spawn_and_instrument(
exe_path: Path,
pe_path: Path,
notify_oep_reached: OepReachedCallback) -> ProcessController:
main_module_name = exe_path.name
pid: int = frida.spawn((str(exe_path), ))
pid: int
if pe_path.suffix == ".dll":
# Use `rundll32` to load the DLL
rundll32_path = "C:\\Windows\\System32\\rundll32.exe"
pid = frida.spawn((rundll32_path, str(pe_path.absolute()), "#0"))
else:
pid = frida.spawn((str(pe_path), ))

main_module_name = pe_path.name
session = frida.attach(pid)
frida_js = resources.open_text("unlicense.resources", "frida.js").read()
script = session.create_script(frida_js)
Expand All @@ -156,7 +163,7 @@ def spawn_and_instrument(
frida_rpc = script.exports
process_controller = FridaProcessController(pid, main_module_name, session,
script)
frida_rpc.setup_oep_tracing(exe_path.name)
frida_rpc.setup_oep_tracing(pe_path.name)
frida.resume(pid)

return process_controller
Expand Down

0 comments on commit dd12f2e

Please sign in to comment.