iCloud 데이터 백업

PHASE 1. 사용자 인증

Pkt 1~5 과정이며, 사용자 인증(ID/PW, 2FA 코드)이 필요한 단계이다.
완료하게 되면 mme_auth_token, dsprsid 값을 얻을 수 있고 이걸 세션파일에 기록해두고 인증 없이 PHASE 2 부터 실행할 수 있다.

==============================================
 Phase 1: auth (사용자 개입 필요)
==============================================

[Pkt 1] SRP Init
  POST gsa.apple.com/grandslam/GsService2
  IN:  Apple ID, Password, Anisette
  OUT: salt, B, challenge
       |
       v
[Pkt 2] SRP Complete
  POST gsa.apple.com/grandslam/GsService2
  IN:  M1, challenge (Pkt1)
  OUT: dsid, idms_token
       |
       v
[Pkt 3] 2FA Trigger
  GET gsa.apple.com/auth/verify/trusteddevice
  IN:  base64(dsid:idms_token) (Pkt2)
  OUT: (기기 푸시 알림)
       |
       v
[Pkt 4] 2FA Validate
  GET gsa.apple.com/grandslam/GsService2/validate
  IN:  2FA code, Identity Token (Pkt2)
  OUT: pe_token
       |
       v
[Pkt 5] Setup Auth
  GET setup.icloud.com/setup/authenticate/$APPLE_ID$
  IN:  pe_auth (pe_token에서 접두사 제거)
  OUT: mme_auth_token, dsprsid -> auth_state.json

Anisette OTP

OTP는 대부분의 패킷에서 전달되는 값이며, X-Apple-I-MD, X-Apple-I-MDM 형태로 패킷헤더나 바디에 포함되어 전달된다.

이 값은 원래 Apple 기기의 비공개 라이브러리에서만 만들어낼 수 있는 값이며, Apple 서버에서 Apple 기기가 맞는지 검증하는 용도로 사용된다.

anisette server를 구현해서 만들어도되고, windows용으로 만들어진 dll 프로젝트를 빌드해도 된다.

git clone https://github.com/Dadoum/anisette-v3-server.git
cd anisette-v3-server
docker stop anisette-v3
docker rm anisette-v3
docker volume rm anisette-v3_data
# 이 사이에 코드 수정
docker build -t anisette-v3-server-ios .
docker run -d --restart always --name anisette-v3 -p 6969:6969 --volume anisette-v3_data:/home/Alcoholic/.config/anisette-v3/lib/ anisette-v3-server-ios

iOS로 프로비저닝 하기 위해서는 코드를 수정해줘야 한다.

diff --git a/Dockerfile b/Dockerfile
-RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates curl \
+RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates curl libplist-2.0-4 \

diff --git a/source/app.d b/source/app.d
-enum clientInfo = "<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>";
+enum clientInfo = "<iPhone14,5> <iPhone OS;26.3;23D127> <com.apple.AuthKit/1 (com.apple.akd/1.0)>";

-                       "user_agent": "akd/1.0 CFNetwork/808.1.4"
+                       "user_agent": "akd/1.0 CFNetwork/1568.100.1"

localhost:6969로 접근할 수 있으며, 접근하게되면 JSON으로 OTP를 리턴해준다.

ad4033fd-cb54-4b10-b3c0-bec764c7ed4b
ad4033fd-cb54-4b10-b3c0-bec764c7ed4b

이 anisette server의 원리는 특정 기기인 척(기본값은 맥북으로 프로비저닝) 하면서 X-Apple-I-MD, X-Apple-I-MDM 값을 기기 정보에 맞게 만들어내기 때문에 이외의 필드(X-MMe-Client-Info, X-Mme-Device-Id 등)도 다 전달받은 값으로 지정해줘야 애플에서 정상적으로 인증을 시켜준다.

만약 macbook 프로비저닝 값을 사용하고 있다면, SRP 이후부터는 X-MMe-Client-Info 값을 iOS의 기기 정보로 변경해야한다.

# config.py
DEVICE_MODEL = 'iPhone14,5'          # iPhone 13
DEVICE_BOARD = 'D17AP'               # iPhone 13 board ID
IOS_VERSION = '26.3'
IOS_BUILD = '23D127'
CLOUDKIT_VERSION = '2340'            # CloudKitDaemon CFBundleVersion
CFNETWORK_VERSION = '1568.100.1'     # CFNetwork for iOS 26.x

인증 과정

Pkt 1. SRP Init

SRP (Secure Remote Password) 프로토콜의 시작으로 비밀번호를 네트워크에 노출시키지 않고도 서버에 전달할 수 있는 키교환 프로토콜이다. 파이썬에서 표준 srp 라이브러리를 임포트해서 쓸 수 있다.

간단하게 설명하면 DH는 완전히 아무것도 없는 상태의 키 교환인데, SRP는 양쪽이 비밀번호를 알고있기 때문에 비밀번호 파생값까지 포함시켜 중간자 공격으로도 확인 불가능하고 서로만 확인 가능한 방식이다. 정확히말하면 교환이라기보다는 나도 비밀번호를 알고있다를 알려주는것.

나머지 값들은 anisette 서버에서 가져온 값들과 로그인할 계정 email 이다.

  • Request

    POST https://gsa.apple.com/grandslam/GsService2
    
    Content-Type: text/x-xml-plist
    User-Agent: CloudKit/2340 (23D127)
    X-MMe-Client-Info: <iPhone14,5> <iPhone OS;26.3;23D127> <com.apple.AuthKit/1 (com.apple.akd/1.0)>
    
    <dict>
        <key>Header</key>
        <dict><key>Version</key><string>1.0.1</string></dict>
        <key>Request</key>
        <dict>
            <key>A2k</key>
            <data>[SRP ephemeral public key]</data>
            <key>cpd</key>
            <dict>
                <key>bootstrap</key><true/>
                <key>icscrec</key><true/>
                <key>prkgen</key><true/>
                <key>svct</key><string>iCloud</string>
                <key>X-Apple-I-MD</key><string>[Anisette OTP]</string>
                <key>X-Apple-I-MD-M</key><string>[Anisette OTP-M]</string>
                <key>X-Apple-I-Client-Time</key><string>2024-01-15T10:30:00Z</string>
                <key>X-Apple-Locale</key><string>en_US</string>
                <key>X-Mme-Device-Id</key><string>[Device UDID]</string>
            </dict>
            <key>o</key><string>init</string>
            <key>ps</key>
            <array><string>s2k</string><string>s2k_fo</string></array>
            <key>u</key><string>[login email]</string>
        </dict>
    </dict>
    
  • Response

    1359a88e-680c-405b-ae29-8dbf81b807f0
    1359a88e-680c-405b-ae29-8dbf81b807f0

응답 데이터에서 M1 값을 계산하고 다음패킷에 사용한다.

password_key = PBKDF2_HMAC_SHA256(password, salt, iterations, dkLen=32)
session_key = srp.User.process_challenge(salt, B, password_key)
M1 = srp.User.compute_M1()

Pkt 2. SRP Complete

클라이언트가 비밀번호를 알고 있음을 수학적으로 증명(M1값)하고, 서버가 검증하게되면 암호화된 세션 데이터(spd)를 반환한다.

  • Request
    POST https://gsa.apple.com/grandslam/GsService2
    
    <dict>
        <key>Request</key>
        <dict>
            <key>M1</key><data>[M1]</data>
            <key>c</key><string>[challenge from Pkt 1]</string>
            <key>cpd</key><dict><!-- same as Pkt 1 --></dict>
            <key>o</key><string>complete</string>
            <key>u</key><string><!-- same as Pkt 1 --></string>
        </dict>
    </dict>
    
  • Response
    <dict>
        <key>Response</key>
        <dict>
            <key>M2</key>     <data>[서버의 SRP 증명값]</data>
            <key>spd</key>    <data>[암호화된 세션 데이터 blob]</data>
            <key>Status</key> <dict>...</dict>
        </dict>
    </dict>
    

M2 값을 검증하고 복호화하면 plist 데이터를 확인할 수 있다.
이 데이터에서 획득한 dsid, idms_token 으로 이후 패킷에서 인증에 사용되는 identity_token 을 만들 수 있다.

spd_key = HMAC_SHA256(session_key, b"extra data key:")
spd_iv  = HMAC_SHA256(session_key, b"extra data iv:")[:16]
decrypted_spd = AES_CBC_decrypt(spd, spd_key, spd_iv)
#  <dict>
#      <key>adsid</key>        <string>003212345678</string>
#      <key>GsIdmsToken</key>  <string>AQAAAABk...긴토큰...</string>
#      <key>...</key>           <!-- 기타 세션 메타데이터 -->
#  </dict>
decrypted_plist = plistlib.loads(PKCS7_unpad(decrypted_spd))
dsid       = decrypted_plist['adsid']
idms_token = decrypted_plist['GsIdmsToken']

# 앞으로 사용될 인증 토큰
identity_token = base64.b64encode(f"{dsid}:{idms_token}".encode())

Pkt 3. 2FA Trigger

위에서 생성한 인증 토큰에 해당하는 기기에 2FA 푸시 알림을 보내달라는 요청이다.
200 OK 응답을 받으면 푸시 요청이 성공한 것이다.

  • Request
    GET https://gsa.apple.com/auth/verify/trusteddevice
    
    X-Apple-Identity-Token: [identity_token]
    X-Apple-I-MD: [Anisette OTP]
    X-Apple-I-MD-M: [Anisette OTP-M]
    

Pkt 4. 2FA Validate

기기에 푸시 알람으로 표시된 6자리 2FA를 security-code 헤더에 넣어서 서버로 검증요청한다.

  • Request

    GET https://gsa.apple.com/grandslam/GsService2/validate
    
    security-code: [123456] 
    X-Apple-Identity-Token: [identity_token]
    X-Apple-I-MD: [Anisette OTP]
    X-Apple-I-MD-M: [Anisette OTP-M]
    
  • Response
    헤더에서 X-Apple-PE-Token 을 얻어서 Pkt 5의 1회용 인증 토큰으로 사용된다. GS-Token 은 다른 서비스의 인증 토큰으로 iCloud 백업에서는 사용되지 않는다.

    X-Apple-GS-Token: [base64 토큰1], [base64 토큰2], ...
    X-Apple-PE-Token: [base64 인코딩된 PE 토큰]
    

Pkt 5. Setup Auth

앞에서는 GrandSlam(gsa.apple.com) 도메인에서 계정을 인증해서 획득한 PE-Token을 제출해서, iCloud(setup.icloud.com) 서비스용 토큰(mme_auth_token)으로 교환 후 앞으로의 세션 토큰으로 사용해야한다.

# 1. Pkt 4에서 받은 pe_token을 base64 디코딩
pe_auth = base64.b64decode(pe_token).decode()
# 결과: "com.apple.gs.idms.pet:EAAbAAAAB...긴문자열..."

# 2. 서비스 접두사 제거 → 실제 토큰값만 추출
pe_auth = pe_auth[len('com.apple.gs.idms.pet:'):]
# 결과: "EAAbAAAAB...긴문자열..."

# 3. HTTP Basic Auth 형식으로 조합
pe_auth_token = base64.b64encode(f"{user_email}:{pe_auth}".encode())
# 결과: base64("user@example.com:EAAbAAAAB...")
  • Request
    요청할땐 $APPLE_ID$ 이 문자열 그대로 들어간다.

    GET https://setup.icloud.com/setup/authenticate/$APPLE_ID$
    
    Authorization: Basic [pe_auth_token]
    
  • Response

    <dict>
        <key>appleAccountInfo</key>
        <dict>
            <key>dsid</key>       <string>19109699500</string>
            <key>dsPrsID</key>    <integer>19109699500</integer>
        </dict>
        <key>tokens</key>
        <dict>
            <key>mmeAuthToken</key>  <string>EAAJAAAABLwI...토큰...</string>
        </dict>
    </dict>
    

auth_state.json

Pkt 5 에서 얻은 dsprsid 값, mme_auth_token 값은 auth_state.json에 저장되고, 이후에는 모든 패킷에서 인증 토큰으로 Basic base64(f"{dsprsid}:{mme_auth_token}") 형태로 쓰인다.
dsid와 dsPrsID는 Apple 내부적으로 사용하는 계정 고유 ID 값이다.

PHASE 2. 백업 계정 정보 획득

Apple 서버와 통신해서 iCloud 서비스에 접근하기 위한 세션 정보(엔드포인트 URL, CloudKit 인증 토큰)를 확보하고, Escrow 서버에서 SRP 인증을 거쳐 백업 마스터 키(PCS Service 8)를 복구한다. 이 마스터 키가 이후 모든 복호화의 시작점이 된다.

==============================================
 Phase 2: setup
==============================================

[Pkt 6] Account Settings
  GET setup.icloud.com/setup/get_account_settings
  IN:  dsprsid, mme_auth_token (<- auth_state.json)
  OUT: cloudkit_auth_token, gateway_url, content_host, escrow_url

[Pkt 7] CloudKit Init
  POST setup.icloud.com/setup/ck/v1/ckAppInit
  IN:  dsprsid, mme_auth_token (<- auth_state.json)
  OUT: cloudkit_user_id

[Pkt 8a-8c] Escrow Recovery
  POST {escrow_url}/escrowproxy/api/get_records -> srp_init -> recover
  OUT: PCS Service Keys 1-11
       -> setup.json + keyset_dump.json

획득 과정

Pkt 6. Service Discovery

Apple ID 계정이 사용할 수 있는 모든 iCloud 서비스의 서버 주소를 가져온다.

  • Request

    GET https://setup.icloud.com/setup/get_account_settings
    
    Authorization: Basic [base64(dsprsid:mme_auth_token)]
    X-MMe-Client-Info: <iPhone14,5> <iPhone OS;26.3;23D127> <com.apple.AuthKit/1 (com.apple.akd/1.0)>
    X-Apple-I-MD: [Anisette OTP]
    X-Apple-I-MD-M: [Anisette OTP-M]
    
  • Response

    <dict>
        <key>appleAccountInfo</key>
        <dict>
            <key>dsPrsID</key>
            <string>16467082494</string>
            <key>appleId</key>
            <string>sheepst3@naver.com</string>
            ...
        </dict>
        <key>tokens</key>
        <dict>
            <key>cloudKitToken</key>
            <string>EAM0AAAABLwI...k0Q~~</string>
            <key>mmeAuthToken</key>
            <string>EAA0AAAABLwI...C4Bw==</string>
            ...
        </dict>
        <key>com.apple.mobileme</key>
        <dict>
            <key>com.apple.Dataclass.CKDatabaseService</key>
            <dict>
                <key>url</key>
                <string>https://p67-ckdatabase.icloud.com:443</string>
                <key>icloud_gateway_url</key>
                <string>https://gateway.icloud.com:443/ckdatabase</string>
                <key>partitionId</key>
                <integer>67</integer>
            </dict>
            <key>com.apple.Dataclass.Content</key>
            <dict>
                <key>url</key>
                <string>https://p67-content.icloud.com:443</string> 
                <key>icloud_gateway_url</key>
                <string>https://gateway.icloud.com:443/content</string>
            </dict>
            <key>com.apple.Dataclass.KeychainSync</key>
            <dict>
                <key>authMechanism</key>
                <string>token</string>
                <key>escrowProxyUrl</key>
                <string>https://p67-escrowproxy.icloud.com:443</string>
            </dict>
            ...
        </dict>
    </dict>
    

획득한 정보들과 앞으로 사용처

938469af-2505-4525-b3ca-5a534bfbc3b4
938469af-2505-4525-b3ca-5a534bfbc3b4

Pkt 7. CloudKit Init

com.apple.backup.ios CloudKit 컨테이너에서 현재 사용자의 CloudKit ID를 할당받는다. 이 ID는 Phase 3 이후 모든 CloudKit protobuf 요청에서 사용자를 식별하는 데 사용된다

  • Request

    POST https://setup.icloud.com/setup/ck/v1/ckAppInit?container=com.apple.backup.ios
    
    Authorization: Basic [base64(dsprsid:mme_auth_token)]
    X-MMe-Client-Info: <iPhone14,5> <iPhone OS;26.3;23D127> <com.apple.AuthKit/1 (com.apple.akd/1.0)>
    X-Apple-I-MD: [Anisette OTP]
    X-Apple-I-MD-M: [Anisette OTP-M]
    
  • Response

    {
      "cloudKitDatabaseUrl": "https://p24-ckdatabase.icloud.com:443",
      "cloudKitDeviceUrl": "https://p24-ckdevice.icloud.com:443",
      "cloudKitShareUrl": "https://p24-ckshare.icloud.com:443",
      "cloudKitMetricsUrl": "https://metrics.icloud.com:443",
      "cloudKitUserId": "_9a2947ac97eed427b22c0b2e7e1dd986",    ← ★ 이것만 사용
      "values": [
        {
          "name": "com.apple.backup.ios",
          "env": "sandbox",
          "url": "https://p24-ckdatabase.icloud.com:443/api/client",
          "ckDeviceUrl": "https://p24-ckdevice.icloud.com:443/api/client"
        },
        {
          "name": "com.apple.backup.ios",
          "env": "production",
          "url": "https://p24-ckdatabase.icloud.com:443/api/client",
          "ckDeviceUrl": "https://p24-ckdevice.icloud.com:443/api/client"
        }
      ],
      "cloudKitCodeGatewayUrl": "https://gateway.icloud.com:443/ckcoderouter",
      "cloudKitDeviceGatewayUrl": "https://gateway.icloud.com:443/ckdevice",
      "cloudKitCodeUrl": "https://p24-ckcoderouter.icloud.com:443",
      "cloudKitDatabaseGatewayUrl": "https://gateway.icloud.com:443/ckdatabase",
      "cloudKitShareGatewayUrl": "https://gateway.icloud.com:443/ckshare"
    }
    

응답값 중에서 cloudKitUserId 만 사용한다. 이 값은 Phase 3 이후 모든 CloudKit 요청 시 protobuf 필드의 zone owner로 들어간다.

Pkt 8a. Escrow 레코드 조회

protectedcloudstorage 레코드의 metadata 필드에서 Service 8 encrypted blob 을 획득하고, 복구가 가능한지 확인하기 위해 remainingAttempts 값을 조회하는 작업이다.

protectedcloudstorage 는 PCS 서비스 키가 보관된 레코드이고, 나머지 레코드들은 기기마다 생성되는 icdp 레코드인데 디바이스 잠금 비밀번호(passcode)의 에스크로 백업 레코드이다.

Service 8은 Apple의 PCS(Protected CloudKit Service)에서 백업 데이터를 복호화 할 때 사용되는 마스터키이다.

Service 1: iCloud Drive
Service 2: Photos
Service 3: CloudKit (일반)
Service 4: Backup (메타데이터)
Service 5: Escrow/Keybag 래핑 전용
Service 6-7: 예약
Service 8: Backup (데이터 복호화)
Service 9-11: 추가 보호 클래스
  • Request

    POST https://p67-escrowproxy.icloud.com:443/escrowproxy/api/get_records
    Authorization: X-MobileMe-AuthToken [base64(dsprsid:mme_auth_token)]
    Content-Type: application/x-apple-plist
    X-Apple-I-MD: [Anisette OTP]
    X-Apple-I-MD-M: [Anisette OTP-M]
    X-Apple-I-MD-RINFO: 17106176
    
    <dict>
        <key>command</key>
        <string>GETRECORDS</string>
        <key>label</key>
        <string>com.apple.protectedcloudstorage.record</string>
        <key>version</key>
        <integer>1</integer>
    </dict>  
    
  • Response

    <dict>
      <key>version</key>
      <integer>1</integer>
      <key>metadataList</key>
      <array>
        <!-- 레코드 1: protectedcloudstorage 이것만 사용함 -->
        <dict>
          <key>label</key>
          <string>com.apple.protectedcloudstorage.record</string>
          <key>remainingAttempts</key>
          <integer>10</integer>         <!-- 남은 시도 횟수. 0이면 영구 잠금! -->
          <key>metadata</key>
          <data>[base64 bplist]</data>  <!-- kPCSMetadataEscrowedKeys 포함 -->
        </dict>
        <!-- 레코드 2: icdp (디바이스별 - iPhone) -->
        <dict>
          <key>label</key>
          <string>com.apple.icdp.record.SHA256:15cyLdIWV4sE6GJy/KC5eGGRd2abQ22vyEFDBS357mA=</string>
          <key>metadata</key>
          <data>[base64 bplist]</data>
          <key>recordStatus</key>
          <string>valid</string>
          <key>remainingAttempts</key>
          <string>10</string>
        </dict>
        <!-- ... 기타 필드 생략 ... -->
      </array>
    </dict>
    

Pkt 8b. Escrow SRP Init

Phase 1 에서 봤던 SRP를 동일하게 사용하지만, Escrow 서버 대상으로 실행한다.
클라이언트 공개값 ephemeral_key_A 를 보내고, 서버 공개값 ephemeral_key_B + salt를 받는 단계이다.

  • Request

    POST https://p67-escrowproxy.icloud.com:443/escrowproxy/api/srp_init
    Authorization: X-MobileMe-AuthToken [base64(dsprsid:mme_auth_token)]
    
    <dict>
      <key>blob</key>
      <string>[base64(ephemeral_key_A), 256 bytes]</string>
      <key>command</key>
      <string>SRP_INIT</string>
      <key>label</key>
      <string>com.apple.protectedcloudstorage.record</string>
      <key>version</key>
      <integer>1</integer>
    </dict>
    
  • Response

    <dict>
      <key>dsid</key>
      <string>18931861436</string>
      <key>message</key>
      <string>Success</string>
      <key>respBlob</key>
      <data>[base64(BlobA4)]</data>
      <key>status</key>
      <string>0</string>
      <key>version</key>
      <integer>1</integer>
    </dict>
    

응답으로 받은 BlobA4를 파싱하고, dsid로 SRP를 설정해서 dsid를 알고있다는 것을 증명할 수 있는 M1 값을 생성한다.

blob = Blob.BlobA4(np.frombuffer(binascii.a2b_base64(respBlob), np.uint8))
# BlobA4 메모리 레이아웃:
# [0x00] Length(4B) Type=0xA4(4B) x(4B)      ← 헤더 12B
# [0x0C] tag(16B)                            ← 서버 세션 토큰
# [0x1C] padding(16B)                        ← zeros
# [0x2C] TLV List:                           ← 가변 길이 데이터
#        [0] uid          "5E96-F697-3089"   ← 서버 내부 세션 ID
#        [1] salt         16 bytes           ← SRP salt
#        [2] ephemeralKey 256 bytes          ← 서버 공개값 B

dsid = srpInitResponse["dsid"].encode("utf-8")   # b"18931861436"
srpUser.I = dsid    # username
srpUser.p = dsid    # password (= username 동일!)

m1 = srpUser.process_challenge(blob.salt, blob.ephemeralKey)

Pkt 8c. Escrow Recover

SRP proof(M1) 값을 서버로 보내서 인증을 완료하고, 서버가 보낸 M2를 검증한 뒤 SRP 세션키로 암호화된 PCS 키를 반환한다. 이때 M1 인증이 실패되면 remainingAttempts 값이 차감된다.

  • Request

    POST https://p67-escrowproxy.icloud.com:443/escrowproxy/api/recover
    Content-Type: application/x-www-form-urlencoded
    Authorization: X-MobileMe-AuthToken [base64(dsprsid:mme_auth_token)]
    
    <dict>
      <key>blob</key>
      <string>AAAAYAAAAKUAAAAAEQYWwvs...csZe</string>   <!-- base64(BlobA5) -->
      <key>command</key>
      <string>RECOVER</string>
      <key>label</key>
      <string>com.apple.protectedcloudstorage.record</string>
      <key>version</key>
      <integer>1</integer>
    </dict>
    
  • Response

    <dict>
      <key>message</key>
      <string>Success</string>
      <key>respBlob</key>
      <data>[base64(BlobA6)]</data>
      <key>status</key>
      <string>0</string>
      <key>version</key>
      <integer>1</integer>
    </dict>
    

BlobA6을 복호화하면서 Service 1~8 키 쌍을 만들어낸다.

# BlobA6 -> BlobA0
pcsData = AES.new(sessionKey, AES.MODE_CBC, iv=blob.iv).decrypt(blob.data)
blobA0 = Blob.BlobA0(pcsData)
# BlobA0 파싱 결과:
# blobA0.dsid       = b"18931861436"                      ← PBKDF2 password
# blobA0.salt       = 16 bytes                            ← PBKDF2 salt + AES IV
# blobA0.iterations = 정수 (예: 10000)                    ← PBKDF2 반복 횟수
# blobA0.key        = (unused)
# blobA0.data       = 암호화된 bplist                     ← 2차 복호화 대상
# blobA0.labal      = b"com.apple.protectedcloudstorage.record"

# BlobA0.data -> BackupBagPassword
derivedKey = pbkdf2_hmac("sha256", blobA0.dsid, blobA0.salt, blobA0.iterations, 16)
dictionaryData = AES.new(derivedKey, AES.MODE_CBC, blobA0.salt[:0x10]).decrypt(blobA0.data)

result = bplist_helper.BPListReader(unpad(dictionaryData, 16)).parse()
# {
#     "BackupBagPassword": b"<ASN.1 KeySet 바이너리>",
#     # ... 기타 필드
# }

# BackupBagPassword -> Service Key 1-7
decoder = asn1.Decoder()
decoder.start(recovered["BackupBagPassword"])
keyset = der.KeySet(decoder)
service_key_set = PCS.build_service_keys(keyset)

이후 Service 5 키를 이용해서 Pkt 8a의 metadata에서 얻은 kPCSMetadataEscrowedKeys 를 복호화하고 Service 8 키를 획득한다.

  Service 5 (Pkt 8c)          kPCSMetadataEscrowedKeys (Pkt 8a)
      |                            | ASN.1 parse
      |                        BackupEscrow { wrappedKey, data }
      |                            |
  ECDH(Service5_priv, ephemeral_X) -> shared_secret
                                          |
                                RFC6637 KDF -> KEK (16B)
                                          |
                                AES-KW unwrap -> session_key (32B)
                                          |
                                AES-256-GCM(session_key, data) -> KeySet
                                          |
                                Service 8: public_X(32B) + private_d(32B)

setup.json, keyset_dump.json

12a6c797-1df0-4992-9ba3-b9fcf064bb8d
12a6c797-1df0-4992-9ba3-b9fcf064bb8d

PHASE 3. 백업 기기 정보 획득

Phase 2에서 획득한 마스터 키로 실제 파일 복호화에 필요한 키 체인을 구축하는 단계이다. CloudKit API를 통해 Zone 암호화 키를 파생하고, 등록된 디바이스 목록에서 복원 대상을 선택한 뒤, 해당 디바이스의 Data Protection Keybag을 가져와 복호화한다. 이 단계가 끝나면 개별 파일의 암호를 풀 수 있는 모든 키가 준비된다.

파일 매칭

0x84 청크 복호화

0x81 청크 복호화

ESC
Type to search...