Nyctalopia - minimalistic flow obfuscator
Probléma: Hogyan tudod megvédeni a szellemi tulajdont másodrendű életformákkal, script kiddie-kkel szemben?
Motiváció
Én hiszek az open source felfogásban, és magam is hozzájárulok a szabad szoftverek fejlődéséhez. Nem pártolom az olyan pici-puha szoftvercégeket, amik titkolózással kompenzálják a gyengébb minőségű műszaki megoldásokat (kulturális szösszenet). Egy műszaki megoldás értékét nem az adja, hogy a szerzőn kívül senki nem ismeri a megvalósítás részleteit; ha a megoldás jó, úgy akkor is értékes marad, ha a "műhelytitkok" napvilágra kerülnek. További kapcsolódó írásokat itt találsz.
A tudás hatalom, és akinek jó ötletei, szabadalmai vannak, ügyes megoldásokat kreál, az megérdemli, hogy jól megéljen belőlük. Amennyiben úgy dönt, hogy a szellemi tulajdonát, műveit nyilvánosságra hozza, úgy rendelkezhet arról, hogy ki, hogyan, milyen célra, milyen megszorításokkal használhatja fel azt. Erre valók a szabadalmak és a licenszek.
Vannak sajnos olyanok, akik mások munkáját, tulajdonát kevésbé tisztelik, mások műveit lekoppintják, és sajátjukként feltüntetve értékesítik. Annak ellenére, hogy azok mondjuk szerzői jogvédelem, pl. open source licensz alatt állnak. Nyilván ilyenkor lehet pereskedni, de nem mindig érdemes. Egy ilyen precedens a mozgatórugója ennek a virrasztásnak (1 napos projektnek).
Műszaki háttér
A Java forráskód jvm bytecode-ra fordul, ami felépítésében hasonlít a gépi kódhoz, viszont nem optimalizált. Maga a jvm olvassa fel ezt a köztes formát, és fordítja le gépi kód utasításokra az adott hardverhez. Ennek a koncepciónak az előnye a platform függetlenség, hátrányaként sokszor a futási sebességet róják fel. Járulékos tulajdonsága viszont az is, hogy mivel a java compliler-nek viszonlag egyszerű transformációkat kell csak végrehajtania, a java bytecode-ot sokkal könnyebben vissza lehet forgatni Java forrássá, mint mondjuk a gépi kódot C-re. Számos hatékony java-decompiler található odakint, az ilyen grafikus eszközök használata már egyáltalán nem igényel szakértelmet.
Vannak olyan eszközök, amik a bytecode-ot úgy transzformálják, hogy különböző részeket - például debug-információt, dead code-ot - eltávolítanak, és a belső változók nevét lerövidítik. Ennek eredménye képpen kisebb, valamivel gyorsabb bytecode-ot kapunk. Folyományként viszont a visszaforgatás nehezebbé válik, és a visszafejtett állomány nehezebben lesz olvasható. Ezeket az eszközöket obfuscator-nak, vagy bytecode optimizer-nek hívjuk. Használatuk nem jelent különösebb védelmet a visszafejtés ellen. Léteznek olyan, feljettebb obfuscator-ok, amik a vezérlési folyamot is megkeverik, így valamelyest megváltoztatják a program futását olyan módon, hogy a program azért ugyanazt tegye; ezeket control flow obfuscator-nak nevezzük.
Alapvető elméleti tudás birtokában belátható, hogy nem lehet tetszőleges bytecode-ot java forrássá alakítani, mint ahogy az is, hogy egy adott bytecode-hoz végtelen olyan különböző bytecode megadható, ami "ugyanazt csinálja".
Célkitűzés
Adott java bytecode-ot szeretném úgy transzformálni, hogy a program működése ne változzon, viszont a visszaforgatás hibát adjon, vagy olyan kimenetet generáljon ami nem érvényes java, és nem lehet túl könnyen azzá alakítani. Fontos szempont volt, hogy minél kevesebb módosítással minél jobb eredményt érjek el. A kísérleti nyuszi a véleményem szerint jelenleg legjobban használható jd-gui decompiler volt.
Fontos tény, hogy kellő tudás és ráfordítás mellett mindig rekonstruálható, megérthető lesz a program. A bizonytalanságon/titkolózáson alapuló biztonság bár elriaszthatja a gyengébbeket, a felkészült támadót nem képes megállítani, maximum lelassítani. A cél most a paraziták elleni védelem.
Hogyan?
Először is érteni kell a compiler technológiához, és a jvm bytecode-hoz. Mivel minimális módon szeretném csak módosítani a bytecode-ot, olyan utasítás-sorozatot kerestem, amit bizonyos helyekre be tudok szúrni anélkül, hogy komplex elemzést (control flow analízist) kellene végrehajtanom az egyes metódusokon. Idempotens, nem intrúzív megoldás, ami viszont kellően összezavarja a decompilert.
- Első ötletként arra gondoltam, hogy a metódus visszatérése után illesztek be néhány utasítást. Mivel ez dead code,
egészen biztosan nem fogja befolyásolni a program működését. Természetesen lehetséges, hogy a
RETURN
után még vannak utasítások, és oda kerül a vezérlés - például, ha több helyen is szerepelRETURN
. Az ugrásokra és az offsetekre figyelni kell. Vagy olyan megoldást használni, ami figyel ezekre helyettünk. A Proof of Concept megoldást jasmin segítségével valósítottam meg. Ez gyakorlatilag egy java assembler. - Ügy tűnik, hogy a
/.?RETURN/
utasítások után beszúrtPOP ATHROW
utasítások elég zavart keltenek az éterben legalább közepesen komplex metódusok esetében. Ezt a megoldást automatizáltam, írtam egy tool-t, ami egy adott jar tartalmát átalakítja és készít egy másik jar-t, amiben a java bytecode már meg vannak mókolva. A tool az asm könyvtárat használja. - Tesztelés közben úgy ítéltem meg, hogy a
/.?RETURN/
utasítások elé is érdemes lenne beszúrni valamit, ami még jobban megbolondítja a helyzetet. Az ötletem az volt, hogy létrehozok egy halott ágat (ICONST_0 IFEQ
), majd verem túltöltés, túlürítés, vagy típus-helytelen ki-be pakolás segítségével olyan szekvenciát hozok létre, amihez nem lehet megfelelő java forrást generálni. Kísérletezgetés közben azt tapasztaltam, a jvm részét képző java bytecode verifier (ami minden osztály betöltéskor ellenőrzés végez a bytecode-on) nagyon jó munkát végez az ilyen disznóságok felderítésében - hiába helyeztem el ezeket halott ágon, ami ugye azt jelenti, hogy nem fog galibát okozni, mivel sosem kerül rá a vezérlés, a verifier nem volt hajlandó betölteni ezeket a megtrükközött osztályokat. - Érvényes, de trükkos vezérlési struktúrákkal kielégítő eredményt értem el, majd a beszúrandó kódot minimalizálva egy sohasem teljesülő végtelen ciklus lett az a verzió, ami mellett maradtam. Meglepő, de ez épp eléggé összezavarja a decompilert.
/*
* Nyctalopia - minimalistic flow obfuscator
*
* Copyright 2011 Tibor Bősze <tibor.boesze@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package hu.lithium.nyctalopia;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
class ObfuscatingClassAdapter extends ClassAdapter {
ObfuscatingClassAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (mv != null) {
mv = new ObfuscatingAdapter(mv);
}
return mv;
}
static class ObfuscatingAdapter extends MethodAdapter implements Opcodes {
ObfuscatingAdapter(MethodVisitor mv) {
super(mv);
}
@Override
public void visitInsn(int opcode) {
if (RETURN >= opcode && opcode >= IRETURN) {
Label l = new Label();
Label k = new Label();
mv.visitLabel(k);
mv.visitInsn(ICONST_0);
mv.visitJumpInsn(IFEQ, l);
mv.visitJumpInsn(GOTO, k);
mv.visitLabel(l);
mv.visitInsn(opcode);
mv.visitInsn(POP);
mv.visitInsn(ATHROW);
} else {
mv.visitInsn(opcode);
}
}
}
}
Jellemző és mókás, hogy egy adott java class transformációjának kiötlése, megvalósítása, a megfelelő minta kiválasztása és tesztelése kevesebb időt vett igénybe, és kevesebb kódsor lett, mint amit a jar felolvasása, háztartás és egyéb melléktevénységek igényeltek. A jar elkészítésénél a *.class fájlok módosításán túl még egy-két apró optimalizáció is történik, mint például directory entry-k kihagyása, tömörítés, és a manifest fájl kibővétése egy "itt jártam" tipusú attribútummal.
/*
* Nyctalopia - minimalistic flow obfuscator
*
* Copyright 2011 Tibor Bősze <tibor.boesze@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package hu.lithium.nyctalopia;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
public class JarProcessor {
public final static String copyright = "Nyctalopia - minimalistic flow obfuscator\n"
+ "Copyright 2011 Tibor Bősze <tibor.boesze@gmail.com>\n"
+ "Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)\n";
private static byte[] processClass(InputStream is) throws IOException {
ClassWriter w = new ClassWriter(ClassWriter.COMPUTE_MAXS);
new ClassReader(is).accept(new ObfuscatingClassAdapter(w), 0);
return w.toByteArray();
}
private static JarOutputStream createJarOutStream(JarInputStream input, String path) throws IOException {
Manifest m = input.getManifest() == null ? new Manifest() : input.getManifest();
Attributes attrs = m.getMainAttributes();
if (!attrs.containsKey(Attributes.Name.MANIFEST_VERSION.toString())) {
attrs.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0");
}
attrs.putValue("Created-By", "1.0 (Nyctalopia)");
JarOutputStream output = new JarOutputStream(new FileOutputStream(path), m);
output.setLevel(9);
return output;
}
private static void transformJarContent(JarInputStream in, JarOutputStream out) throws IOException {
byte[] buf = new byte[4096];
JarEntry entry = null;
while((entry = in.getNextJarEntry()) != null) {
if (entry.isDirectory() || JarFile.MANIFEST_NAME.equalsIgnoreCase(entry.getName())) {
System.out.println("skipping " + entry.getName());
continue;
}
JarEntry newEntry = new JarEntry(entry);
newEntry.setMethod(JarEntry.DEFLATED);
newEntry.setCompressedSize(-1);
if (entry.getName().endsWith(".class")) {
System.out.println("weaving " + entry.getName());
byte[] byteCode = processClass(in);
out.putNextEntry(newEntry);
out.write(byteCode);
out.closeEntry();
} else {//copy
System.out.println("copying " + entry.getName());
out.putNextEntry(newEntry);
int c = 0;
while ((c = in.read(buf)) != -1) {
out.write(buf, 0, c);
}
out.closeEntry();
}
}
}
public static void main(String[] args) throws IOException {
System.out.println(copyright);
if (args.length != 2) {
System.out.println("Usage: <input_jar> <output_jar>");
return;
}
JarInputStream in = null;
JarOutputStream out = null;
try {
in = new JarInputStream(new FileInputStream(args[0]));
out = createJarOutStream(in, args[1]);
transformJarContent(in, out);
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
}