제어 흐름 난독화란? if, switch와 같은 제어/분기 구문에 난독화를 하는 것
아래는 예제이다.
package obfuscate.test;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import java.util.Random;
import bam.boo.zoo;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Random을 넣은 이유는, 상수를 넣으면 최적화 되어 버린다.
Random rand = new Random();
int num = rand.nextInt(100000);
System.out.println("hohoh!oho " + num);
zoo.func1(num);
System.out.println("end!");
}
}
난독화 테스트를 위해 제어 흐름을 if 구문을 사용하여 코딩하였다.
package bam.boo;
public class zoo {
public static void func1(int num)
{
if (num > 1)
{
System.out.println("if");
}
else
{
System.out.println("else");
}
}
}
zoo class를 decompile하면 아래와 같다.
package a.a;
import java.io.PrintStream;
public class a {
public static void a(int var0) {
PrintStream var1;
String var2;
if (var0 > 1) {
var1 = System.out;
var2 = "if";
} else {
var1 = System.out;
var2 = "else";
}
var1.println(var2);
}
}
아래는 smali 코드이다.
smali 코드로 변환하면서 goto 명령어가 생성되었다.
goto와 같은 분기 명령어에 pass를 적용한 후 dummy 코드를 포함하여 ifne/goto 명령어를 적절히 추가하면 분기가 복잡하게 보이도록 만들 수 있다.
@Override
public void visitProgramClass(ProgramClass programClass)
{
if ((programClass.getAccessFlags() & AccessConstants.INTERFACE) != 0)
{
return;
}
// 수를 참조할때 static 변수가 아니면 최적화되어 버리므로 번거롭지만 생성하여야 한다.
ConstantPoolEditor constantPoolEditor = new ConstantPoolEditor(programClass);
ClassEditor classEditor = new ClassEditor(programClass);
int nameIndex = constantPoolEditor.addUtf8Constant("valueX");
int descriptorIndex = constantPoolEditor.addUtf8Constant("I");
ProgramField opaqueField = new ProgramField(
AccessConstants.PRIVATE | AccessConstants.STATIC, nameIndex, descriptorIndex, null);
classEditor.addField(opaqueField);
new InitializerEditor(programClass).addStaticInitializerInstructions(/*mergeIntoExistingInitializer=*/true,
// static 변수에 저장
____ -> {
____.ldc(rand.nextInt(10000)) // 양수 생성
.putstatic(programClass, opaqueField);
});
programClass.accept(new AllMethodVisitor(new AllAttributeVisitor(this)));
}
@Override
public void visitBranchInstruction(Clazz clazz,
Method method,
CodeAttribute codeAttribute,
int offset,
BranchInstruction branch)
{
// goto 명령어에만 적용한다.
if (branch.opcode != Instruction.OP_GOTO) {
return;
}
InstructionSequenceBuilder ____ = new InstructionSequenceBuilder((ProgramClass)clazz);
FrameFinder finder = new FrameFinder(this.partialEvaluator, offset);
codeAttribute.instructionsAccept(clazz, method, finder);
if (!finder.targets.isEmpty())
{
// x + 1 != 0
// x 값으로 양수를 생성했으므로 위의 수식은 무조건 성립한다.
____.getstatic(clazz.getName(), "valueX", "I") // 위에서 생성한 x값
.iconst_1() // 상수 1
.iadd() // 덧셈
.ifne(branch.branchOffset) // 0이 아니면 분기
// 여기서부터 절대 실행되면 안됨(dummy)
.getstatic("java/lang/System", "out", "Ljava/io/PrintStream;")
.ldc("never seen")
.invokevirtual("java/io/PrintStream", "println", "(Ljava/lang/String;)V")
.goto_(finder.targets.get(rand.nextInt(finder.targets.size())) - offset);
// 끝
// 명령어 교체
codeAttributeEditor.replaceInstruction(offset, ____.instructions());
}
}
위 pass를 적용하면 goto 명령어가 getstatic ~ goto로 대체된다.
apk 빌드 후 smali 코드를 확인해 보면 아래와 같다.
위에서는 ifne가 goto 명령어를 대체하므로, 무조건 참이 되는 수식을 작성하여 원래 가고자 했던 곳으로 점프시킨다.
그 밑에는 실행되면 안되는 fake코드를 넣고, 마지막에는 goto 명령어를 넣어서 임의의 위치를 가리키게 한다.
java 코드로 변환하면 아래와 같이 난독화 된 것을 볼 수 있다.