안드로이드 악성코드 분석 시, 원본 dex를 숨기기 위해 dex파일 동적로딩을 하는 경우가 많이 있습니다.
동적으로 Dex파일을 로드하기 위해선 파일 혹은 메모리에서 로드하는데 다음과 같은 함수를 사용하게 됩니다.
From file: dalvik.system.DexFile.loadDex depreciated after API 26 dalvik.system.DexClassLoader dalvik.system.PathClassLoader
From memory: dalvik.system.InMemoryDexClassLoader (not common in malwares)
파일을 통한 동적로딩 중 DexClassLoader함수를 이용하여 동적로딩하는 코드 작성 및 분석 방법을 확인하겠습니다.
android developer를 통해 확인한 dexclassloader 함수 형식입니다.
아래는 dexclassloader를 이용하여 외부 apk의 dex를 로드하는 코드 예제입니다.
dexclassloader.java
public class MainActivity extends AppCompatActivity { static final int BUF_SIZE = 8 * 1024; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
findViewById(R.id.button).setOnClickListener( new Button.OnClickListener(){ public void onClick(View v){ String text = loadClass2(); Toast.makeText(getApplicationContext(),text,Toast.LENGTH_LONG).show(); } });
}
private String loadClass2(){ String APPJS = "test.apk"; // 불러올 apk 이름 명시 File dexInternalStoragePath = new File(getDir("cache",Context.MODE_PRIVATE), APPJS); BufferedInputStream bis = null; OutputStream dexWriter = null;
try { // asset 폴더 내 저장된 apk파일을 앱 내 read가 가능한 영역으로 복사
// (/data/data/<app>/app_cache/) bis = new BufferedInputStream(getAssets().open(APPJS)); dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath)); byte[] buf = new byte[BUF_SIZE]; int len; while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) { dexWriter.write(buf, 0, len); } dexWriter.close(); bis.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }
final File optimizedDexOutputPath = getDir("cache",Context.MODE_PRIVATE); // 복사한 apk파일 경로 획득 DexClassLoader dexClassLoader = new DexClassLoader(dexInternalStoragePath.getAbsolutePath(), optimizedDexOutputPath.getAbsolutePath(), null,getClassLoader()); //dex 파일 로드 try { Class clazz = dexClassLoader.loadClass("com.example.user.test.test"); // dex 파일 내 class 로드 Object object = clazz.newInstance(); Method method = clazz.getMethod("get_Message"); //class 내 method 로드 String text = (String) method.invoke(object); //method 호출 return text; }catch (Exception e){ e.printStackTrace(); } return "Error..!!"; }
}
test.java(test.apk)
package com.example.user.flag;
public class test{ public String get_Message(){ return "Catch Me!"; }
}
test.java의 경우 앱 실행은 되지 않지만 빌드를 통해 apk파일 생성 및 dexclassloader.java가 있는 프로젝트 assets 파일 내 apk파일 복사
이렇게 작성된 앱의 경우, assets파일 내 apk파일이 존재하는 것을 모른다면 test.java의 내용을 알기 어렵습니다.
원본 Dex 추출을 위해 앱 실행 및 단말기 내 /proc/<pid>/maps를 확인하여 불러오는 dex파일의 위치를 확인합니다.
그 후 해당 위치(위 예제 앱의 경우 /data/data/<package_name>/app_cache/)에 복사한 dex파일이 존재합니다.
다만, 복사한 dex파일은 odex파일로 dex형식으로 변환하여 원본소스코드 확인이 가능합니다.
첫 화면이고, 여러 문제를 볼 수 있는데 Login Method 1 / Check For Jailbreak / Show alert / Kill Application이 있지만 Login Method 1만 풀이를 올리도록 하겠습니다. 사실 하나만 풀면 나머지도 다 풀 수 있는거라.. ;)
IDA를 통해 login 문자열이 들어간 함수를 확인한 결과 ApplicationPatchingDetailsVC loginMethod1Tapped:를 확인할 수 있었습니다.
사실 소스코드를 좀만 살펴보면 소스에 ID/PW가 노출되어 있지만, 목적은 이게 아니니까 넘어가서 비교 후 분기문이나 인증 성공 함수 호출하는 부분을 찾아보겠습니다.
해당 함수의 끝 부분에 분기문을 통해 인증 결과값으로 성공 혹은 실패 로직을 실행하고 있습니다.
그럼 분기문(B.EQ loc_100170C58)을 변조하여 인증실패일 경우 SuccessPage를 호출하는 것으로 Binary를 패치하겠습니다.
이 페이지에서 opcode의 hex값을 확인 할 수 있는데, BEQ는 000100, BNE는 000101인 것을 확인 할 수 있습니다.
앱 다운로드 주소는 다음과 같고, 현재 v2까지 나와있는데 v1부터 풀어보도록 하겠습니다.
앱 설치 후에 왼쪽 상단 [Menu] - [Jailbreak Detection]으로 들어갑니다.
그럼 다음과 같은 화면을 볼 수 있는데, Jailbreak Test 1,2를 각각 클릭하면 탈옥여부를 탐지하여 Alert창을 띄워줍니다.
그럼 실행파일을 IDA를 통해 분석해보겠습니다.
함수명 중 "jail"이란 문자가 들어간 함수명을 검색했을 때 왼쪽상단과 같이 출력됩니다. Test1,2가 있는것으로 보아 각각 Test1,2를 클릭했을 때 실행되는 함수로 추측이 가능합니다.
소스코드 중 빨간 네모박스 위의 $OBJC_CLASS_$_DamnVulnerableAppUtilities의 isJailbroken함수로 넘어가네요.
즉, jailbreakTest1Tapped: 함수에서 isJailbroken함수를 호출하는 것을 알 수 있습니다.
isJailbroken함수를 살펴보면 bool return형을 가진 함수고 내부 탈옥탐지 로직을 확인할 수 있습니다.
위 사진에서는 Cydia.app만 보이지만 소스코드를 더 살펴보면 /bin/bash, apt, 등등이 있는지 확인하는 것을 알 수 있습니다.
그 후에 해당 파일이 존재하면 true(탈옥), 존재하지 않다면 false(미탈옥)을 리턴해서 탈옥여부를 확인합합니다.
Frida를 통해 리턴함수를 조작하여 탈옥탐지를 우회하도록 하겠습니다.
if (ObjC.available) {
try { var className = "JailbreakDetectionVC"; # 후킹할 클래스 명 var funcName = "- isJailbroken"; # 후킹할 클래스 내 함수명(앞의 -도 적어주셔야 합니다.) var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
Interceptor.attach(hook.implementation,{ onLeave:function(retval){ console.log("[*] Class Name: "+ className); #onLeave함수를 통해 함수호출후 리턴값 조작 console.log("[*] Method Name: "+ funcName); console.log("\t[-] Type of return value: "+typeof retval); console.log("\t[-] Original Return Value: "+ retval); newretval = ptr("0x0") retval.replace(newretval) # true를 false로 변조하여 탈옥탐지 우회 시도 console.log("\t[-] New Return Value: " + newretval) } });