[IOS] IOS 앱 무결성 검증 우회 및 대응 방법
IOS 앱 무결성 검증
iOS 앱 무결성 검증은 iOS 운영체제를 실행하는 디바이스에서 애플리케이션의 무결성을 확인하고 변조되지 않았음을 보장하는 과정이다.
이를 통해 앱의 코드와 데이터가 보안 위협으로부터 안전하게 보호되며, 사용자의 신뢰와 개인정보 보호를 강화한다.
iOS 앱 무결성 검증은 주로 코드 서명, 앱 스토어 연결, 앱 카탈로그 파일 등의 메커니즘을 활용하여 수행된다.
iOS 앱 무결성 검증의 핵심 원리는 코드와 데이터의 변조를 방지하고, 신뢰할 수 있는 출처를 확인하여 사용자의 보안을 보장하는 것이고 이를 위해 아래와 같은 메커니즘이 사용된다:
- 코드 서명 (Code Signing): 앱의 코드와 데이터는 디지털 서명으로 보호됩니다. 애플 또는 앱 개발자로부터 발급된 서명은 코드의 무결성을 보장하며, 변조된 앱을 탐지할 수 있습니다.
- 앱 스토어 검증: 앱은 앱 스토어를 통해 배포되기 전에 애플의 검증을 거칩니다. 앱 스토어는 앱의 무결성과 보안을 검증하여 사용자의 신뢰를 유지합니다.
- 앱 스토어 연결 (App Store Connect): 개발자는 앱을 앱 스토어와 연결하여 앱 ID와 프로비전 프로필을 사용하여 앱을 서명하고 무결성을 보장합니다.
부가개념:
- 앱 트랜스페어런스 (App Transport Security): ATS는 네트워크 통신에 대한 보안 표준을 적용하여 데이터 보호와 통신 보안을 강화한다.
- 앱 카탈로그 파일 (App Catalog): 앱 카탈로그는 앱의 버전 및 서명 정보를 포함하여 앱의 무결성과 출처를 확인하는 데 사용된다.
IOS 앱 코드 서명 예시:
// iOS 앱 코드 서명 예시
let appBundleURL = Bundle.main.bundleURL
do {
let appAttributes = try FileManager.default.attributesOfItem(atPath: appBundleURL.path)
if let codeSignature = appAttributes[FileAttributeKey("SignerIdentity")] as? String {
print("앱 코드 서명: \(codeSignature)")
}
} catch {
print("앱 코드 서명을 읽을 수 없습니다.")
}
// 앱 카탈로그 파일 예시
let catalogURL = Bundle.main.url(forResource: "AppCatalog", withExtension: "plist")
if let catalogInfo = NSDictionary(contentsOf: catalogURL) {
if let appVersion = catalogInfo["AppVersion"] as? String, let appSignature = catalogInfo["AppSignature"] as? String {
print("앱 버전: \(appVersion)")
print("앱 서명: \(appSignature)")
}
}
- Bundle.main.bundleURL: 현재 앱 번들의 URL을 가져온다.
- FileManager.default.attributesOfItem(atPath:): 주어진 경로의 아이템 (파일 또는 디렉토리)의 속성을 가져온다. 여기서는 앱 번들의 속성을 가져온다.
- FileAttributeKey("SignerIdentity"): 파일 속성 중에서 "SignerIdentity" 키에 해당하는 값을 읽어온다. 이 값은 코드 서명 정보를 나타낸다.
- if let codeSignature = appAttributes[FileAttributeKey("SignerIdentity")] as? String: 파일 속성에서 읽어온 코드 서명 정보를 문자열로 변환하여 codeSignature 변수에 저장
- print("앱 코드 서명: \(codeSignature)"): 코드 서명 정보를 출력한다.
- Bundle.main.url(forResource:withExtension:): 앱 번들 내의 "AppCatalog.plist" 파일의 URL을 가져온다.
- NSDictionary(contentsOf:): 주어진 URL의 내용을 NSDictionary로 읽어온다.
- if let appVersion = catalogInfo["AppVersion"] as? String: "AppCatalog.plist"에서 "AppVersion" 키에 해당하는 값을 문자열로 변환하여 appVersion 변수에 저장한다.
- if let appSignature = catalogInfo["AppSignature"] as? String: "AppCatalog.plist"에서 "AppSignature" 키에 해당하는 값을 문자열로 변환하여 appSignature 변수에 저장한다.
- print("앱 버전: \(appVersion)")와 print("앱 서명: \(appSignature)"): 앱의 버전 정보와 서명 정보를 출력한다.
*앱 번들 : 앱 번들은 앱을 실행하는 데 필요한 모든 구성 요소를 담고 있는 디렉토리입니다. 이 디렉토리 안에는 앱의 실행 파일, 이미지, 사운드, 아이콘, NIB 파일, 리소스 파일 등이 포함된다.
대응 방법:
- 앱의 코드와 데이터에 디지털 서명을 추가하여 변조를 방지합니다.
- 앱을 앱 스토어를 통해 배포하여 애플의 검증을 거치도록 합니다.
- 앱 스토어 연결을 통해 앱을 서명하고 무결성을 확보합니다.
- 애플리케이션 소스코드 무결성 검사
- 파일 스토리지 무결성 검사
- DCAppAttestService
- Memory 기반 hash 검증 : 메모리상에서 실행 모듈 base 주소 획득 후 접근하여 data(실행 파일 전체, Code section, File signature 등) Hash, 암호화를 통해 검증할 수 있다. 주로 코드 섹션 __TEXT 세그먼트의 __text 섹션의 해시를 검증한다.
+
애플리케이션 소스 코드 무결성 검사
Apple은 DRM으로 무결성 검사를 처리한다. 그러나 추가 컨트롤이 가능하다.
mach_header는 서명을 생성하는 데 사용되는 명령 데이터의 시작을 계산하기 위해 구문 분석된다.
다음으로 서명이 주어진 서명과 비교된다. 생성된 서명이 다른 곳에 저장되거나 코딩되었는지 확인해야한다.
int xyz(char *dst) {
const struct mach_header * header;
Dl_info dlinfo;
if (dladdr(xyz, &dlinfo) == 0 || dlinfo.dli_fbase == NULL) {
NSLog(@" Error: Could not resolve symbol xyz");
[NSThread exit];
}
while(1) {
header = dlinfo.dli_fbase; // Pointer on the Mach-O header
struct load_command * cmd = (struct load_command *)(header + 1); // First load command
// Now iterate through load command
//to find __text section of __TEXT segment
for (uint32_t i = 0; cmd != NULL && i < header->ncmds; i++) {
if (cmd->cmd == LC_SEGMENT) {
// __TEXT load command is a LC_SEGMENT load command
struct segment_command * segment = (struct segment_command *)cmd;
if (!strcmp(segment->segname, "__TEXT")) {
// Stop on __TEXT segment load command and go through sections
// to find __text section
struct section * section = (struct section *)(segment + 1);
for (uint32_t j = 0; section != NULL && j < segment->nsects; j++) {
if (!strcmp(section->sectname, "__text"))
break; //Stop on __text section load command
section = (struct section *)(section + 1);
}
// Get here the __text section address, the __text section size
// and the virtual memory address so we can calculate
// a pointer on the __text section
uint32_t * textSectionAddr = (uint32_t *)section->addr;
uint32_t textSectionSize = section->size;
uint32_t * vmaddr = segment->vmaddr;
char * textSectionPtr = (char *)((int)header + (int)textSectionAddr - (int)vmaddr);
// Calculate the signature of the data,
// store the result in a string
// and compare to the original one
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(textSectionPtr, textSectionSize, digest); // calculate the signature
for (int i = 0; i < sizeof(digest); i++) // fill signature
sprintf(dst + (2 * i), "%02x", digest[i]);
// return strcmp(originalSignature, signature) == 0; // verify signatures match
return 0;
}
}
cmd = (struct load_command *)((uint8_t *)cmd + cmd->cmdsize);
}
}
}
해당 코드는 C 언어로 작성된 함수로, 주어진 문자열 포인터 dst에 대해 앱의 __TEXT 섹션의 데이터를 해시하여 서명을 생성하고 원래 서명과 비교하는 작업을 수행한다.
- int xyz(char *dst): 함수의 시작을 정의하며, dst는 서명 값을 저장하기 위한 문자열 포인터이다. 함수의 반환 값은 검증 결과를 나타낸다.
- const struct mach_header * header;: mach_header 구조체에 대한 포인터로, 현재 앱의 Mach-O 헤더 정보를 저장한다.
- Dl_info dlinfo;: Dl_info 구조체에 대한 변수로, dladdr 함수로부터 반환되는 정보를 저장한다.
- if (dladdr(xyz, &dlinfo) == 0 || dlinfo.dli_fbase == NULL) { ... }: dladdr 함수를 사용하여 현재 함수 xyz의 정보를 가져온다. 만약 정보를 가져오지 못하거나 기본 주소가 없다면 에러를 출력하고 프로그램을 종료한다.
- while (1) { ... }: 무한 루프를 시작하여 Mach-O 헤더에서 세그먼트와 섹션을 찾고 서명을 생성하는 작업을 반복한다.
- header = dlinfo.dli_fbase;: 현재 함수의 기본 주소에서 Mach-O 헤더를 가져와 header에 저장한다.
- struct load_command * cmd = (struct load_command *)(header + 1);: 헤더의 다음 위치에 있는 첫 번째 로드 커맨드를 가져와 cmd에 저장한다.
- for (uint32_t i = 0; cmd != NULL && i < header->ncmds; i++) { ... }: 로드 커맨드를 순회하며 __TEXT 세그먼트를 찾는 작업을 수행헌다.
- if (cmd->cmd == LC_SEGMENT) { ... }: 로드 커맨드의 타입이 LC_SEGMENT인 경우 세그먼트 정보를 가져온다.
- struct segment_command * segment = (struct segment_command *)cmd;: 세그먼트 정보를 가져와 segment에 저장한다.
- if (!strcmp(segment->segname, "__TEXT")) { ... }: 세그먼트 이름이 __TEXT인 경우 섹션 정보를 찾는다.
- struct section * section = (struct section *)(segment + 1);: 섹션 정보를 가져와 section에 저장한다.
- for (uint32_t j = 0; section != NULL && j < segment->nsects; j++) { ... }: 섹션을 순회하며 __text 섹션을 찾는다.
- if (!strcmp(section->sectname, "__text")) break;: 섹션이 __text 섹션이면 반복을 멈춘다.
- 서명을 생성하기 위해 __text 섹션의 데이터를 해시합니다. 해시 알고리즘으로 MD5를 사용하고, 생성된 해시 값을 dst에 저장한다.
- return 0;: 함수의 무결성 검증이 완료되었으므로 0을 반환한다.
해당 코드는 Mach-O 헤더 및 로드 커맨드를 분석하여 __TEXT 섹션의 데이터를 해시하여 서명을 생성하고, 이 서명을 기존 서명과 비교하여 앱의 무결성을 검증하는 메커니즘을 나타내고 있다.
우회방법:
- 안티 디버깅 기능을 패치하고 관련 코드를 NOP 명령으로 덮어써 원치 않는 동작을 비활성화한다.
- 코드의 무결성을 평가하는 데 사용되는 저장된 해시를 패치한다.
- Frida를 사용하여 파일 시스템 API를 연결하고 수정된 파일 대신 원본 파일에 대한 핸들을 반환한다.
+
파일 스토리지 무결성 검사
앱은 지정된 키-값 쌍 또는 장치에 저장된 파일(예: 키체인, / 또는 데이터베이스)에 대해 HMAC 또는 서명을 생성하여 애플리케이션 저장소 자체의 무결성을 보장하도록 선택할 수 UserDefaults있다
예를 들어 앱에는 HMAC를 생성하는 다음 코드가 포함될 수 있다
// Allocate a buffer to hold the digest and perform the digest.
NSMutableData* actualData = [getData];
//get the key from the keychain
NSData* key = [getKey];
NSMutableData* digestBuffer = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, [actualData bytes], (CC_LONG)[key length], [actualData bytes], (CC_LONG)[actualData length], [digestBuffer mutableBytes]);
[actualData appendData: digestBuffer];
이 스크립트는 다음 단계를 수행한다.
- 데이터를 NSMutableData.
- 데이터 키를 가져옵니다(일반적으로 키체인에서).
- 해시 값을 계산합니다.
- 실제 데이터에 해시 값을 추가한다.
- 4단계의 결과를 저장한다.
그런 다음 다음을 수행하여 HMAC를 확인할 수 있다.
NSData* hmac = [data subdataWithRange:NSMakeRange(data.length - CC_SHA256_DIGEST_LENGTH, CC_SHA256_DIGEST_LENGTH)];
NSData* actualData = [data subdataWithRange:NSMakeRange(0, (data.length - hmac.length))];
NSMutableData* digestBuffer = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, [actualData bytes], (CC_LONG)[key length], [actualData bytes], (CC_LONG)[actualData length], [digestBuffer mutableBytes]);
return [hmac isEqual: digestBuffer];
- 메시지와 hmacbytes를 별도로 추출한다
- 에서 HMAC를 생성하는 절차의 1-3단계를 반복한다
- 추출된 HMAC 바이트를 1단계의 결과와 비교한다.
참고: 앱이 파일도 암호화하는 경우 암호화한 다음 인증된 암호화 에 설명된 대로 HMAC를 계산해야 한다 .
우회방법:
- " 장치 바인딩 " 섹션 에 설명된 대로 장치에서 데이터를 검색한다 .
- 검색된 데이터를 변경하고 스토리지로 반환한다.
+
DCAppAttestService는 애플이 제공하는 프레임워크 중 하나로, 앱이 서버로 데이터를 보낼 때 해당 데이터의 무결성을 보장하고 신뢰성을 검증하기 위해 사용될 수 있는 기술이다. 이 서비스는 디바이스와 서버 간의 통신에서 중간자 공격 등을 방지하고 데이터 변조를 감지할 수 있는 강력한 메커니즘을 제공한다.
특히, DCAppAttestService는 앱이 신뢰할 수 있는 디바이스로부터 생성된 애플리케이션 인증 토큰을 획득할 수 있도록 돕는 역할을 한다. 이 토큰은 앱과 디바이스 간의 신뢰성을 확인하기 위한 서명된 데이터로 사용될 수 있다. 이 토큰은 앱 내에서 생성되며, 이 토큰을 사용하여 서버로 전송되는 데이터의 무결성을 검증할 수 있다.
