악성코드 안에 패킹된 실행파일 뜯어내기
Reverse Engineering Packed Binaries to Discover Hidden Logic
Introduction
지난 글에 이어서 이번엔 dex 파일을 복호화하여 취해보려고 한다.
이전에 암호화된 문자열에 대한 정보를 얻었으니 리플렉션을 시도하는 API 에서 어떤 API 를 정확히 호출하는지 알 수 있을 것이다.
How We Got Here
원본 코드는 아래와 같다.
복호화된 문자열을 사용하여 로드할 때 호출하는 API 확인하기
아래는 dex 를 로드할 때의 코드를 복호화 문자열을 적용한 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public fm(Context context0) {
Object object4;
Object object3;
Class class2;
File file0;
Object object2;
int v1;
Class class1;
Field field1;
Field field0;
Class class0 = File.class;
super();
try {
field0 = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
field0.setAccessible(true);
Object object0 = field0.get(context0.getClassLoader());
field1 = object0.getClass().getDeclaredField("dexElements");
field1.setAccessible(true);
Object object1 = field1.get(object0);
class1 = Array.get(object1, 0).getClass();
v1 = Array.getLength(object1);
object2 = Class.forName("java.lang.reflect.Array").getDeclaredMethod("newInstance", class1.getClass(), Integer.TYPE).invoke(null, class1, ((int)(v1 + 1)));
for(int v = 0; v < v1; ++v) {
Array.set(object2, v, Array.get(object1, v));
}
file0 = fm.fm(context0, new Double(0.0), new Long(0L)); // dex file
class2 = Class.forName("dalvik.system.DexFile");
object3 = class2.getConstructor(class0).newInstance(file0); // new DexFile(File.class)
}
catch(Exception exception0) {
exception0.printStackTrace();
return;
}
try {
object4 = null;
object4 = class1.getConstructor(class2).newInstance(object3);
}
catch(Throwable unused_ex) {
}
if(object4 == null) {
try {
object4 = class1.getConstructor(class0, Boolean.TYPE, class0, class2).newInstance(file0.getParentFile(), Boolean.FALSE, 0, object3);
}
catch(Exception unused_ex) {
}
}
if(object4 == null) {
try {
object4 = class1.getConstructor(class2, class0).newInstance(object3, file0);
goto label_28;
}
catch(Exception unused_ex) {
try {
Object[] arr_object = {file0, Class.forName("java.util.zip.ZipFile").getConstructor(class0).newInstance(file0), object3};
object4 = class1.getConstructor(class0, ZipFile.class, class2).newInstance(arr_object);
label_28:
Array.set(object2, v1, object4);
field1.set(field0.get(context0.getClassLoader()), object2);
return;
}
catch(Exception exception0) {
}
}
}
else {
goto label_28;
}
exception0.printStackTrace();
}
object4 == null
을 계속 확인하는 걸 보니까 어떻게든 getConstructor
를 통해서 이를 바롭 잡으려고 하는게 보인다. 해당 코드는 dex 파일 로직을 보면서 다시 봐보겠다.
실행파일 언패킹하기
아래는 assets 에 있는 파일을 통해 실행파일을 복호화한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static File fm(Context context0, Double double0, Long long0) throws Exception {
String s = aE.aE("uny|f");
File file0 = new File(context0.getFilesDir(), aE.aE("l52m2%1~3yzc"));
file0.createNewFile();
String s1 = aE.aE("ecjNprgva");
Object object0 = Context.class.getMethod(s1, null).invoke(context0, null);
InputStream inputStream0 = (InputStream)object0.getClass().getMethod(aE.aE("ylva"), String.class).invoke(object0, aE.aE(")6*79->,.!//66;206."));
int v = inputStream0.available();
byte[] arr_b = new byte[v];
int v1 = inputStream0.read(arr_b);
inputStream0.close();
byte[] arr_b1 = new byte[v1];
new Random(0x1B32D871E7B02153L).nextBytes(arr_b1);
for(int v2 = 0; v2 < v1; ++v2) {
arr_b[v2] = (byte)(arr_b[v2] ^ arr_b1[v2]);
}
Class class0 = Class.forName(aE.aE("ruto/|k=Evsu^zeyw{F{bhxm"));
Object object1 = class0.getConstructor(File.class).newInstance(file0);
try {
class0.getMethod(aE.aE("ap\u007F{f"), byte[].class, Integer.TYPE, Integer.TYPE).invoke(object1, arr_b, ((int)0), v);
return file0;
}
finally {
class0.getMethod(s, null).invoke(object1, null);
}
}
복호화된 문자열을 적용해주면 다음과 같이 볼 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static File fm(Context context0, Double double0, Long long0) throws Exception {
// String s = aE.aE("uny|f");
String s = "close";
// File file0 = new File(context0.getFilesDir(), aE.aE("l52m2%1~3yzc"));
File file0 = new File(context0.getFilesDir(), "c06b102c.dex");
file0.createNewFile();
// String s1 = aE.aE("ecjNprgva");
String s1 = "getAssets";
Object object0 = Context.class.getMethod(s1, null).invoke(context0, null);
// InputStream inputStream0 = (InputStream)object0.getClass().getMethod(aE.aE("ylva"), String.class).invoke(object0, aE.aE(")6*79->,.!//66;206."));
InputStream inputStream0 = (InputStream)object0.getClass().getMethod("open", String.class).invoke(object0, "1959866771589505364");
int v = inputStream0.available();
byte[] arr_b = new byte[v];
int v1 = inputStream0.read(arr_b);
inputStream0.close();
byte[] arr_b1 = new byte[v1];
new Random(0x1B32D871E7B02153L).nextBytes(arr_b1);
for(int v2 = 0; v2 < v1; ++v2) {
arr_b[v2] = (byte)(arr_b[v2] ^ arr_b1[v2]);
}
// Class class0 = Class.forName(aE.aE("ruto/|k=Evsu^zeyw{F{bhxm"));
Class class0 = Class.forName("java.io.FileOutputStream");
Object object1 = class0.getConstructor(File.class).newInstance(file0);
try {
// class0.getMethod(aE.aE("ap\u007F{f"), byte[].class, Integer.TYPE, Integer.TYPE).invoke(object1, arr_b, ((int)0), v);
class0.getMethod("write", byte[].class, Integer.TYPE, Integer.TYPE).invoke(object1, arr_b, ((int)0), v);
return file0;
}
finally {
class0.getMethod(s, null).invoke(object1, null);
}
}
더 나아가 아래와 같이 코드를 정리할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void fm() throws Exception {
File file0 = new File( "c06b102c.dex");
file0.createNewFile();
InputStream inputStream0 = new FileInputStream(new File("1959866771589505364"));
int v = inputStream0.available();
byte[] arr_b = new byte[v];
int v1 = inputStream0.read(arr_b);
inputStream0.close();
byte[] arr_b1 = new byte[v1];
new Random(0x1B32D871E7B02153L).nextBytes(arr_b1);
for(int v2 = 0; v2 < v1; ++v2) {
arr_b[v2] = (byte)(arr_b[v2] ^ arr_b1[v2]);
}
FileOutputStream fileOutputStream0 = new FileOutputStream(file0);
fileOutputStream0.write(arr_b, 0, v1);
fileOutputStream0.close();
}
assets 에서 1959866771589505364 파일 추출
해당 로직에 따라 dex 파일을 풀어주기 위해 assets 에 있는 1959866771589505364
파일을 뽑아 c06b102c.dex
로 언패킹해준다.
먼저 1959866771589505364
을 추출한다.
1
2
3
4
5
6
7
8
9
10
11
% unzip -l 309ebe1aa5a9d02949c263cdb646f40cac0024850d0860e71dbd634610504ab2.apk
Archive: 309ebe1aa5a9d02949c263cdb646f40cac0024850d0860e71dbd634610504ab2.apk
Length Date Time Name
--------- ---------- ----- ----
0 01-01-1981 01:01 META-INF/com/android/build/gradle/app-metadata.properties
25 01-01-1981 01:01 assets/dexopt/baseline.prof
26 01-01-1981 01:01 assets/dexopt/baseline.profm
7884 01-01-1981 01:01 classes.dex
2209224 01-01-1981 01:01 assets/1959866771589505364
1738 01-01-1981 01:01 DebugProbesKt.bin
{ . . . }
1
2
unzip 309ebe1aa5a9d02949c263cdb646f40cac0024850d0860e71dbd634610504ab2.apk assets/1959866771589505364 -d sample_309
Archive: 309ebe1aa5a9d02949c263cdb646f40cac0024850d0860e71dbd634610504ab2.apk
상황에 따라 압축을 풀거나 특정 파일만 뽑도록 진행한다. 상황이 여의치 않은 경우 jadx 를 통해 진행하거나 간단하게 Zip 파일을 푸는 코드를 작성해놓아도 괜찮을 거 같다.
c06b102c.dex 획득하기
1
2
3
4
Created c06b102c.dex
length is 2209224 Bytes.
Writing..
Done.
c06b102c.dex 디컴파일하기
c06b102c.dex 을 획득했다면 디컴파일러를 통해 확인해본다.
뽑은 c06b102c.dex
를 디컴파일러에 올리니 에러가 난다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
jadx.plugins.input.dex.DexException: Unexpected endian tag: 0x5d72c78
at jadx.plugins.input.dex.sections.DexHeader.<init>(DexHeader.java:32)
at jadx.plugins.input.dex.DexReader.<init>(DexReader.java:22)
at jadx.plugins.input.dex.DexFileLoader.loadSingleDex(DexFileLoader.java:110)
at jadx.plugins.input.dex.DexFileLoader.lambda$loadDexReaders$2(DexFileLoader.java:99)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at jadx.plugins.input.dex.DexFileLoader.loadDexReaders(DexFileLoader.java:100)
at jadx.plugins.input.dex.DexFileLoader.load(DexFileLoader.java:75)
at jadx.plugins.input.dex.DexFileLoader.loadDexFromFile(DexFileLoader.java:58)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
at jadx.plugins.input.dex.DexFileLoader.collectDexFiles(DexFileLoader.java:53)
at jadx.plugins.input.dex.DexInputPlugin.loadFiles(DexInputPlugin.java:42)
at jadx.plugins.input.dex.DexInputPlugin.loadFiles(DexInputPlugin.java:38)
at jadx.api.JadxDecompiler.loadInputFiles(JadxDecompiler.java:157)
at jadx.api.JadxDecompiler.load(JadxDecompiler.java:123)
at jadx.gui.JadxWrapper.open(JadxWrapper.java:77)
at jadx.gui.ui.MainWindow.lambda$loadFiles$3(MainWindow.java:562)
at jadx.core.utils.tasks.TaskExecutor.wrapTask(TaskExecutor.java:166)
at jadx.core.utils.tasks.TaskExecutor.runStages(TaskExecutor.java:142)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:840)
무슨 에러인지 정확히 어디서 어떻게 난건지 확인해본다.
jadx DexFileLoader 에는 Dex header 를 읽는 코드가 있는데 이때 엔디언 태그가 예상과 다르면 Exception 을 던진다.
Hex-Editor 를 통해 Binary 를 확인해본다.
1
2
3
4
00000000: 504b 0304 1400 0808 0800 5608 755a 0000 PK........V.uZ..
00000010: 0000 0000 0000 0000 0000 0b00 0000 636c ..............cl
00000020: 6173 7365 732e 6465 782c d705 94d4 d5ff asses.dex,......
00000030: 30e0 a14b 6297 5240 1104 2454 1a41 babb 0..Kb.R@..$T.A..
0x28 부터 4 Bytes 가 엔디언 태그로 읽은 것이다.
그런데 확인해보니 파일 확장자는 dex 로 끝나지만 file magic 은 zip magic 이다.
그러면 당연하게 zip 으로 열려야 하는거 아닐까? 문제가 되는 부분의 jadx 코드를 봐본다.
1
2
3
4
5
6
7
8
9
10
11
12
65 private List<DexReader> load(@Nullable File file, InputStream inputStream, String fileName) throws IOException {
66 try (InputStream in = inputStream.markSupported() ? inputStream : new BufferedInputStream(inputStream)) {
67 byte[] magic = new byte[DexConsts.MAX_MAGIC_SIZE];
68 in.mark(magic.length);
69 if (in.read(magic) != magic.length) {
70 return Collections.emptyList();
71 }
72 if (isStartWithBytes(magic, DexConsts.DEX_FILE_MAGIC) || fileName.endsWith(".dex")) { // <<<<<<<
73 in.reset();
74 byte[] content = readAllBytes(in);
75 return loadDexReaders(fileName, content);
76 }
사실 Dex magic 만 검사해도 되지만 jadx 측에서 파일 확장자도 검사하는 것인데 이 조건이 or 로 묶여 dex magic 이 아닌데 파일 확장자가 dex 일 경우 dex 파일로 인식하는 문제가 발생한 것이다.
이를 해결하기 위하여 일시적으로 파일에 확장자 이름을 지워주고서 다시 jadx 로 열거나 jeb decompiler 로 열어주면 된다.
해당 문제는 jadx 개발자에게 report 하였으며 다른 방안으로 조치할 예정이다.
1
2
3
4
5
6
7
% unzip -l c06b102c.dex
Archive: c06b102c.dex
Length Date Time Name
--------- ---------- ----- ----
6103848 03-21-2025 01:02 classes.dex
--------- -------
6103848 1 file
1
2
3
% unzip c06b102c.dex -d dex
Archive: c06b102c.dex
inflating: dex/classes.dex
zip 을 풀어주고 최종적으로 dex 를 획득한다.
잠깐 봤지만 언패킹된 dex 파일은 아래의 갯수만큼 클래스가 있다.
1
2
3
4
5
6
7
[] Dex file: dex/classes.dex
[] DEX magic: 64 65 78 0A 30 33 35 00
[] DEX version: 035
[] Adler32 checksum: 0x619573da
[] SHA1 signature: cb990adb92594eab847711d086a1faf7e840589b
[] Number of classes in the archive: 6777
Conclusion
디음엔 언패킹된 dex 파일 안에 로직을 살펴보고자 한다.
If you find any errors, please let me know by comment or email. Thank you.