Android
Android 변조앱 만들기(feat. sandhook)
2022년 4월 20일
원문에서 보기 ↗1. 프롤로그
몇 년 전에 필자가 IT 컨설팅 업체에서 모의해킹 일을 하고 있을 때의 경험입니다.
어느 날 직장 동료에게 변조 매크로 앱을 분석해달라는 의뢰가 들어왔습니다. 저는 다른 프로젝트에 투입되어 있었지만 호기심이 생겨 곁눈질로 보게 되었습니다. 그동안 안드로이드 위/변조 테스트는 간단한 smali 코드를 삽입하는 정도였던 저에게 있어서 해당 변조 앱은 신선한 충격으로 다가왔습니다.
그 변조 앱은 순정 단말기의 최신 OS 버전에서 자유자재로 원본 앱의 메서드를 후킹 하여 로직을 조작하고 있었죠. 마치 숙련된 댄서가 무대 위에서 춤을 추고 있는 듯하였습니다. 심지어 원본 앱은 무결성 검증 로직이 있었는데도 말이죠!
도대체 어떻게 변조한 것일까? 이 생각이 머리를 떠나지 않았습니다.
2. 변조앱 분석
일단 변조 앱을 디컴파일 해보았습니다. 원본 앱에는 없는 "ABCDE"라는 클래스가 있고, 해당 클래스에서 Xposed를 import 하고 있었습니다. Xposed는 안드로이드 7 이전 버전에서 후킹 도구로써 유명세를 떨쳤었기 때문에 이 부분은 이해가 되었습니다. 그리고 sandhook이라는 것도 import 하고 있었는데, 일단 뭔지 잘 몰라서 넘어갔습니다. 
"ABCDE" 클래스에서는 xposed로 "android.app.ApplicationPackageManager.getPackageInfo" 같은 메서드를 후킹 하여 원래의 Signature 값으로 변조하여 무결성 검증을 우회하고 있는 것으로 생각되었습니다. 이외에도 많은 메서드를 후킹 하여 로직을 조작하고 있었습니다. 
여기까지는 이해가 되었는데, 도대체 이 방대한 코드들을 어떻게 원본 apk파일에 삽입하였을까 의문이 들더군요. 가능한 시나리오를 생각해보았습니다. ⓵ 원본 apk파일을 디컴파일한다음에, smali수준에서 하나하나 코드를 짠다 보통 안드로이드 보안점검 할때, apk파일 디컴파일 한다음에 smali수준에서 간단히 토스트 메세지 띄우는 코드를 삽입해서 무결성 검증을 하곤 합니다. 그래서, 이 생각이 가장 먼저 떠올랐는데요. 토스트 메세지야 한두줄이면 되고, 로직도 없어서 문제 없겠지만, 방대한 후킹로직을 smali로만 구성한다? 엄청난 고수면 가능하겠지만 일단 가능성이 좀 떨어진다고 생각하였습니다.
⓶ 자바로 코드를 짜고, smali로 변환시킨 다음에 원본 smali 파일에 주입한다 이 시나리오가 그나마 가능성이 있어 보였습니다.(나중에 알게 되었지만 smali로의 변환 과정은 필요 없었습니다) 그런데 무작정 자바 코드를 짠다고 해서 후킹이 되지는 않습니다. import xposed 또는 sandhook 한다고 해서 xposed나 sandhook이 import 되지는 않습니다. 분명 작성을 편하게 해주는 framework가 있겠거니라고 생각하였습니다.
그래서 sandhook(https://github.com/asLody/SandHook)을 찾아봤는데요, sandhook은 Lody라는 개발자가 만든 ART 및 Native Inline 후킹을 지원하는 라이브러리였습니다. 해당 라이브러리에서 Xposed API도 사용할 수 있도록 지원하고 있었죠. 퍼즐이 조금 풀리는 느낌이었습니다. 여기서 멈추지 않고 sandhook을 이용해서 순정 단말기에서 작동하는 변조 앱을 만들 수 있는지를 테스트하였고, 정말로 로직을 원하는 대로 조작한 변조 앱을 만들 수 있다는 것을 확인하였습니다. 그 후로 저는 종종 sandhook을 활용해서 안드로이드 모바일 앱의 위/변조 테스트를 하고 있습니다. 다음 섹션에서 sandhook을 이용해서 구체적으로 어떻게 변조 앱을 만드는지 설명해 보겠습니다.
3. 변조앱 만들기
유명한 안드로이드 리버싱 테스트 앱인 "uncrackable level3" 앱(https://github.com/OWASP/OWASP-mstg/blob/master/Crackmes/Android/Level_03/UnCrackable-Level3.apk)에 대한 변조 앱 제작 과정을 설명해 보고자 합니다.
(1) 토스트 메시지 띄우기
우선, smali 파일을 변조해서 간단한 토스트 메시지를 띄우는 변조 앱을 만들어보겠습니다. "uncrackable level3"앱을 디컴파일해서 "MainActivity.smali" 파일의 onCreate 메서드에 간단한 토스트 메시지를 띄우는 smali 코드를 삽입합니다. 
apk 파일을 리패키지, 사이닝 후 순정 단말기에 설치해 줍니다.(갤럭시 s10, Android OS 11) 설치 후 앱을 실행해 보면 "NHN CLOUD" 토스트 메시지가 뜨는 것을 확인할 수 있으며, "uncrackable level3"앱은 무결성 검증을 하고 있기 때문에 "Rooting or tampering detected" 알림 창도 뜨는 것을 확인할 수 있습니다. 
(2) 무결성 검증 우회
이제 sandhook을 이용하여 앱을 변조하여 저 변조 탐지 로직을 우회해 보도록 하겠습니다. "uncrackable level3"앱은 "java.util.zip.ZipEntry.getCrc()" 메서드를 이용하여 dex 파일의 무결성을 검증합니다. 따라서, getCrc() 메서드를 후킹 하여 원본 파일의 CRC 값으로 변조해 주면 무결성 검증 우회가 가능하겠습니다. 
안드로이드 스튜디오에서 build.gradle(:app)에서 sandhook dependencies를 설정해 줍니다. 
그다음으로는 MainModule 클래스에 sandhook에서 제공하는 xposed api를 이용해서 getCrc() 메서드를 후킹 하여 값을 변조하는 코드를 작성합니다. 원본 dex 파일의 crc 값은 25235683인데요, 토스트 메시지를 띄우는 smali 코드를 삽입했으므로 crc 값이 변하게 되었습니다. 따라서, xposed api를 이용해서 원본 앱의 crc 값을 반환하도록 코드를 작성한 것입니다. 
다음은 MainModule 클래스의 전체 소스코드입니다.
package com.hackcatml.sandhooktest;
import android.content.Context;
import android.util.Log;
import com.swift.sandhook.SandHook;
import com.swift.sandhook.SandHookConfig;
import com.swift.sandhook.xposedcompat.XposedCompat;
import java.util.zip.ZipEntry;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
public class MainModule {
static final String TAG = "[hackcatml]";
Context mContext;
String mTargetPackageName;
public MainModule(Context argv0){
this.mContext = argv0;
this.mTargetPackageName = argv0.getPackageName();
SandHookConfig.DEBUG = true;
SandHook.disableVMInline();
SandHook.tryDisableProfile(this.mTargetPackageName);
SandHook.disableDex2oatInline(false);
if(SandHookConfig.SDK_INT >= 28) {
SandHook.passApiCheck();
}
XposedCompat.cacheDir = XposedCompat.getCacheDir();
XposedCompat.context = argv0;
XposedCompat.classLoader = argv0.getClassLoader();
XposedCompat.isFirstApplication = true;
this.startHook();
}
public void startHook() {
try {
XposedHelpers.findAndHookMethod(ZipEntry.class, "getCrc", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
long result = (long) param.getResult();
Log.d(TAG, "getCrc result: " + result);
if(result != 25235683L && result != 1608485481L && result != 881998371L && result != 1618896864L && result != 2856060114L){
param.setResult(25235683L);
}
}
});
}
catch(Exception e) {
Log.d(TAG, "getPackageInfo Error");
}
}
}
"com.hackcatml.sandhooktest.MainModule"의 생성자를 호출해야 startHook() 메서드가 차례로 호출될 것입니다. 이 부분은 원본 smali 파일을 건드릴 필요가 있습니다. "Uncrackable Level3" apk 파일을 디컴파일해서 "MainActivity.smali" 파일의 onCreate 메서드에 다음 호출 구문을 삽입하였습니다. 
이제 안드로이드 스튜디오에서 apk 파일을 빌드 해주고, 빌드 한 apk 파일을 apktool로 디컴파일해줍니다.

lib/arm64-v8a/ 디렉터리에 "libsandhook.so 파일이 들어있는데, 이를 디컴파일한 "uncrackable level3"의 동일한 디렉터리 하위로 이동시켜 줍니다. 또한, smali 디렉터리들도 이름이 중복되지 않게 해서 "uncrackable level3" 디렉터리 내부로 옮겨줍니다. 
이제 "uncrackable level3"앱을 리패키지, 사이닝, 설치해 줍니다.(갤럭시 s10, Android OS 11 순정 단말기) 
앱을 실행하게 되면 "NHN CLOUD" 토스트 메시지는 뜨지만 무결성 검증 로직이 우회되어 변조 앱 알림 창이 더 이상 뜨지 않는 것을 확인할 수 있습니다. 
(3) 중요 로직 변조
"uncrackable level3"앱은 최종 flag를 입력받아 맞으면 "Success..."를 틀리면 "Nope..." 알림을 띄웁니다. 실제 앱이라면 비밀번호를 입력받아 특정 동작을 할 수도 있으므로 중요 로직이라고 가정할 수 있겠죠. 임의의 secret 입력하고 VERIFY 버튼을 클릭하면 "Nope..." 알림 창이 뜨고 있습니다. 이 부분을 변조해서 항상 "Success..." 알림 창이 뜨도록 변조해 보겠습니다. 
입력한 secret이 올바른 secret 인지 여부는 "check_code" 메서드에서 검사하여 boolean 값을 반환하고 있으므로 이 부분을 후킹 하여 항상 true를 반환하도록 변조하면 항상 "Success..." 알림이 뜰 것입니다. 
다음은 "무결성 검증 우회 + VERIFY 로직 변조"에 대한 전체 소스코드입니다.
package com.hackcatml.sandhooktest;
import android.content.Context;
import android.util.Log;
import com.swift.sandhook.SandHook;
import com.swift.sandhook.SandHookConfig;
import com.swift.sandhook.xposedcompat.XposedCompat;
import java.util.zip.ZipEntry;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
public class MainModule {
static final String TAG = "[hackcatml]";
Context mContext;
String mTargetPackageName;
public MainModule(Context argv0){
this.mContext = argv0;
this.mTargetPackageName = argv0.getPackageName();
SandHookConfig.DEBUG = true;
SandHook.disableVMInline();
SandHook.tryDisableProfile(this.mTargetPackageName);
SandHook.disableDex2oatInline(false);
if(SandHookConfig.SDK_INT >= 28) {
SandHook.passApiCheck();
}
XposedCompat.cacheDir = XposedCompat.getCacheDir();
XposedCompat.context = argv0;
XposedCompat.classLoader = argv0.getClassLoader();
XposedCompat.isFirstApplication = true;
this.startHook();
}
public void startHook() {
try {
XposedHelpers.findAndHookMethod(ZipEntry.class, "getCrc", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
long result = (long) param.getResult();
Log.d(TAG, "getCrc result: " + result);
if(result != 25235683L && result != 1608485481L && result != 881998371L && result != 1618896864L && result != 2856060114L){
param.setResult(25235683L);
}
}
});
// sg.vantagepoint.uncrackable3.CodeCheck.check_code(java.lang.String) : boolean
XposedHelpers.findAndHookMethod("sg.vantagepoint.uncrackable3.CodeCheck", XposedCompat.classLoader, "check_code", String.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Log.d(TAG, "check_code got hooked!");
param.setResult(true);
}
});
}
catch(Exception e) {
Log.d(TAG, "getPackageInfo Error");
}
}
}
이제 다시 임의의 값을 입력하고 VERIFY 버튼을 터치하면 "Success..." 알림이 뜨는 것을 확인할 수 있습니다. 
4. 결론
안드로이드 앱 변조 기술에 관심을 가지게 된 계기부터 시작해서 sandhook을 이용한 구체적인 앱 변조 과정을 알아보았습니다. 앱 변조라고 하면 어떤 생각부터 드시나요? 나쁜 기술? 막아야 하는 기술?
저는 앱 변조 기술이 나쁜 것은 아니라고 생각합니다. 쓰는 사람에 따라 달라질 뿐이죠. 잘 활용하면 원래 앱에서 제공하지 않는 기능을 추가해서 사용할 수 있는 유용한 기술입니다. 모듈을 추가하는 것처럼 말이죠. 다만 악용하는 사람들이 많기는 하므로 보안을 업으로 하는 입장에서 앱 변조가 가능하다면 취약점으로 잡고 있습니다. 대부분의 보안 설루션들 역시 모바일 앱의 위/변조 여부는 기본적으로 탐지하여 차단하고 있습니다.
앱 변조가 가능한 것은 알겠는데, 구체적으로 어떻게 만들지? 궁금하셨던 분들은 이번 기회에 학습용으로 한번 만들어 보는 게 어떨까요? 보안을 업으로 하신다면 모바일 앱에 대한 흥미와 함께 좀 더 깊이 있는 이해가 가능해질 것입니다. 긴 글 읽어주셔서 감사합니다.