JJANG-JOON
article thumbnail
반응형

IOS 앱 무결성 검증 

iOS 앱 무결성 검증은 iOS 운영체제를 실행하는 디바이스에서 애플리케이션의 무결성을 확인하고 변조되지 않았음을 보장하는 과정이다.

이를 통해 앱의 코드와 데이터가 보안 위협으로부터 안전하게 보호되며, 사용자의 신뢰와 개인정보 보호를 강화한다.

iOS 앱 무결성 검증은 주로 코드 서명, 앱 스토어 연결, 앱 카탈로그 파일 등의 메커니즘을 활용하여 수행된다.

 

iOS 앱 무결성 검증의 핵심 원리는 코드와 데이터의 변조를 방지하고, 신뢰할 수 있는 출처를 확인하여 사용자의 보안을 보장하는 것이고 이를 위해 아래와 같은 메커니즘이 사용된다:

  1. 코드 서명 (Code Signing): 앱의 코드와 데이터는 디지털 서명으로 보호됩니다. 애플 또는 앱 개발자로부터 발급된 서명은 코드의 무결성을 보장하며, 변조된 앱을 탐지할 수 있습니다.
  2. 앱 스토어 검증: 앱은 앱 스토어를 통해 배포되기 전에 애플의 검증을 거칩니다. 앱 스토어는 앱의 무결성과 보안을 검증하여 사용자의 신뢰를 유지합니다.
  3. 앱 스토어 연결 (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 섹션의 데이터를 해시하여 서명을 생성하고 원래 서명과 비교하는 작업을 수행한다.

 

  1. int xyz(char *dst): 함수의 시작을 정의하며, dst는 서명 값을 저장하기 위한 문자열 포인터이다. 함수의 반환 값은 검증 결과를 나타낸다.
  2. const struct mach_header * header;: mach_header 구조체에 대한 포인터로, 현재 앱의 Mach-O 헤더 정보를 저장한다.
  3. Dl_info dlinfo;: Dl_info 구조체에 대한 변수로, dladdr 함수로부터 반환되는 정보를 저장한다.
  4. if (dladdr(xyz, &dlinfo) == 0 || dlinfo.dli_fbase == NULL) { ... }: dladdr 함수를 사용하여 현재 함수 xyz의 정보를 가져온다. 만약 정보를 가져오지 못하거나 기본 주소가 없다면 에러를 출력하고 프로그램을 종료한다.
  5. while (1) { ... }: 무한 루프를 시작하여 Mach-O 헤더에서 세그먼트와 섹션을 찾고 서명을 생성하는 작업을 반복한다.
  6. header = dlinfo.dli_fbase;: 현재 함수의 기본 주소에서 Mach-O 헤더를 가져와 header에 저장한다.
  7. struct load_command * cmd = (struct load_command *)(header + 1);: 헤더의 다음 위치에 있는 첫 번째 로드 커맨드를 가져와 cmd에 저장한다.
  8. for (uint32_t i = 0; cmd != NULL && i < header->ncmds; i++) { ... }: 로드 커맨드를 순회하며 __TEXT 세그먼트를 찾는 작업을 수행헌다.
  9. if (cmd->cmd == LC_SEGMENT) { ... }: 로드 커맨드의 타입이 LC_SEGMENT인 경우 세그먼트 정보를 가져온다.
  10. struct segment_command * segment = (struct segment_command *)cmd;: 세그먼트 정보를 가져와 segment에 저장한다.
  11. if (!strcmp(segment->segname, "__TEXT")) { ... }: 세그먼트 이름이 __TEXT인 경우 섹션 정보를 찾는다.
  12. struct section * section = (struct section *)(segment + 1);: 섹션 정보를 가져와 section에 저장한다.
  13. for (uint32_t j = 0; section != NULL && j < segment->nsects; j++) { ... }: 섹션을 순회하며 __text 섹션을 찾는다.
  14. if (!strcmp(section->sectname, "__text")) break;: 섹션이 __text 섹션이면 반복을 멈춘다.
  15. 서명을 생성하기 위해 __text 섹션의 데이터를 해시합니다. 해시 알고리즘으로 MD5를 사용하고, 생성된 해시 값을 dst에 저장한다.
  16. return 0;: 함수의 무결성 검증이 완료되었으므로 0을 반환한다.

해당 코드는 Mach-O 헤더 및 로드 커맨드를 분석하여 __TEXT 섹션의 데이터를 해시하여 서명을 생성하고, 이 서명을 기존 서명과 비교하여 앱의 무결성을 검증하는 메커니즘을 나타내고 있다.

 

우회방법:

 

  1. 안티 디버깅 기능을 패치하고 관련 코드를 NOP 명령으로 덮어써 원치 않는 동작을 비활성화한다.
  2. 코드의 무결성을 평가하는 데 사용되는 저장된 해시를 패치한다.
  3. 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];

이 스크립트는 다음 단계를 수행한다.

  1. 데이터를 NSMutableData.
  2. 데이터 키를 가져옵니다(일반적으로 키체인에서).
  3. 해시 값을 계산합니다.
  4. 실제 데이터에 해시 값을 추가한다.
  5. 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];

 

  1. 메시지와 hmacbytes를 별도로 추출한다 
  2. 에서 HMAC를 생성하는 절차의 1-3단계를 반복한다 
  3. 추출된 HMAC 바이트를 1단계의 결과와 비교한다.

참고: 앱이 파일도 암호화하는 경우 암호화한 다음 인증된 암호화 에 설명된 대로 HMAC를 계산해야 한다 .

 

우회방법:

 

  1. " 장치 바인딩 " 섹션 에 설명된 대로 장치에서 데이터를 검색한다 .
  2. 검색된 데이터를 변경하고 스토리지로 반환한다.

+

DCAppAttestService는 애플이 제공하는 프레임워크 중 하나로, 앱이 서버로 데이터를 보낼 때 해당 데이터의 무결성을 보장하고 신뢰성을 검증하기 위해 사용될 수 있는 기술이다. 이 서비스는 디바이스와 서버 간의 통신에서 중간자 공격 등을 방지하고 데이터 변조를 감지할 수 있는 강력한 메커니즘을 제공한다.

 

특히, DCAppAttestService는 앱이 신뢰할 수 있는 디바이스로부터 생성된 애플리케이션 인증 토큰을 획득할 수 있도록 돕는 역할을 한다. 이 토큰은 앱과 디바이스 간의 신뢰성을 확인하기 위한 서명된 데이터로 사용될 수 있다. 이 토큰은 앱 내에서 생성되며, 이 토큰을 사용하여 서버로 전송되는 데이터의 무결성을 검증할 수 있다.

 

 

반응형
profile

JJANG-JOON

@JJANG-JOON

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...