401 lines
14 KiB
PowerShell
401 lines
14 KiB
PowerShell
# decompile.ps1 — Decompile APK/XAPK/JAR/AAR using jadx, fernflower, or both
|
|
param(
|
|
[Alias('o')]
|
|
[string]$Output,
|
|
[switch]$Deobf,
|
|
[switch]$NoRes,
|
|
[string]$Engine = 'jadx',
|
|
[Parameter(Position=0)]
|
|
[string]$InputFile,
|
|
[Alias('h')]
|
|
[switch]$Help
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# Refresh PATH from user environment so we pick up tools installed in the same session
|
|
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
|
if ($userPath) {
|
|
foreach ($dir in $userPath -split ';') {
|
|
if ($dir -and $env:PATH -notlike "*$dir*") {
|
|
$env:PATH = "$dir;$env:PATH"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Show-Usage {
|
|
Write-Host @"
|
|
Usage: decompile.ps1 [OPTIONS] <file>
|
|
|
|
Decompile an Android APK, XAPK, JAR, or AAR file.
|
|
|
|
Arguments:
|
|
<file> Path to the .apk, .xapk, .jar, or .aar file
|
|
|
|
Options:
|
|
-Output DIR Output directory (default: <filename>-decompiled)
|
|
-Deobf Enable deobfuscation of names
|
|
-NoRes Skip resource decoding (faster, code-only)
|
|
-Engine ENGINE Decompiler engine: jadx, fernflower, or both (default: jadx)
|
|
-Help Show this help message
|
|
|
|
Engines:
|
|
jadx Use jadx (default). Handles APK/JAR/AAR natively, decodes resources.
|
|
fernflower Use Fernflower/Vineflower. Better on complex Java, lambdas, generics.
|
|
For APK files, requires dex2jar as intermediate step.
|
|
both Run both decompilers side by side for comparison.
|
|
jadx output -> <output>/jadx/
|
|
fernflower -> <output>/fernflower/
|
|
|
|
Environment:
|
|
FERNFLOWER_JAR_PATH Path to fernflower.jar or vineflower.jar
|
|
|
|
Examples:
|
|
.\decompile.ps1 app-release.apk
|
|
.\decompile.ps1 app-bundle.xapk
|
|
.\decompile.ps1 -Engine both -Deobf app-release.apk
|
|
.\decompile.ps1 -Engine fernflower library.jar
|
|
"@
|
|
exit 0
|
|
}
|
|
|
|
if ($Help) { Show-Usage }
|
|
|
|
# --- Validate input ---
|
|
if (-not $InputFile) {
|
|
Write-Host "Error: No input file specified." -ForegroundColor Red
|
|
Show-Usage
|
|
}
|
|
|
|
if (-not (Test-Path $InputFile)) {
|
|
Write-Host "Error: File not found: $InputFile" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
$extLower = [IO.Path]::GetExtension($InputFile).TrimStart('.').ToLower()
|
|
if ($extLower -notin @('apk', 'xapk', 'jar', 'aar')) {
|
|
Write-Host "Error: Unsupported file type '.$extLower'. Expected .apk, .xapk, .jar, or .aar" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
if ($Engine -notin @('jadx', 'fernflower', 'both')) {
|
|
Write-Host "Error: Unknown engine '$Engine'. Use jadx, fernflower, or both." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
$baseName = [IO.Path]::GetFileNameWithoutExtension($InputFile)
|
|
$inputFileAbs = (Resolve-Path $InputFile).Path
|
|
|
|
if (-not $Output) {
|
|
$Output = "$baseName-decompiled"
|
|
}
|
|
|
|
# --- XAPK handling ---
|
|
$xapkExtractedDir = $null
|
|
$xapkApkFiles = @()
|
|
|
|
if ($extLower -eq 'xapk') {
|
|
$xapkExtractedDir = Join-Path $env:TEMP "xapk-extract-$(Get-Random)"
|
|
Write-Host "=== Extracting XAPK archive ==="
|
|
New-Item -ItemType Directory -Path $xapkExtractedDir -Force | Out-Null
|
|
Expand-Archive -Path $inputFileAbs -DestinationPath $xapkExtractedDir -Force
|
|
|
|
# Show manifest.json if present
|
|
$manifestPath = Join-Path $xapkExtractedDir 'manifest.json'
|
|
if (Test-Path $manifestPath) {
|
|
Write-Host "XAPK manifest found:"
|
|
Get-Content $manifestPath
|
|
Write-Host ""
|
|
}
|
|
|
|
# Find all APK files inside
|
|
$xapkApkFiles = Get-ChildItem -Path $xapkExtractedDir -Recurse -Filter '*.apk' | Sort-Object Name
|
|
|
|
if ($xapkApkFiles.Count -eq 0) {
|
|
Write-Host "Error: No APK files found inside XAPK archive." -ForegroundColor Red
|
|
Remove-Item $xapkExtractedDir -Recurse -Force
|
|
exit 1
|
|
}
|
|
|
|
Write-Host "Found $($xapkApkFiles.Count) APK(s) inside XAPK:"
|
|
foreach ($f in $xapkApkFiles) {
|
|
Write-Host " - $($f.Name)"
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
# --- Locate fernflower JAR ---
|
|
function Find-FernflowerJar {
|
|
if ($env:FERNFLOWER_JAR_PATH -and (Test-Path $env:FERNFLOWER_JAR_PATH -ErrorAction SilentlyContinue)) {
|
|
return $env:FERNFLOWER_JAR_PATH
|
|
}
|
|
$candidates = @(
|
|
"$env:USERPROFILE\.local\share\vineflower\vineflower.jar",
|
|
"$env:USERPROFILE\fernflower\build\libs\fernflower.jar",
|
|
"$env:USERPROFILE\vineflower\build\libs\vineflower.jar",
|
|
"$env:USERPROFILE\fernflower\fernflower.jar",
|
|
"$env:USERPROFILE\vineflower\vineflower.jar"
|
|
)
|
|
foreach ($c in $candidates) {
|
|
if (Test-Path $c -ErrorAction SilentlyContinue) { return $c }
|
|
}
|
|
return $null
|
|
}
|
|
|
|
# --- Locate dex2jar ---
|
|
function Find-Dex2Jar {
|
|
$cmd = Get-Command d2j-dex2jar -ErrorAction SilentlyContinue
|
|
if ($cmd) { return $cmd.Source }
|
|
$cmd = Get-Command d2j-dex2jar.bat -ErrorAction SilentlyContinue
|
|
if ($cmd) { return $cmd.Source }
|
|
return $null
|
|
}
|
|
|
|
# --- jadx decompilation ---
|
|
function Invoke-Jadx {
|
|
param([string]$OutDir, [string]$FileAbs, [string]$FileExt)
|
|
|
|
if (-not (Get-Command jadx -ErrorAction SilentlyContinue)) {
|
|
Write-Host "Error: jadx is not installed or not in PATH." -ForegroundColor Red
|
|
return $false
|
|
}
|
|
|
|
$jadxArgs = @('-d', $OutDir)
|
|
if ($Deobf) { $jadxArgs += '--deobf' }
|
|
if ($NoRes) { $jadxArgs += '--no-res' }
|
|
$jadxArgs += '--show-bad-code'
|
|
$jadxArgs += $FileAbs
|
|
|
|
Write-Host "Running: jadx $($jadxArgs -join ' ')"
|
|
& jadx @jadxArgs
|
|
|
|
$sourcesDir = Join-Path $OutDir 'sources'
|
|
if (Test-Path $sourcesDir) {
|
|
$count = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java').Count
|
|
Write-Host "jadx output: $sourcesDir\"
|
|
Write-Host "Java files decompiled by jadx: $count"
|
|
}
|
|
return $true
|
|
}
|
|
|
|
# --- Fernflower decompilation ---
|
|
function Invoke-Fernflower {
|
|
param([string]$OutDir, [string]$FileAbs, [string]$FileExt)
|
|
|
|
$ffJar = Find-FernflowerJar
|
|
if (-not $ffJar) {
|
|
Write-Host "Error: Fernflower/Vineflower JAR not found." -ForegroundColor Red
|
|
Write-Host "Set FERNFLOWER_JAR_PATH or see references/setup-guide.md"
|
|
return $false
|
|
}
|
|
|
|
New-Item -ItemType Directory -Path $OutDir -Force | Out-Null
|
|
|
|
$jarToDecompile = $FileAbs
|
|
$convertedJar = $null
|
|
|
|
# For APK/AAR, we need dex2jar first
|
|
if ($FileExt -in @('apk', 'aar')) {
|
|
$d2j = Find-Dex2Jar
|
|
if (-not $d2j) {
|
|
Write-Host "Error: dex2jar is required to use Fernflower on .$FileExt files." -ForegroundColor Red
|
|
Write-Host "Install dex2jar - see references/setup-guide.md"
|
|
return $false
|
|
}
|
|
|
|
Write-Host "Converting $FileExt to JAR with dex2jar..."
|
|
$convertedJar = Join-Path $OutDir "$baseName-dex2jar.jar"
|
|
& $d2j -f -o $convertedJar $FileAbs 2>&1 | Write-Host
|
|
if (-not (Test-Path $convertedJar)) {
|
|
Write-Host "Error: dex2jar conversion failed." -ForegroundColor Red
|
|
return $false
|
|
}
|
|
$jarToDecompile = $convertedJar
|
|
}
|
|
|
|
# Build fernflower args
|
|
$ffArgs = @('-dgs=1', '-mpm=60')
|
|
if ($Deobf) { $ffArgs += '-ren=1' }
|
|
$ffArgs += $jarToDecompile
|
|
$ffArgs += $OutDir
|
|
|
|
Write-Host "Running: java -jar $ffJar $($ffArgs -join ' ')"
|
|
& java -jar $ffJar @ffArgs
|
|
|
|
# Fernflower outputs a JAR containing .java files — extract it
|
|
$resultJar = Join-Path $OutDir ([IO.Path]::GetFileName($jarToDecompile))
|
|
if (Test-Path $resultJar) {
|
|
$sourcesDir = Join-Path $OutDir 'sources'
|
|
New-Item -ItemType Directory -Path $sourcesDir -Force | Out-Null
|
|
Expand-Archive -Path $resultJar -DestinationPath $sourcesDir -Force
|
|
Remove-Item $resultJar -Force
|
|
$count = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java').Count
|
|
Write-Host "Fernflower output: $sourcesDir\"
|
|
Write-Host "Java files decompiled by Fernflower: $count"
|
|
}
|
|
|
|
# Clean up intermediate dex2jar output
|
|
if ($convertedJar -and (Test-Path $convertedJar -ErrorAction SilentlyContinue)) {
|
|
Remove-Item $convertedJar -Force
|
|
}
|
|
return $true
|
|
}
|
|
|
|
# --- Summary helper ---
|
|
function Show-Structure {
|
|
param([string]$SrcDir, [string]$Label)
|
|
if (Test-Path $SrcDir) {
|
|
Write-Host ""
|
|
Write-Host "Top-level packages ($Label):"
|
|
Get-ChildItem -Path $SrcDir -Directory -Recurse -Depth 2 |
|
|
Select-Object -First 20 |
|
|
ForEach-Object { $_.FullName.Replace("$SrcDir\", '') } |
|
|
Sort-Object
|
|
}
|
|
}
|
|
|
|
# --- Decompile a single file ---
|
|
function Invoke-DecompileSingle {
|
|
param([string]$FileAbs, [string]$OutDir, [string]$Label)
|
|
|
|
$fileExt = [IO.Path]::GetExtension($FileAbs).TrimStart('.').ToLower()
|
|
|
|
if ($Label) {
|
|
Write-Host "=== Decompiling $Label (engine: $Engine) ==="
|
|
}
|
|
|
|
switch ($Engine) {
|
|
'jadx' {
|
|
Invoke-Jadx -OutDir $OutDir -FileAbs $FileAbs -FileExt $fileExt
|
|
Show-Structure (Join-Path $OutDir 'sources') 'jadx'
|
|
}
|
|
'fernflower' {
|
|
Invoke-Fernflower -OutDir $OutDir -FileAbs $FileAbs -FileExt $fileExt
|
|
Show-Structure (Join-Path $OutDir 'sources') 'fernflower'
|
|
}
|
|
'both' {
|
|
Write-Host "--- Pass 1: jadx ---"
|
|
Invoke-Jadx -OutDir (Join-Path $OutDir 'jadx') -FileAbs $FileAbs -FileExt $fileExt
|
|
Write-Host ""
|
|
Write-Host "--- Pass 2: Fernflower ---"
|
|
Invoke-Fernflower -OutDir (Join-Path $OutDir 'fernflower') -FileAbs $FileAbs -FileExt $fileExt
|
|
|
|
Show-Structure (Join-Path $OutDir 'jadx\sources') 'jadx'
|
|
Show-Structure (Join-Path $OutDir 'fernflower\sources') 'fernflower'
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Comparison ==="
|
|
$jadxCount = 0; $ffCount = 0
|
|
$jadxSources = Join-Path $OutDir 'jadx\sources'
|
|
$ffSources = Join-Path $OutDir 'fernflower\sources'
|
|
if (Test-Path $jadxSources) {
|
|
$jadxCount = (Get-ChildItem -Path $jadxSources -Recurse -Filter '*.java').Count
|
|
}
|
|
if (Test-Path $ffSources) {
|
|
$ffCount = (Get-ChildItem -Path $ffSources -Recurse -Filter '*.java').Count
|
|
}
|
|
Write-Host "jadx: $jadxCount Java files"
|
|
Write-Host "Fernflower: $ffCount Java files"
|
|
|
|
if (Test-Path $jadxSources) {
|
|
$jadxErrors = (Get-ChildItem -Path $jadxSources -Recurse -Filter '*.java' -File |
|
|
Select-String -Pattern 'JADX WARNING|JADX WARN|JADX ERROR|Code decompiled incorrectly' -SimpleMatch -ErrorAction SilentlyContinue |
|
|
Select-Object -ExpandProperty Path -Unique).Count
|
|
Write-Host "jadx files with warnings/errors: $jadxErrors"
|
|
}
|
|
Write-Host ""
|
|
Write-Host "Tip: compare specific classes between jadx/ and fernflower/ to pick the better output."
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- Run ---
|
|
Write-Host "=== Decompiling $InputFile (engine: $Engine) ==="
|
|
Write-Host "Output directory: $Output"
|
|
Write-Host ""
|
|
|
|
if ($extLower -eq 'xapk') {
|
|
New-Item -ItemType Directory -Path $Output -Force | Out-Null
|
|
|
|
# Copy XAPK manifest for reference
|
|
$manifestSrc = Join-Path $xapkExtractedDir 'manifest.json'
|
|
if (Test-Path $manifestSrc) {
|
|
Copy-Item $manifestSrc (Join-Path $Output 'xapk-manifest.json')
|
|
}
|
|
|
|
# List OBB files
|
|
$obbFiles = Get-ChildItem -Path $xapkExtractedDir -Recurse -Filter '*.obb' -ErrorAction SilentlyContinue
|
|
if ($obbFiles) {
|
|
Write-Host "OBB files found (not decompiled, data-only):"
|
|
foreach ($obb in $obbFiles) {
|
|
$size = '{0:N1} MB' -f ($obb.Length / 1MB)
|
|
Write-Host " - $($obb.Name) ($size)"
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
foreach ($apkFile in $xapkApkFiles) {
|
|
$apkName = [IO.Path]::GetFileNameWithoutExtension($apkFile.Name)
|
|
Write-Host ""
|
|
Write-Host "======================================================"
|
|
Invoke-DecompileSingle -FileAbs $apkFile.FullName -OutDir (Join-Path $Output $apkName) -Label "$($apkFile.Name)"
|
|
}
|
|
|
|
# Cleanup extracted XAPK
|
|
Remove-Item $xapkExtractedDir -Recurse -Force
|
|
|
|
Write-Host ""
|
|
Write-Host "=== XAPK decompilation complete ==="
|
|
Write-Host "Subdirectories in ${Output}\"
|
|
Get-ChildItem -Path $Output -Directory | ForEach-Object { Write-Host $_.Name }
|
|
} else {
|
|
Invoke-DecompileSingle -FileAbs $inputFileAbs -OutDir $Output -Label ''
|
|
|
|
# --- Split/bundled APK detection ---
|
|
# Some APKs are bundles: the outer APK contains
|
|
# base.apk + split_config.*.apk inside the resources directory. jadx will
|
|
# decompile the thin outer wrapper and produce very few Java files.
|
|
# Detect this and automatically decompile the inner base.apk.
|
|
$sourcesDir = Join-Path $Output 'sources'
|
|
$resourcesDir = Join-Path $Output 'resources'
|
|
if ((Test-Path $sourcesDir) -and (Test-Path $resourcesDir)) {
|
|
$javaCount = (Get-ChildItem -Path $sourcesDir -Recurse -Filter '*.java' -File -ErrorAction SilentlyContinue).Count
|
|
$innerApks = Get-ChildItem -Path $resourcesDir -Filter '*.apk' -File -ErrorAction SilentlyContinue
|
|
$baseApk = $innerApks | Where-Object { $_.Name -eq 'base.apk' }
|
|
|
|
if ($javaCount -le 10 -and $baseApk) {
|
|
Write-Host ""
|
|
Write-Host "=== Split/bundled APK detected ==="
|
|
Write-Host "Outer APK produced only $javaCount Java file(s) but contains $($innerApks.Count) inner APK(s):"
|
|
foreach ($inner in $innerApks) {
|
|
Write-Host " - $($inner.Name)"
|
|
}
|
|
Write-Host ""
|
|
Write-Host "Decompiling base.apk (contains the actual app code)..."
|
|
$baseOutput = Join-Path $Output 'base'
|
|
Invoke-DecompileSingle -FileAbs $baseApk.FullName -OutDir $baseOutput -Label 'base.apk'
|
|
|
|
# Decompile any split APKs that aren't just config splits
|
|
$splitApks = $innerApks | Where-Object { $_.Name -ne 'base.apk' -and $_.Name -notmatch 'split_config\.' }
|
|
foreach ($split in $splitApks) {
|
|
$splitName = [IO.Path]::GetFileNameWithoutExtension($split.Name)
|
|
Write-Host ""
|
|
Write-Host "Decompiling $($split.Name)..."
|
|
Invoke-DecompileSingle -FileAbs $split.FullName -OutDir (Join-Path $Output $splitName) -Label $split.Name
|
|
}
|
|
|
|
if ($innerApks | Where-Object { $_.Name -match 'split_config\.' }) {
|
|
Write-Host ""
|
|
Write-Host "Skipped config splits (resource/ABI only):"
|
|
$innerApks | Where-Object { $_.Name -match 'split_config\.' } | ForEach-Object { Write-Host " - $($_.Name)" }
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "NOTE: The main decompiled source is in: $(Join-Path $Output 'base\sources')"
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Decompilation complete ==="
|
|
}
|