Created
September 9, 2017 12:21
-
-
Save yoavst/24407f389aad6097220bfaf516e8d519 to your computer and use it in GitHub Desktop.
Titanium app resources extractor
This file contains hidden or 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
package com.yoavst.psychometric | |
import org.apache.commons.lang3.StringEscapeUtils | |
import java.io.File | |
import java.nio.CharBuffer | |
import javax.crypto.spec.SecretKeySpec | |
import javax.crypto.Cipher | |
/** | |
* Based on https://www.npmjs.com/package/ti_recover | |
* compile 'org.apache.commons:commons-lang3:3.5' | |
*/ | |
fun main(args: Array<String>) { | |
if (args.size != 2) { | |
println("java TitaniumResourcesExtractor path/to/AssetCryptImpl.smali package.name.of.app") | |
} else { | |
val (path, packageName) = args | |
val lines = File(path).readLines() | |
parseSmali(lines, packageName) | |
} | |
} | |
fun parseSmali(lines: List<String>, packageName: String) { | |
val initAssets = lines.asSequence().dropWhile { it != InitAssetsMethodStart }.dropWhile { RealMethodStart !in it }.takeWhile { it != MethodEnd } | |
val ranges = parseRanges(initAssets, packageName) | |
val initAssetsBytes = lines.asSequence().dropWhile { it != InitAssetsBytesMethodStart }.drop(1).takeWhile { it != MethodEnd } | |
val buffer = parseData(initAssetsBytes) | |
val finalData = decodeBytes(buffer, ranges) | |
println(finalData) | |
} | |
fun parseRanges(lines: Sequence<String>, packageName: String): Map<String, IntRange> { | |
val ranges = mutableMapOf<String, IntRange>() | |
val registers = mutableMapOf<String, String>() | |
var lastRange = 0..0 | |
for (line in lines.map(String::trim).filterNot { it.startsWith(".") || it.isBlank() }) { | |
val (command, register, data) = parseCommand(line) | |
when { | |
command in ConstCommands -> { | |
registers[register] = data.replace("\"", "") | |
} | |
command == "invoke-direct" && data == "L${packageName.replace('.', '/')}/AssetCryptImpl${'$'}Range;-><init>(II)V" -> { | |
val (_, second, third) = parseRegisters(register) | |
val first = decode(registers[second]!!) | |
lastRange = first..first + decode(registers[third]!!) | |
} | |
command == "invoke-interface" && data == "Ljava/util/Map;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;" -> { | |
val (_, name, _) = parseRegisters(register) | |
ranges[registers[name]!!] = lastRange | |
} | |
} | |
} | |
return ranges | |
} | |
fun parseData(lines: Sequence<String>): CharBuffer { | |
var buffer: CharBuffer = CharBuffer.wrap("") | |
for (line in lines.map(String::trim).filterNot { it.startsWith(".") || it.isBlank() }) { | |
val (command, register, data) = parseCommand(line) | |
when { | |
command == "const" || command == "const/16" -> { | |
buffer = CharBuffer.allocate(decode(data)) | |
} | |
command == "const-string" -> { | |
val encoded = StringEscapeUtils.unescapeJava(data.substring(1, data.length - 1)) | |
buffer.put(encoded) | |
} | |
data == "Ljava/nio/CharBuffer;->rewind()Ljava/nio/Buffer;" -> { | |
buffer.rewind() | |
} | |
} | |
} | |
return buffer | |
} | |
fun decodeBytes(buffer: CharBuffer, ranges: Map<String, IntRange>): Map<String, String> { | |
val assetBytes = Charsets.ISO_8859_1.encode(buffer).array() | |
return ranges.mapValues { (_, range) -> | |
val key = SecretKeySpec(assetBytes, assetBytes.size - 0x10, 0x10, "AES") | |
val cipher = Cipher.getInstance("AES") | |
cipher.init(2, key) | |
try { | |
val responseBinary = cipher.doFinal(assetBytes, range.first, range.last - range.first) | |
String(responseBinary) | |
} catch (e: Exception) { | |
val responseBinary = cipher.doFinal(assetBytes, range.first - 1, range.last - range.first) | |
String(responseBinary) | |
} | |
} | |
} | |
//region Utils | |
private fun parseCommand(command: String): Triple<String, String, String> { | |
val firstIndex = command.indexOf(' ') | |
var lastIndex = command.lastIndexOf(' ') | |
val strIndex = command.indexOf('"') | |
if (strIndex != -1) { | |
lastIndex = strIndex - 1 | |
} | |
if (firstIndex == lastIndex) | |
return Triple(command.substring(0, firstIndex), command.substring(firstIndex + 1, command.length), "") | |
return Triple(command.substring(0, firstIndex), command.substring(firstIndex + 1, lastIndex - 1), command.substring(lastIndex + 1)) | |
} | |
fun parseRegisters(registers: String): List<String> { | |
if (registers[0] != '{') return listOf(registers) | |
return registers.substring(1, registers.length - 1).split(", ") | |
} | |
private fun decode(number: String): Int = number.replace("0x", "").toInt(16) | |
private const val InitAssetsMethodStart = ".method private static initAssets()Ljava/util/Map;" | |
private const val InitAssetsBytesMethodStart = ".method private static initAssetsBytes()Ljava/nio/CharBuffer;" | |
private val ConstCommands = arrayOf("const/16", "const/4", "const", "const-string") | |
private const val RealMethodStart = ".prologue" | |
private const val MethodEnd = ".end method" | |
//endregion |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment