최근 모바일 진단 중 루팅탐지를 하고 있지만 어디서 호출하는지를 찾을 수가 없었는데 어찌저찌 하다가 Dynamic Dex Loading이 적용된 것을 발견 하였습니다.
Dynamic Dex Loading을 하는 App에서 DEX를 추출하는 방법들에 대해서 알아봅시다.
Dynamic Dex Loading 🔵⚪️🔴
Dynamic Dex Loading 기법은 APP 분석을 어렵게 하기 위해 사용하는 난독화 기법입니다. 보통은 APK내 일반적인 경로(classes.dex)가 아닌 다른 경로(assets, App 내부 저장소)에 암호화되어 저장되어 있으며 앱이 실행될 때 복호화되어 메모리에 로드 됩니다. 복호화 과정을 거친 후 삭제 되기 때문에 일반적으로는 찾기가 어렵습니다.
How to Extract Dynamic DEX?
암호화된 DEX를 복호화 하는 방법은 Java Code 내에서 할 수도 있고 JNI Library 파일을 통한 복호화등이 있습니다. 난독화 된 APP에서 암호화 된 DEX 파일을 찾고 복호화 방법을 찾는 방법은 시간이 많이 소요됩니다.
빠른 시간 내 우회를 하기 위해서는 복호화된 DEX가 메모리에 로드되거나 삭제된다는 것으로 시작할 수 있습니다.
다음과 같은 우회 방법을 생각할 수 있습니다.
- Android 기기 내 폴더 복사
- 메모리에 올라간 복호화 된 DEX 추출
- JNI Library 함수 후킹
Find Dymanic DEX Loading
가장 먼저 Dynamic DEX Loading 기법을 사용하고 있는지 확인을 해야합니다. 확인하는 방법으로는 loadDEX(), openDexFile() 함수 후킹을 통해 인자 값에서 확인할 수 있습니다. 추가적으로 /proc/{pid}/maps | grep delete 명령어를 통해 메모리에서 삭제된 DEX를 확인할 수 있습니다.
Dynamic Dex Extract - Shell Script
Android 기기 내 폴더 복사를 통해 DEX 파일을 추출하는 방법입니다. Shell Scipt로 삭제되는 DEX 폴더의 위치를 알고 있을 때 0.1초마다 해당 폴더를 지워지지않도록 다른 폴더에 복사를 하는 방법입니다.
1
2
3
4
5
6
7
8
9
10
11
#!/system/bin/sh
cnt=0
while [ 1 ]
do
if [ $cnt -ge 0]; then
echo "Copy Dex -->>>>>>>";
cp -r {DEX 폴더} /sdcard/test/DEX$cnt;
fi
cnt=$(($cnt+1))
sleep 0.1
done
Dynamic Dex Extract - JNI Function
JNI Library 함수에서 파일을 삭제시키는 함수를 후킹하여 삭제하지 않도록 하는 방법입니다. Unlink(), Remove() 함수등이 있습니다.
unlink()함수가 실행될때 NativeCallback으로 함수를 재정의 하여 삭제가 아닌 console.warn() 만 실행되도록 합니다.
1
2
3
4
5
6
7
function unlink(){
var unlink = Module.findExportByName(null, 'unlink');
var open = new NativeFunction(unlink, 'void', ['int']);
Interceptor.replace(open, new NativeCallback(function(){
console.warn('[*] unlink Hook complete');
}, 'void', ['int']));
}
Dynamic Dex Extract - Memory Load DEX
복호화된 DEX는 메모리에 로드되기 때문에 메모리에서 복호화된 DEX를 추출하는 방법입니다.
❗️❗️원하는 DEX를 추출하여도 디컴파일이 되지 않는다면 바이너리 편집 도구(HxD)를 이용하여 DEX 시그니처(64 65 78 0a 30)를 찾아 불필요한 영역을 지워야 합니다.
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
Java.perform(function (){
var ThreadDef = Java.use('java.lang.Thread');
var ThreadObj = ThreadDef.$new();
function stackTrace() {
var stack = ThreadObj.currentThread().getStackTrace();
for (var i = 0; i < stack.length; i++) {
console.log(i + " => " + stack[i].toString());
}
console.log("-------------------------------------");
}
var DexFile = Java.use("dalvik.system.DexFile");
DexFile.loadDex.overload('java.lang.String', 'java.lang.String', 'int', 'java.lang.ClassLoader', '[Ldalvik.system.DexPathList$Element;').implementation = function(sourcepath, outputPathName, flags,arg4,arg5){
console.warn(sourcepath);
if(outputPathName.indexOf("{찾는dex}")>-1){
console.log("[*] arg1 : "+sourcepath);
}
ExtractDexFile();
return this.loadDex(sourcepath, outputPathName, flags,arg4,arg5)
}
})
function ExtractDexFile() {
console.warn('Dex Extract');
Process.enumerateRanges('r--').forEach(function (range) {
try {
if(range.file.path && (range.file.path.startsWith("/data/dalvik-cache/") || range.file.path.startsWith("/system/") || range.file.path.startsWith("/dev/"))) {
return;
}
else {
Memory.scanSync(range.base, range.size, "64 65 78 0a 30 ?? ?? 00").forEach(function (match) {
console.log('\x1b[32m[*] file_path : ' + range.file.path + '\x1b[0m');
var dex_addr = match.address;
var dex_size = dex_addr.add(0x20).readUInt();
console.log('\x1b[33m[*] dex_addr : ' + dex_addr + '\x1b[0m');
console.log('\x1b[36m[*] dex_size : ' + dex_size + '\x1b[0m');
if(range.file.path.indexOf("{찾는 DEX 이름}")>-1){
var file = new File("{생성할 DEX File Name}", "wb");
file.write(Memory.readByteArray(dex_addr, dex_size));
file.flush();
file.close();
}
});
}
} catch (e) {}
})
console.warn('[!] Extract Complete ! :)');
}
//frida -U -f com.kakaobank.channel -l a.js --no-pause
Reference
https://blog.naver.com/gigs8041/222137154226 https://www.igloo.co.kr/security-information/악성-apk을-이용한-dynamic-dex-loading-분석/
후기
Android 여러 버전에서 시도를 해봤는데 모든 버전이 되지 않았습니다. 요건 되고 저건 안되고… 다른 버전에선 요게 되고 다른게 안되고…. 신기 했지만 왜 안되는지는 결국 못찾았습니다😱😱 아직 많이 부족하기에.. 더 열심히 해야겠습니다