Skip to content

Instantly share code, notes, and snippets.

@yoavst
Created September 9, 2017 12:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yoavst/24407f389aad6097220bfaf516e8d519 to your computer and use it in GitHub Desktop.
Save yoavst/24407f389aad6097220bfaf516e8d519 to your computer and use it in GitHub Desktop.
Titanium app resources extractor
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