
1. 무결성(Integrity)
무결성(Integrity)은 애플리케이션 코드나 데이터가 변조되지 않았음을 보장하는 보안 기법입니다. 주로 악성 코드 삽입, 리패키징(앱 변조), 후킹 방어 등의 목적으로 사용됩니다.
무결성 검사 방법
- (1) 서명(Signature) 검사
- (2) 체크섬(Checksum) 검사
- (3) 메모리 무결성 검사(앱 실행 중 코드나 데이터가 조작되었는지 검사)
1-1. BYPASS APP NAME : 앱 이름 변조 여부 탐지 방식
IntegrityDetector 생성자에서 선언되는 ANDITER 값과 R.string.app_name의 값(문자열)을 비교하여 애플리케이션 이름이 변경되었는지 검사하고 있으므로 isCheckAppName() 함수의 리턴 값 변조 Intrinsics.areEqual(...) 함수의 인자 혹은 리턴 값을 변조하는 방법으로 우회가 가능합니다.

애플리케이션명 변경
현재 애플리케이션명이 ANDITER이며 특별한 코드 패치를 진행하지 않았으므로 앱 무결성이 변조되지 않았습니다. 실습을 위해 앱의 무결성 변조를 진행해보겠습니다.
<!-- \res\values => app_name -->
[ . . . ]
<string name="app_name">NEW-ANDITER</string>
[ . . . ]

앱의 무결성 변조를 검사하는 함수를 우회하기 위하여 Intrinsics.areEqual(...) 함수를 후킹하여 리턴 값을 변조하면 무결성 변조를 검사(애플리케이션 이름 변경) 로직을 우회할 수 있게 됩니다.
Java.perform(function () {
var Intrinsics = Java.use("kotlin.jvm.internal.Intrinsics");
Intrinsics.areEqual.overload("java.lang.Object", "java.lang.Object").implementation = function (args0, args1) {
var stackTrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
if (stackTrace.includes("com.playground.anditer.IntegrityDetector.isCheckAppName")) {
console.warn("[*] Intrinsics.areEqual() called inside isCheckAppName()");
console.log(`\tOriginal - Intrinsics.areEqual(${args0}, ${args1}) => ${this.areEqual(args0, args1)}`);
console.log(`\tModified - Intrinsics.areEqual(${"ANDITER"}, ${args1}) => ${this.areEqual("ANDITER", args1)}`);
return this.areEqual("ANDITER", args1);
}
return this.areEqual(args0, args1);
};
});
1-2. BYPASS HASH KEY : 사이니킹 변조 여부 탐지 방식
디컴파일 오류로 정확한 코드 분석은 어려웠는데요. 아래 코드를 살펴보면 패키지의 서명(Signing) 값을 가져온 뒤 MessageDigest 클래스로 SHA 해시 암호화를 진행하고 있습니다. 이후 이 값을 개발자가 기존에 선언한 해시 값(r3: "7nd5QagBehUoHzVC+c43zic+/ro=")[더 정확하게는 해시 값을 base64 인코딩한 값]과 비교하고, 동일하지 않으면 애플리케이션에 위/변조가 발생했다고 판단하고 있습니다.

여기서 서명 값(HASH) 변조를 우회할 수 있는 방법은 3가지로 나눌 수 있습니다. isCheckHashKey 함수의 리턴 값을 변조하거나, Intrinsics.areEqual 함수의 리턴/인자 값을 변조하거나, android.util.Base64.encodeToString(r1, r0)의 리턴 값을 기존에 개발자가 설정해놓은 값(7nd5QagBehUoHzVC+c43zic+/ro=)으로 변조하면 됩니다. 필자는 간단하게 1번째 방법을 사용했습니다.
Java.perform(function () {
var IntegrityDetector = Java.use("com.playground.anditer.IntegrityDetector");
IntegrityDetector.isCheckHashKey.implementation = function () {
console.log(this.isCheckHashKey());
return false;
};
});
1-3. BYPASS INSTALLER : 마켓 출처 탐지 방식
isCheckInstaller() 코드를 살펴보면 애플리케이션(APK)을 설치한 소스 정보를 가져온 뒤 installStore 배열에 선언된 소스(ex; 구글 플레이스토어(com.andorid.vending))에서 다운로드 받은 경우에만 검증을 통과시키고 있습니다.

따라서, isCheckInstaller() 함수의 리턴 값을 변조하거나, Intrinsics.areEqual(...) 함수의 리턴/인자 값을 변조하거나, this.mContext.getPackageManager().getInstallSourceInfo(this.mContext.getPackageName()).getInstallingPackageName() 함수의 리턴 값을 변조하면 우회가 가능합니다. 이번에는 2번째 방법으로 진행을 해보았습니다.
Java.perform(function () {
var IntegrityDetector = Java.use("com.playground.anditer.IntegrityDetector");
var Intrinsics = Java.use("kotlin.jvm.internal.Intrinsics");
var areEqual = Intrinsics.areEqual.overload(
"java.lang.Object",
"java.lang.Object"
);
areEqual.implementation = function (str, installerPackageName) {
var stackTrace = Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Exception").$new());
if (stackTrace.includes("com.playground.anditer.IntegrityDetector.isCheckInstaller")) {
var installStore = [
"com.android.vending",
"com.google.android.feedback",
"com.skt.skaf.A000Z00040",
"com.sec.android.app.samsungapps",
"com.sec.android.easyMover.Agent",
"com.google.android.packageinstaller",
"com.samsung.android.mateagent"
];
console.log(str, installerPackageName);
if (!installStore.includes(installerPackageName)) return true;
}
return this.areEqual(str, installerPackageName);
};
});
1-4. BYPASS CRC : Dex 파일 체크섬 변조 여부 탐지 방식
애플리케이션 내에 기존에 선언된 R.string.CRC_Check_Code 값과 classes.dex 파일의 CRC(Cyclic Redundancy Check) 값을 비교하여 동일하지 않으면 변조된 APK 파일로 탐지하는 방법입니다.

탐지 우회 방법은 아래와 같습니다.
String.valueOf(...).getCrc())리턴 값 변조new ZipFile(...)인자(파일 경로) 값 변조this.mContext.getString(R.string.CRC_Check_Code)리턴 값 변조Intrinsics.areEqual(...)리턴/인자 값 변조isCheckCRC리턴 값 변조
CRC(dex checksum) 검증 우회 코드 1
(1) String.valueOf(...) 리턴 값 변조를 통한 우회
위 이미지의 isCheckCRC() 함수 코드를 확인하면 valueOf, string 변수의 값을 비교하여 무결성 검증을 하고있는 것을 알 수 있는데요. 이 때 string(R.string.CRC_Check_Code) 값은 아래 이미지에서 볼 수 있듯이, 이미 리소스 파일 내에 선언된 값입니다. 만약 APK가 변조되었다면 valueOf 값이 이전과 달라지게 됩니다. 따라서 해당 값을 후킹하여 this.mContext.getString(R.string.CRC_Check_Code) 값으로 변환해주면 우회가 가능합니다.

(1) this.mContext.getString(R.string.CRC_Check_Code) 값 확인
Java.perform(function () {
// Context 클래스와 getString 메서드를 타겟팅
var context = Java.use("android.content.Context");
context.getString.overload("int").implementation = function (resId) {
// R.string.CRC_Check_Code 값을 확인
if (resId == 0x7f110000) {
// CRC_Check_Code의 리소스 ID
console.log("CRC_Check_Code is being accessed!");
}
// 원래 메서드를 호출하여 값을 반환
var result = this.getString(resId);
console.log("Returned value from getString: " + result);
return result;
};
});
/*
CRC_Check_Code is being accessed!
resId value is: 2131820544
Returned value from getString: 2303541361
*/(2) valueOf 인자 값을 2303541361로 변조
Java.perform(function () {
var String = Java.use("java.lang.String");
var valueOf = String.valueOf.overload("long");
valueOf.implementation = function (arg) {
var stackTrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
if (stackTrace.includes("com.playground.anditer.IntegrityDetector.isCheckCRC")) {
return "2303541361";
}
return this.valueOf(arg);
};
});
CRC(dex checksum) 검증 우회 코드 2
Intrinsics.areEqual(...) 인자 값 변조를 통한 우회
areEqual 함수를 후킹하여 인자(valueOf, string)에 동일한 값을 사용하면 우회가 가능합니다.
Java.perform(function () {
var Intrinsics = Java.use("kotlin.jvm.internal.Intrinsics");
var areEqual = Intrinsics.areEqual.overload(
"java.lang.Object",
"java.lang.Object"
);
areEqual.implementation = function (valueOf, string) {
var stackTrace = Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Exception").$new()
);
if (
stackTrace.includes("com.playground.anditer.IntegrityDetector.isCheckCRC")
) {
console.log(valueOf, string);
return this.areEqual(valueOf, valueOf);
}
return this.areEqual(valueOf, string);
};
});
REFERENCE
- https://github.com/naroSEC/Anditer?tab=readme-ov-file
- https://www.igloo.co.kr/security-information/anditer%eb%a5%bc-%ed%99%9c%ec%9a%a9%ed%95%9c-%ec%95%88%eb%93%9c%eb%a1%9c%ec%9d%b4%eb%93%9c-%ec%9c%84%ed%98%91-%ed%83%90%ec%a7%80-%eb%b0%8f-%ec%9a%b0%ed%9a%8c-%eb%b0%a9%ec%95%88-part-4-1-%eb%ac%b4/
- How to Hook Dynamic Loaded DEX Files