iOS 코드 서명에 대해서

개요

안녕하세요. LINE에서 AIR ARMOR 개발을 담당하고 있는 심민영입니다. AIR ARMOR는 LINE GAME PLATFORM 중 하나인 AIR를 구성하는 보안 솔루션 중 하나입니다.

이전 포스팅, ‘AIR GO에 안드로이드 9 APK 서명 scheme v3 적용하기‘에서는 승훈 님이 Android의 서명 구조인 APK Signing에 대해 설명드렸는데요. 저는 이번 포스팅에서 iOS의 보안 구조 중 하나인 ‘코드 서명’에 대해서 설명하려고 합니다. 코드 서명은 파일의 무결성을 검증하고 서명자(개발자)를 확인하는 역할을 합니다. Mach-O 형식의 iOS 바이너리 파일의 무결성을 검증하고 서명자를 확인하는 작업엔 뒤에서 설명할 ‘code signature 구조체’를 이용합니다. 그럼 우선 iOS 코드 서명이 무엇인지부터 먼저 말씀드리겠습니다.

iOS 코드 서명

Apple 앱스토어에서 설치한 앱 혹은 테스트하기 위해 별도로 빌드한 앱은 코드 서명을 해야 iOS 기기에서 실행할 수 있습니다. 또한 코드 서명은 반드시 Apple에서 발급한 인증서로 진행되어야 합니다. 

앱스토어에서 설치한 앱은 아래와 같이 Apple의 인증서로 코드 서명되어 있습니다.

그림1. 앱스토어에서 설치한 앱을 서명한 인증서

개발자가 테스트하거나 배포하는 앱은 Apple이 발급한 개발자 인증서로 코드 서명합니다.

그림2. development 앱을 서명한 개발자 인증서
그림3. Ad Hoc 배포 앱을 서명한 개발자 인증서

Apple의 인증서가 아닌 Apple이 발급한 ‘개발자 인증서’로 코드 서명한 앱을 기기에 설치할 때는 ‘프로비저닝 프로파일(provisioning profile)’이 반드시 필요합니다. 프로비저닝 프로파일에 명시된 기기에 프로비저닝 프로파일을 설치해야 Apple의 인증서로 코드 서명된 앱이 아니더라도 기기에서 실행할 수 있기 때문입니다.

프로비저닝 프로파일

프로비저닝 프로파일은 기기에서 앱을 실행하고 특정 서비스를 사용하고자 할 때 사용되는 파일입니다. 프로비저닝 프로파일은 Xcode에서 자동 생성하거나 ‘Apple Developer Program‘에서 생성할 수 있습니다(저는 Xcode에서 자동생성하는 것을 추천합니다). 

아래 화면은 샘플 앱을 Ad Hoc으로 배포할 때 나타나는 창입니다.

그림4. Ad Hoc으로 배포하기 전 나타나는 Xcode의 Re-sign 창

위 화면에서 Automatically manage signing을 선택하면 프로비저닝 프로파일을 자동으로 생성하여 빌드합니다. 자동 생성된 프로파일은 ~/Library/MobileDevice/Provisioning Profiles 경로에서 확인할 수 있습니다.

$ ls ~/Library/MobileDevice/Provisioning\ Profiles
1axxxx49-xxxx-xxxx-xxxx-c2xxxxxxxxff.mobileprovision 
a8xxxxec-xxxx-xxxx-xxxx-5cxxxxxxxx22.mobileprovision

Manually manage signing을 선택하면 아래와 같이 수동으로 생성한 프로파일을 직접 선택해야 합니다. 이때 자동 생성한 프로비저닝 프로파일을 수정하여 수동 적용하는 것은 불가능합니다.

그림5. 그림4에서 Manually manage signing을 선택했을 때 나타나는 창

프로비저닝 프로파일의 내용

프로비저닝 프로파일은 빌드된 앱(.ipa 파일) 내부에서 찾을 수 있습니다. .ipa 파일의 압축을 풀면 Payload 디렉터리 안에 embedded.mobileprovision란 이름의 파일이 있는데요(예: Payload/sample.app/embedded.mobileprovision). 이 파일이 프로비저닝 프로파일입니다. 아래 명령어로 내용을 확인할 수 있고 텍스트 에디터에서 바로 파일을 열어서 확인하는 것도 가능합니다.

$ security cms -D -i embedded.mobileprovision

프로비저닝 프로파일엔 인증서와 기기 목록, Entitlements 항목, 유효기간 등이 명시되어 있습니다. 만약 실행 환경이 프로비저닝 프로파일에 명시된 자격 조건과 맞지 않는다면 앱을 실행할 수 없습니다.

인증서

프로비저닝 프로파일 내 DeveloperCertificates 항목을 보면 base64로 인코딩되어 있는 문자열이 보입니다.

그림6. 프로비저닝 프로파일 내의 DeveloperCertificates 항목

openssl 명령어가 base64로 인코딩된 데이터를 제대로 해석하지 못하기 때문에 openssl 명령어가 해석할 수 있는 PEM(Privacy-Enhanced Mail) 파일 포맷에 맞추기 위해 해당 문자열을 복사하여 상단에 -----BEGIN CERTIFICATE-----를 추가하고 하단에 -----END CERTIFICATE-----를 추가하여 파일로 저장합니다(저는 ‘sample.txt’란 이름으로 저장했습니다).

openssl 명령어를 이용하면 아래와 같이 파일로 저장된 인증서의 정보를 확인할 수 있습니다. 

$ openssl x509 -in sample.txt -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: XXXXXX... (XXXXXX... )
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = US, O = Apple Inc., OU = Apple Worldwide Developer Relations, CN = Apple Worldwide Developer Relations Certification Authority
        Validity
            Not Before: Oct 26 09:45:00 2018 GMT
            Not After : Oct 26 09:45:00 2019 GMT
        Subject: UID = XXXXXXXX, CN = iPhone Developer: XXXXXX (XXXXXXXX), OU = XXXXXXXX, O = XXXXXX, C = US
        Subject Public Key Info:
.
.
.

Entitlements

인증서 정보를 확인한 후 바로 아래를 보시면 Entitlements 항목이 존재합니다.

그림7. 프로비저닝 프로파일 내의 Entitlements 항목

Entitlements 항목에는 어떤 서비스를 사용할 수 있는지에 대한 자격 증명이 명시되어 있습니다(위 프로비저닝 프로파일 샘플은 디버그용 빌드이기 때문에 프로세스 디버깅이 가능하도록 get-task-allow 항목이 true로 설정되어 있습니다).

위 항목 내 배열(array)은 Xcode 빌드 설정의 Capabilities 탭에서 무엇을 선택했느냐에 따라 달라집니다. 아래 그림과 같이 Access WiFi Information 기능과 Apple Pay 기능을 추가해 보겠습니다.

그림8. Xcode 빌드 설정의 Capabilities 탭

아래처럼 추가한 기능에 맞게 key와 value가 Entitlements 항목에 추가됩니다.

그림9. 그림8과 같이 설정하였을 때 추가된 Entitlements 항목

유효 기간

프로비저닝 프로파일의 유효기간은 ExpirationDate 항목에서 확인할 수 있습니다. 이 항목에 명시된 유효기간이 지나면 앱을 실행할 수 없습니다.

그림10. 프로비저닝 프로파일 내의 ExpirationDate 항목

기기 목록

실행 가능한 기기 목록은 ProvisionedDevices 항목에서 확인할 수 있는데요. 앱은 이 목록에 있는 기기에서만 실행할 수 있습니다.

그림11. 프로비저닝 프로파일 내의 ProvisionedDevices 항목

Ad Hoc 등 모든 기기에서 실행 가능한 앱의 프로비저닝 프로파일에는 아래와 같이 ProvisionsAllDevices 항목이 true로 설정되어 있습니다.

그림12. Ad Hoc으로 빌드한 앱의 프로비저닝 프로파일 내의 ProvisionsAllDevices 항목

바이너리 파일의 무결성 검증

서명된 앱의 Mach-O 바이너리 파일에는 코드 서명과 관련된 구조체가 있습니다. 이 구조체를 이용하여 Mach-O 바이너리 파일의 무결성을 검증할 수 있습니다.

Mach-O 바이너리 파일

먼저 Mach-O 바이너리 파일에 대해서 간략하게 설명하고 넘어가겠습니다. iOS에서 동작하는 앱(.ipa 파일)의 내부에는 Mach-O 바이너리 파일이 있는데요. Mach-O는 iOS와 macOS 계열에서 사용하는 실행 파일 포맷입니다. iOS는 이 Mach-O 바이너리 파일을 실행하여 앱을 구동합니다. 

$ file sample
sample: Mach-O 64-bit executable x86_64
$ ./sample
sample!

Mach-O 바이너리 파일 얻기

ipa 파일의 압축을 풀면 Payload 디렉터리 안에 Info.plist라는 파일이 있습니다(예: Payload/sample.app/Info.plist). 이 파일을 열어보면 Executable file이란 항목이 보이는데요. Executable file은 앱이 실행될 때 가장 먼저 실행되는 Mach-O 바이너리 파일을 의미합니다.

그림13. Info.plist 파일

이를 통해 Payload/sample.app/iOSSample-mobile 파일이 앱이 실행될 때 가장 먼저 실행되는 Mach-O 바이너리 파일이라는 것을 알 수 있습니다.

 아래는 샘플 Mach-O 바이너리 파일의 구조를 간략하게 나타낸 그림입니다. 

그림14. Mach-O 바이너리 파일의 구조

machHeader

machHeader의 Magic 값을 확인하면 해당 파일이 Mach-O인지 아닌지 식별할 수 있습니다.

그림15. machHeader 구조체

구조체의 내용을 아래 표로 정리해 봤습니다. 

항목설명샘플참고 URL
magicMach-O 파일인지 식별하며 byte order도 결정합니다.
0xfeedface: 32비트 크기의 Mach-O 파일이며 byte order는 host pc의 endian을 따라갑니다.
<mach-o/loader.h>
cpu_type
cpu 유형을 나타냅니다.
0xc: arm
<mach/machine.h>
cpu_subtype
cpu 하위 유형을 나타냅니다.
0x9: arm v7
<mach/machine.h>
file_type
파일 종류를 나타냅니다.
0x2: 실행 파일 형식입니다.
<mach-o/loader.h>
num_load_commands
load_command의 개수를 나타냅니다.
34: 총 34개의 load command를 가지고 있습니다.
size_of_load_commandsload_command의 전체 크기를 바이트 단위로 나타냅니다.
3792: 34개의 load command를 모두 합한 크기가 3792바이트 입니다.
flagsMach-O 파일 포맷의 부가 기능들 중 어떤 것을 사용하는지 나타냅니다.
<mach-o/loader.h>

표1. machHeader 구조체의 상세 내용

LoadCommand

LoadCommand는 machHeader 뒤에 위치하고 있습니다. machHeader를 파싱하여 Mach-O 파일로 판명되면 LoadCommand 부분을 파싱합니다.

그림16. LoadCommand 구조체 리스트

LoadCommand에는 SEGMENT들의 위치, 동적 라이브러리의 이름, 심볼 테이블 위치 등 많은 정보들이 담겨 있습니다. 이번 포스팅에선 그중 코드 서명과 관련된 LC_CODE_SIGNATURE(CODE_SIGNATURELoadCommand)만 확인해 보겠습니다.

LC_CODE_SIGNATURE

Mach-O의 코드 서명 정보와 관련된 LoadCommand입니다. 

그림17. CODE_SIGNATURE의 LoadCommand

otool 명령어를 사용해서 LC_CODE_SIGNATURE에 대한 정보를 얻을 수 있습니다. 

$ otool -l Payload/iOSSample-mobile.app/iOSSample-mobile | grep LC_CODE_SIGNATURE -A3
      cmd LC_CODE_SIGNATURE
  cmdsize 16
  dataoff 3188480
 datasize 59808

dataoff는 코드 서명 정보와 관련된 구조체인 CodeSignature의 offset을 의미합니다. 이 정보를 하나씩 따라가면 코드 서명 정보를 전부 찾을 수 있습니다. 이번 포스팅에서 직접 찾는 방법은 생략하겠습니다.

아래는 CodeSignature의 대략적인 구조입니다.

그림18. CodeSignature 구조체

CodeDirectory

위에서 찾은 CodeSignature 구조체에서 CodeDirectory 항목을 찾을 수 있습니다. CodeDirectory는 특정 파일과 실행 바이너리 파일 조각들의 해시값들이 담겨져 있는 부분입니다. 샘플 앱 바이너리 파일의 CodeDirectory를 확인해 보았습니다. 

그림19. CodeDirectory 구조체

구조체 목록 하단에서 HashSlot 유형의 codeHash와 specialHash를 확인할 수 있습니다.

codeHash

codeHash는 바이너리 파일을 pageSize(0x1000)만큼 나눈 부분들에 대한 해시값을 나타냅니다. 다음 그림은 샘플 앱의 codeHash에 대해 간략히 나타낸 것입니다.

그림 20. codeHash

만약 코드를 수정하면 수정한 코드가 포함된 페이지의 해시값이 달라집니다. 이 해시값은 codeHash에 저장되어 있는 해시값과는 다르기 때문에 iOS는 코드가 수정된 사실을 알 수 있습니다. 

specialHash

이번에는 specialHash를 살펴보겠습니다. specialHash를 간단하게 정리하면 아래 표와 같습니다.

IndexContainsHash (sample)
0Entitlement (bound in code signature)4b255acb014ab5dc8cd63f5120baeef19309e340
1Application Specific (largely unused)0000000000000000000000000000000000000000
2Resource Directory (_CodeResources)35b65bb61f6b617e9a944cdada31cb78b47ab393
3Internal requirementscdbf07382a5a26998e34dc9d80070fc5db8c9230
4Bound Info.plist (Manifest)5108d83c00eb7f294fb73914abdb0a1c977b92f2

표2. specialHash 설명과 예제

각 영역이 어디를 가리키는지 확인해 보겠습니다. 

Entitlement

CodeDirectory는 바이너리 파일 내에 있는 Entitlement 부분의 해시값을 가지고 있습니다. Entitlement는 아래 그림과 같이 CodeSignature 구조체 안에서 찾을 수 있습니다.

그림21. Entitlement 구조체

Entitlement 항목을 전부 복사하여 따로 파일에 저장합니다(magiclengthdata 전부 포함합니다).

그림22. Entitlement 항목을 텍스트 파일로 저장

shasum 명령어를 사용하여 파일의 sha-1 해시값을 확인할 수 있습니다.

$ shasum entitlement.txt
4b255acb014ab5dc8cd63f5120baeef19309e340  entitlement.txt

‘표2. specialHash 설명과 예제’에서 specialHash[0]의 값과 동일합니다.

Application Specific

Apple Mach-O 코드 서명에 관련된 코드를 보면 Application Specific은 아직 사용되고 있지 않습니다.

Resource Directory

ipa 내 _CodeSignature/CodeResources 파일을 가리킵니다(예: Payload/sample.app/_CodeSignature/CodeResources).

그림23. Payload/sample.app/_CodeSignature/CodeResources 파일 내용

CodeResources 파일은 앱 내에 존재하는 리소스 파일들에 대한 체크섬이 나열되어 있습니다. shasum 명령어로 파일의 해시값을 확인해보겠습니다.

$ shasum Payload/iOSSample-mobile.app/_CodeSignature/CodeResources
35b65bb61f6b617e9a944cdada31cb78b47ab393  Payload/iOSSample-mobile.app/_CodeSignature/CodeResources

‘표2. specialHash 설명과 예제’에서 specialHash[2]의 값과 동일합니다.

Internal requirements

파일 내에 존재하는 Requirements 부분을 가리킵니다. Requirement는 코드 서명을 검증하기 위한 규칙을 나타냅니다. 규칙은 여러 개가 존재할 수 있습니다. Requirement의 개수는 Requirements 구조체 내에 명시되어 있습니다. Requirements에 관한 정보는 codesign 명령어를 사용하여 확인할 수 있습니다.

$ codesign --display -r- Payload/iOSSample-mobile.app/iOSSample-mobile
Executable=Payload/iOSSample-mobile.app/iOSSample-mobile
designated => identifier "armor.sdk.sample.cocos2dx.ios" and anchor apple generic and certificate leaf[subject.CN] = "iPhone Distribution: XXXXXXX (XXXXXXX)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */

체크섬을 확인하기 위해서 Requirements와 Requirement 항목을 전부 복사하여 파일로 저장합니다.

그림24. requirementsrequirement 구조체

shasum 명령어로 파일의 해시값을 확인해보겠습니다.

$ shasum requirements.txt
cdbf07382a5a26998e34dc9d80070fc5db8c9230  requirements.txt

‘표2. specialHash 설명과 예제’에서 specialHash[3]의 값과 동일합니다.

Bound Info.plist

Bound Info.plist는 .app 디렉터리 내 Info.plist 파일을 가리킵니다. Info.plist는 앱 이름, 앱 버전, 앱 아이콘 파일 경로 등 애플리케이션에 대한 기본 정보들을 포함하고 있습니다. shasum 명령어로 파일의 해시값을 확인해보겠습니다.

$ shasum Payload/iOSSample-mobile.app/Info.plist
5108d83c00eb7f294fb73914abdb0a1c977b92f2  Payload/iOSSample-mobile.app/Info.plist

‘표2.specialHash 설명과 예제’에서 specialHash[4]의 값과 동일합니다.

위 내용을 통해 코드 외 다른 정보들의 무결성도 검증한다는 것을 알 수 있습니다.

BlobWrapper

BlobWrapper 안에는 CMS(Cryptographic Message Syntax) 서명이 있습니다. BlobWrapper 정보는 jtool 명령어를 사용하여 볼 수 있습니다.

$ jtool --sig -v Payload/iOSSample-mobile.app/iOSSample-mobile
    ...
    Blob 4: Type: 10000 @41788: Blob Wrapper (4802 bytes) (0x10000 is CMS (RFC3852) signature)

BlobWrapper은 앞서 기술한 CodeDirectory를 서명한 데이터와 데이터를 서명한 인증서를 포함하고 있습니다. 인증서의 정보를 확인하기 위해 바이너리 파일에서 BlobWrapper의 data 부분만 추출하여 파일로 저장합니다.

그림25. BlobWrapper 구조체

이 파일은 openssl 명령어를 사용하여 대략적인 내용을 확인할 수 있습니다. 

$ openssl pkcs7 -inform der -in blobwrapper.txt -print -noout
PKCS7:
  type: pkcs7-signedData (1.2.840.113549.1.7.2)
  d.sign:
    version: 1
    md_algs:
        algorithm: sha256 (2.16.840.1.101.3.4.2.1)
        parameter: NULL
    contents:
      type: pkcs7-data (1.2.840.113549.1.7.1)
      d.data: <ABSENT>
    cert:
        cert_info:
          version: 2
          serialNumber: 134752589830791184
          signature:
            algorithm: sha1WithRSAEncryption (1.2.840.113549.1.1.5)
            parameter: NULL
          issuer: C=US, O=Apple Inc., OU=Apple Certification Authority, CN=Apple Root CA
          validity:
            notBefore: Feb  7 21:48:47 2013 GMT
            notAfter: Feb  7 21:48:47 2023 GMT
          subject: C=US, O=Apple Inc., OU=Apple Worldwide Developer Relations, CN=Apple Worldwide Developer Relations Certification Authority
          key:
            algor:
              algorithm: rsaEncryption (1.2.840.113549.1.1.1)
              parameter: NULL
            public_key:  (0 unused bits)
              0000 - 30 82 01 0a 02 82 01 01-00 ca 38 54 a6 cb   0.........8T..
.
.
.

CodeDirectory의 내용을 변경하면 동일한 인증서로 다시 서명해도 CMS 서명이 달라집니다. 따라서 공격자가 코드를 변경한 후 CodeDirectory의 내용을 변경된 코드에 맞춰 수정한다고 해도 CMS 데이터는 키를 가지고 있는 서명자 이외에는 변경할 수 없기 때문에 CMS 서명을 검증하면 앱의 무결성을 보장할 수 있습니다.

정리

그림26. iOS 무결성 검증

iOS는 바이너리 파일과 관련 파일들의 해시값을 확인하는 것에서 끝나지 않고, 코드 서명을 검증하여 앱의 무결성까지 확인합니다. iOS는 이런 과정을 통해 변조된 앱이 임의의 사용자 기기에서 실행되는 것을 미연에 방지합니다. AIR ARMOR에서는 이번 포스팅에서 설명드린 Mach-O 실행 파일의 코드 서명 원리를 이용한 보안 기능 외의 더 많은 보안 기능들을 제공하고 있으니 한번 확인해 보시기 바랍니다. 긴 글 읽어주셔서 감사합니다.

Related Post