java - How to obfuscate with ProGuard but keep names readable while testing? -


i'm in pre-release stage app started compiling release builds assemblerelease instead of assembledebug. obfuscation breaks things , it's hard decipher what's what. debugging impossible, line numbers kept variables classes unreadable. while release build not stable i'd make obfuscation less of pain, should still behave obfuscated.

usually proguarded release converts names from

net.twisterrob.app.pack.myclass 

to

b.a.c.b.a 

with reflection , android layout/menu resources can break, if encountered classes didn't keep names of.

it helpful pre-release testing able obfuscate code, "not much", converting names from

net.twisterrob.app.pack.myclass 

to

net.twisterrob.app.pack.myclass // or n.t.a.p.mc or in between :) 

the proguard -dontobfuscate of course helps, makes broken stuff work again because class names correct.

what i'm looking break broken full obfuscation, @ same time it's easy figure out what's without using mapping.txt because names kept human readable.

i looking around http://proguard.sourceforge.net/manual/usage.html#obfuscationoptions -*dictionary options don't seem doing this.

i fine generate renaming file myself (it running through classes , give them tolowercase or something):

net.twisterrob.app.pack.myclassa -> myclassa net.twisterrob.app.pack.myclassb -> myclassb 

the question how feed such file proguard , format?

so looks i've managed skip on option -applymapping in section linked.

tl;dr

jump implementation / details section , copy 2 blocks of gradle/groovy code android subproject's build.gradle file.

mapping.txt

the format of mapping.txt pretty simple:

full.pack.age.class -> obf.usc.ate.d:     type mfield -> mobfusc     #:#:type method(arg,s) -> methobfusc kept.class -> kept.class:     type mkept -> mkept     #:#:type kept() -> kept 

the shrinked classes , members not listed @ all. information available, if can generate same or transform this, there's pretty chance of success.

solution 1: dump classes [failed]

i've tried generate input mapping.txt based current classpath passed proguard (-injars). loaded classes in urlclassloader had program jars libraryjars (to resolve super classes example). iterated through each class , each declared member , output name have liked use.

there big problem this: solution contained obfuscated name each , every renameable thing in app. problem here -applymapping takes things literally , tries apply mappings in input mapping file, ignoring -keep rules, leading warnings conflicted renames. gave on path, because didn't want duplicate proguard config, nor wanted implement proguard config parser myself.

solution 2: run proguardrelease twice [failed]

based on above fail, thought of solution make use of configuration , keeps there are. flow following:

  • let proguardrelease it's job
    outputs source mapping.txt
  • transform mapping.txt new file
  • duplicate proguardrelease gradle task , run transformed mapping

the problem it's complicated duplicate whole task, it's inputs, outputs, dolast, dofirst, @taskaction, etc... started on route anyway, joined 3rd solution.

solution 3: use proguardrelease's output [success]

while trying duplicate whole task , analyzing proguard/android plugin code realized easier simulate proguardrelease doing again. here's final flow:

  • let proguardrelease it's job
    outputs source mapping.txt
  • transform mapping.txt new file
  • run proguard again same config,
    time using mapping file renames

the result wanted:
(example pattern <package>.__<class>__.__<field>__ class , field names inverted case)

java.lang.nullpointerexception: cannot find actionview! declared in xml , kept in proguard?         @ net.twisterrob.android.utils.tools.__androidtools__.__preparesearch__(androidtools.java:533)         @ net.twisterrob.inventory.android.activity.mainactivity.oncreateoptionsmenu(mainactivity.java:181)         @ android.app.activity.oncreatepanelmenu(activity.java:2625)         @ android.support.v4.app.__fragmentactivity__.oncreatepanelmenu(fragmentactivity.java:277)         @ android.support.v7.internal.view.__windowcallbackwrapper__.oncreatepanelmenu(windowcallbackwrapper.java:84)         @ android.support.v7.app.__appcompatdelegateimplbase$appcompatwindowcallback__.oncreatepanelmenu(appcompatdelegateimplbase.java:251)         @ android.support.v7.app.__appcompatdelegateimplv7__.__preparepanel__(appcompatdelegateimplv7.java:1089)         @ android.support.v7.app.__appcompatdelegateimplv7__.__doinvalidatepanelmenu__(appcompatdelegateimplv7.java:1374)         @ android.support.v7.app.__appcompatdelegateimplv7__.__access$100__(appcompatdelegateimplv7.java:89)         @ android.support.v7.app.__appcompatdelegateimplv7$1__.run(appcompatdelegateimplv7.java:123)         @ android.os.handler.handlecallback(handler.java:733) 

or notice underscores here: _<name> unfuscated names mixed kept names

implementation / details

i tried make simple possible, while keeping maximum flexibility. call unfuscation, becuase it's undoing proper obfuscation, still considered obfuscation in terms of reflection example.

i implemented few guards because 2nd round makes few assumptions. obivously if there's no obfuscation, there's no need unfuscated. it's pointless unfuscate (and may accidentally released) if debuging turned off since unfuscation helps inside ide. if the app tested , obfuscated, interals of androidproguardtask using mapping file , didn't want deal now.

so went ahead , created unfuscate task, transformation , runs proguard. sadly proguard configuration not exposed in proguard.gradle.proguardtask, when did stop anyone?! :)

there's 1 drawback, takes double time proguard it, guess worth if need debug it.

here's android hooking code gradle:

afterevaluate {     project.android.applicationvariants.all { com.android.build.gradle.api.applicationvariant variant ->         task obfuscatetask = variant.obfuscation         def skipreason = [ ];         if (obfuscatetask == null) { skipreason += "not obfuscated" }         if (!variant.buildtype.debuggable) { skipreason += "not debuggable" }         if (variant.testvariant != null) { skipreason += "tested" }         if (!skipreason.isempty()) {             logger.info("skipping unfuscation of {} because {}", variant.name, skipreason);             return;         }          file mapping = variant.mappingfile         file newmapping = new file(mapping.parentfile, "unmapping.txt")          task unfuscatetask = project.task("${obfuscatetask.name}unfuscate") {             inputs.file mapping             outputs.file newmapping             outputs.uptodatewhen { mapping.lastmodified() <= newmapping.lastmodified() }             dolast {                 java.lang.reflect.field configfield =                         proguard.gradle.proguardtask.class.getdeclaredfield("configuration")                 configfield.accessible = true                 proguard.configuration config = configfield.get(obfuscatetask) proguard.configuration                 if (!config.obfuscate) return; // nothing unfuscate when -dontobfuscate                  java.nio.file.files.copy(mapping.topath(), new file(mapping.parentfile, "mapping.txt.bck").topath(),                         java.nio.file.standardcopyoption.replace_existing)                 logger.info("writing new mapping file: {}", newmapping)                 new mapping(mapping).remap(newmapping)                  logger.info("re-executing {} new mapping...", obfuscatetask.name)                 config.applymapping = newmapping // use our re-written mapping file                 //config.note = [ '**' ] // -dontnote **, noted in first run                  loggingmanager loggingmanager = getlogging();                 // lower level of logging prevent duplicate output                 loggingmanager.capturestandardoutput(loglevel.warn);                 loggingmanager.capturestandarderror(loglevel.warn);                 new proguard.proguard(config).execute();             }         }         unfuscatetask.dependson obfuscatetask         variant.dex.dependson unfuscatetask     } } 

the other part of whole transformation. managed compose all-matching regex pattern, pretty simple. can safely ignore class structure , remap method. key processline called each line. line split parts, text before , after obfuscated name stays (two substrings) , name changed in middle. change return statement in unfuscate suit needs.

class mapping {     private static java.util.regex.pattern mapping_pattern =             ~/^(?<member>    )?(?<location>\d+:\d+:)?(?:(?<type>.*?) )?(?<name>.*?)(?:\((?<args>.*?)\))?(?: -> )(?<obfuscated>.*?)(?<class>:?)$/;     private static int mapping_pattern_obfuscated_index = 6;      private final file source     public mapping(file source) {         this.source = source     }      public void remap(file target) {         target.withwriter { source.eachline mapping.&processline.curry(it) }     }      private static void processline(writer out, string line, int num) {         java.util.regex.matcher m = mapping_pattern.matcher(line)         if (!m.find()) {             throw new illegalargumentexception("line #${num} not recognized: ${line}")         }         try {             def originalname = m.group("name")             def obfuscatedname = m.group("obfuscated")             def newname = originalname.equals(obfuscatedname) ? obfuscatedname : unfuscate(originalname, obfuscatedname)             out.write(line.substring(0, m.start(mapping_pattern_obfuscated_index)))             out.write(newname)             out.write(line.substring(m.end(mapping_pattern_obfuscated_index)))             out.write('\n')         } catch (exception ex) {             stringbuilder sb = new stringbuilder("line #${num} failed: ${line}\n");             0.upto(m.groupcount()) { sb.append("group #${it}: '${m.group(it)}'\n") }             throw new illegalargumentexception(sb.tostring(), ex)         }     }      private static string unfuscate(string name, string obfuscated) {         int lastdot = name.lastindexof('.') + 1;         string pkgwithdot = 0 < lastdot ? name.substring(0, lastdot) : "";         name = 0 < lastdot ? name.substring(lastdot) : name;         // reassemble names readable, still breaking changes         // pkgwithdot empty fields , methods         return pkgwithdot + '_' + name;     } } 

possible unfuscations

you should able apply transformation package names, didn't test that.

// android.support.v4.a.a, original obfuscated 1 return obfuscated;  // android.support.v4.app._fragment return pkgwithdot + '_' + name;  // android.support.v4.app.fragment_a17d4670 return pkgwithdot + name + '_' + integer.tohexstring(name.hashcode());  // android.support.v4.app.fragment_a return pkgwithdot + name + '_' + afterlastdot(obfuscated)  // android.support.v4.app.fragment return pkgwithdot + org.apache.commons.lang.stringutils.swapcase(name); // needs following in build.gradle: buildscript {     repositories { jcenter() }     dependencies { classpath 'commons-lang:commons-lang:2.6' } }  // android.support.v4.app.fragment return pkgwithdot + name.tolowercase(); 

warning: irreversible transformations error-prone. consider following:

class x {     private static final factory factory = ...;     ...     public interface factory {     } } // notice how both `x.factory` , `x.factory` become `x.factory` not allowed. 

of course of above transformations can tricked in 1 way or another, it's less uncommon pre-postfixes , text-transformations.


Comments

Popular posts from this blog

jquery - How do you format the date used in the popover widget title of FullCalendar? -

asp.net mvc - SSO between MVCForum and Umbraco7 -

Python Tkinter keyboard using bind -