Android Device Encryption
2025년 1월 15일
ref #
google FBE 문서
google FDE 문서
Quarkslab 암호화 심층분석
LLeaves Android 데이터 암호화
기기 암호화 종류 #
Full Disk Encryption #
안드로이드 5 이상 9 이하 버전에서는 전체 디스크를 암호화하는 Full Disk Encryption(FDE) 방식을 사용한다. (7 이상부터 FBE가 추가됐지만 9까지는 일반적으로 FDE를 사용)
락을 처음 세팅할 때 유저가 입력한 인증정보를 키스트레칭 해서 128bit로 맞추고 salt와 함께 scrypt 같은 KDF(키 파생 함수)를 사용해서 IK를 만들고, HBK(Hardware Bound Key)와 RSA Sign 과정을 통해 KEK(Key Encryption Key)를 유도한다.
HBK는 TEE 메모리에 저장되기 때문에 UserKey + Salt (=IK1) 만으로는 KEK를 찾을 수 없다.
DEK(Data Encryption Key) 는 랜덤하게 만들어진(/dev/urandom) 128bit의 키 이며, 이 키를 통해 /data 파티션 전체를 암호화한다.
DEK는 KEK로 암호화되어 Crypto Footer (/metadata 등의 폴더) 에 저장되어 관리된다.
암복호화 할때마다 PIN 등의 유저 데이터를 입력받으며, KEK를 유도하고 Crypto Footer의 DEK를 복호화 해서 사용하게 된다.
File-Based Encryption #
AOS 7 버전부터 지원하는 암호화 방식이며, 10 이상 버전부터는 강제로 FBE를 사용하도록 변경됐다. 이 방식은 파일 단위로 독립적으로 암복호화 되며, DE와 CE 영역을 나뉘어 암호화할 수 있다는 점이 FDE와 다르다.
FDE처럼 유저 인증정보로 KEK를 만들고 동일하게 TEE를 통해 RSA Sign을 수행한다. DEK는 /dev/urandom에서 512bit를 읽어서 사용하며, 상위 256bit는 파일이름, 하위 256bit는 파일 내용을 암호화한다.
암복호화 타이밍 #
플래시메모리는 기본적으로 암호화되어 있고, 처음에 인증을 수행할 때 복호화 키가 만들어져서 메모리에 올라온다.
이후 데이터 접근을 하면서 RAM에 올리는 것들만 인증으로 만들어진 키로 실시간 블록단위 복호화를 하면서 유저는 복호화된 데이터에 접근할 수 있게 되는 것이고, 기기를 재부팅하거나 종료할때 직접적으로 암호화해서 저장하지는 않는다.
Strong Protection을 설정한 경우 화면 잠금이 될때 다시 암호화 되는 것처럼 보이는데, 사실 이건 RAM에 있던 키를 제거하는 것이라 플래시메모리를 복호화할 수 없어서 보이는 현상이다.
관련 TA #
1. Gatekeeper #
입력받은 사용자 인증정보를 토큰화 해서 입력받고 인증해주는 역할을 수행한다. 연속 인증시도에 대한 실패 카운트를 기록하고 패널티를 주기도 한다.
2. Keymaster #
암호화 키의 생성, 저장, 사용 연산(암복호화, 서명)을 담당하는 TA이다. 보통 Keystore API를 통해 키를 요청할때도 TEE 영역의 Keymaster TA 에서 연산이 이뤄진다.
FBE 암호화 #
FDE는 /data 파티션 전체를 암호화하지만, FBE는 파일 단위로 암호화하기 때문에 세분화된 처리가 가능하다.
Credential Encryption #
재부팅 후 언락을 하지 않고 adb를 이용해 /data/data 폴더의 내용을 보려고 하면 아래와 같이 앱 패키지이름이 아닌 이상한 값들이 보일것이다. 그리고 그 폴더 안의 파일을 읽으려 하면 Required key not available 같은 에러가 발생한다.
당연하게도 파일 접근권한과 별개로 동작하기 때문에 읽으려면 파일 접근권한은 있어야 한다.
기기가 처음 부팅되면 파일들이 암호화 되어있기 때문이고 언락을 해야만 복호화가 되기 때문이다. CE 는 위의 캡쳐에서 보이듯 부팅 후 인증이 완료된 경우에만 복호화가 되는 방식이다.
여기에서 FBE를 복호화하는 인증이라는 것은 암호/PIN/패턴 정도만 해당되며, 생체 인증은 해당되지 않기 때문에 기기에서 재부팅 후 첫 잠금은 생체인증으로 해제가 불가능한 것이다.
CE 키(DEK) 획득 과정 #
1. applicationId 만들기 #
사용자의 인증 정보(credentials)를 이용해서 {handle}.pwd 파일에 담긴 내용을 바탕으로 token을 생성한다.
Gatekeeper (TA) 의 검증이 완료되면 {handle}.secdis 로 얻어온 해시값과 합쳐 applicationId를 만들 수 있게 된다.
2. Synthetic Password #
{handle}.spblob 파일을 복호화해서 합성 비밀번호라고 불리는 Synthetic Password를 만들어야 한다.
이 SP는 사용자별로 무작위로 생성되는 시크릿키이며 FBE 암호화에 사용되는 CE키, Keystore 키를 보호하는 키, Gatekeeper HAT 발급 등 여러 복호화에 사용되는 키들은 SP에서 파생되어 사용된다.
첫번째 키는 /data/misc/keystore/persistent.sqlite DB에서 synthetic_password_{handle} 을 키로 사용해서 찾아온 blob 데이터를 TA를 통해 한번 가공해서 만들어낸 키이다. 이 TA를 Keymaster 라고 부르며 TA에서 구현되어야 한다. (= keystore 2.0 에서 쓰는 db라는 뜻)
두번째 키는 1번에서 만들어낸 applicationId 를 이용한다.
CE 키(DEK) 획득 과정 + Weaver #
Weaver는 보안 칩(갤럭시는 Knox Vault Processor)에 연결된 HAL 구현체로, applicationId 생성에서 Weaver가 개입하게 된다.
read, write, getConfig 기능을 제공하며 Weaver에 read를 요청 할때 {handle}.weaver에 적혀있는 slot 번호와 거기에 맞는 키를 전달해줘야 한다.
이 키는 PIN이 세팅되면서 weaver에 개념적으로 slot-key 쌍을 키로 두고 랜덤한 값과 상태를 저장하게 된다.
만약 옳지 않은 slot-key 쌍을 전달한다면 read 요청이 실패하게 되며, 해당 슬롯에 대한 실패 카운트를 기록하고 일정 수 이상 실패 시 하드웨어적인 패널티를 겪게 된다.
이 로직에서는 PIN이 weaver와 합쳐져서 applicationId를 생성하기 때문에 Keymaster TA를 분석했다 하더라도 weaver의 value를 얻어올 수 없다면 bruteforce가 방지된다.
Device Encryption #
DE 는 부팅시 복호화되며 인증과 상관없이 언제든지 접근 가능한 데이터가 저장된다.
테스트 경로: /data/system_de/0
실제 코드 분석 #
1. PIN 생성 진입점 #
LockSettingsService.java #
핀 관련 작업은 setLockCredential 함수에서 시작된다. 처음 PIN을 설정할 땐 savedCredential.isNone() 상태이고, oldProtector는 PIN이 없어도 초기화때 None-LSKF protector 가 저장된다.
1private boolean setLockCredentialInternal(LockscreenCredential credential,
2 LockscreenCredential savedCredential, int userId, boolean isLockTiedToParent) {
3 // None-LSKF protector (not null)
4 final long oldProtectorId = getCurrentLskfBasedProtectorId(userId);
5 AuthenticationResult authResult = mSpManager.unlockLskfBasedProtector(
6 getGateKeeperService(), oldProtectorId, savedCredential, userId, null);
7 VerifyCredentialResponse response = authResult.gkResponse;
8 // not null
9 SyntheticPassword sp = authResult.syntheticPassword;
10
11 // 키 설정을 위해 필요한 unlock들이 있음. 준비 절차 수행하는곳
12 onSyntheticPasswordUnlocked(userId, sp);
13 // 실질적인 키 세팅. Gatekeeper에 최초 세팅하는 함수
14 setLockCredentialWithSpLocked(credential, sp, userId);
15}
새로 입력한 credential을 세팅하는 함수이다. credential이 null인 경우엔 제거하는 로직도 있다.
Sid를 생성하며 /data/system_de/<userId>/spblob/0000000000000000.handle 파일이 만들어진다.
1private long setLockCredentialWithSpLocked(LockscreenCredential credential,
2 SyntheticPassword sp, int userId) {
3 // 전달받은 credential로 새 ProtectorID를 생성. 이때 Gatekeeper가 관여한다.
4 // 이때 oldProtector의 sp를 그대로 전달하는데, sp는 동일하게 사용함.
5 final long newProtectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(), credential, sp, userId);
6 if (!credential.isNone()) {
7 // 완전 최초 설정엔 Sid(Keystore에서 사용할 사용자 식별값)가 없다.
8 if (!mSpManager.hasSidForUser(userId)) {
9 // Gatekeeper를 통해 비밀키로 서명된 password_handle을 발급받아 REE에 저장
10 // /data/system_de/<userId>/spblob/0000000000000000.handle
11 mSpManager.newSidForUser(getGateKeeperService(), sp, userId);
12 // Gatekeeper에서 인증된 상태임을 확인하고 범용 HAT(Hardware Auth Token) 전달
13 // 지정한 AUTH_TIMEOUT 시간 동안 HAT가 유효하며 PIN 설정 직후에도 백업등 키가 필요한 일부 기능들 사용 가능
14 mSpManager.verifyChallenge(getGateKeeperService(), sp, 0L, userId);
15 }
16 else {
17 // ... 잠금해제 로직
18 }
19 // LSKF protector를 방금 생성한걸로 스위치.
20 // 이후 잠금해제에서 사용되는 protector는 이 값이 된다.
21 setCurrentLskfBasedProtectorId(newProtectorId, userId);
22 // 시스템에 캐시된 자격 타입(PIN/패턴/없음)을 무효화
23 LockPatternUtils.invalidateCredentialTypeCache();
24 // 이전 oldProtector 삭제
25 mSpManager.destroyLskfBasedProtector(oldProtectorId, userId);
26}
2. Weaver/GK enroll #
SyntheticPasswordManager.java #
weaver를 호출하며 protectorSecret (credential+weaver/gk)을 생성하는 위치이다.
- credential → stretchedLskf: 그림에서는
Token - weaver/gk 랜덤생성 secret → hashed: 그림에서는
hashed secret spblob inner key: 위 두개를 applicationId와 합쳐 키로 사용 (이부분은 암호화할때 나옴)
1/***
2입력: (gatekeeper, credential, sp, userId)
3↓
4protectorId 생성 → PIN 길이(옵션) 계산 → PasswordData 준비 → LSKF 스트레칭
5↓
6Weaver 사용? (환경/설정/빈 LSKF 여부로 결정)
7 ├─ Yes: Weaver 슬롯 등록(enroll) → FRP 동기화(Weaver) → protectorSecret = f(stretched, weaverSecret)
8 └─ No : (빈 LSKF 아니면) GK enroll → FRP 동기화(GK)
9 protectorSecret = f(stretched, secdiscardable)
10↓
11PasswordData/메트릭 저장(빈 LSKF 아니면)
12↓
13SP Blob 생성(= protectorId로 SP를 저장, sid 함께) → 디스크 flush → protectorId 반환
14***/
15public long createLskfBasedProtector(IGateKeeperService gatekeeper,
16 LockscreenCredential credential, SyntheticPassword sp, int userId) {
17 long protectorId = generateProtectorId();
18 // LSKF 타입, 길이 등 메타데이터 및 scrypt 저장 컨테이너(N, r, p, salt) 생성
19 PasswordData pwd = credential.isNone() ? null :
20 PasswordData.create(credential.getType(), pinLength);
21 // scrypt로 credential 스트레치.
22 // 그림에서 Token이라고 부르는건 이 stretchedLskf 키를 의미함.
23 byte[] stretchedLskf = stretchLskf(credential, pwd);
24 long sid = GateKeeper.INVALID_SECURE_USER_ID;
25 final byte[] protectorSecret;
26
27 final IWeaver weaver = getWeaverService(); // or 사용할 수 없다면 null
28 if (weaver != null) {
29 // 사용가능한 슬롯 할당
30 int weaverSlot = getNextAvailableWeaverSlot();
31 // stretchedLskf 로 만든 WeaverKey를 등록하는데 null이면 새 랜덤값을 기록하고 반환
32 byte[] weaverSecret = weaverEnroll(weaver, weaverSlot,
33 stretchedLskfToWeaverKey(stretchedLskf), null);
34 // 어떤 슬롯을 사용하고있는지 기록. /data/system_de/0/spblob/<protector_id>.weaver 파일 생성
35 saveWeaverSlot(weaverSlot, protectorId, userId);
36 // 메모리상으로 사용중인 슬롯관리
37 mPasswordSlotManager.markSlotInUse(weaverSlot);
38 // SP를 보호하는 protectorSecret 생성 (spblob의 복호화 재료임) concat(stretchedLskf, hash(weaverSecret))
39 protectorSecret = transformUnderWeaverSecret(stretchedLskf, weaverSecret);
40 } else {
41 if (!credential.isNone()) {
42 gatekeeper.clearSecureUserId(fakeUserId(userId));
43 // stretchedLskf 로 GK용 키를 만들고 그걸로 enroll해서 등록한다. 나중에 verify에서 사용
44 // GateKeeper도 실패 누적 시 RETRY/타임아웃을 적용한다.
45 response = gatekeeper.enroll(fakeUserId(userId), null, null,
46 stretchedLskfToGkPassword(stretchedLskf));
47 pwd.passwordHandle = response.getPayload();
48 sid = sidFromPasswordHandle(pwd.passwordHandle);
49 }
50 // concat(stretchedLskf, hash(secdiscardable))
51 protectorSecret = transformUnderSecdiscardable(stretchedLskf,
52 createSecdiscardable(protectorId, userId));
53 synchronizeGatekeeperFrpPassword(pwd, 0, userId);
54 }
55 if (!credential.isNone()) {
56 saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId); // *.pwd
57 savePasswordMetrics(credential, sp, protectorId, userId); // *.metrics
58 }
59 // sp를 protectorSecret(+@)로 암호화해서 spblob 파일로 저장
60 // *.spblob 파일 생성
61 createSyntheticPasswordBlob(protectorId, PROTECTOR_TYPE_LSKF_BASED, sp, protectorSecret, sid, userId);
62 syncState(userId);
63 return protectorId;
64}
호출 경로 #
Weaver와 Gatekeeper의 실제 TA 코드는 BinderHAL로 연결되기 때문에 벤더마다 다르게 작성된다.
- IGateKeeperService.aidl
-
gatekeeperd.cpp : 프레임워크 요청을 받아서 각 벤더 하드웨어에 맞게 enroll 호출해줌
aidl_hw_device->enroll - IGateKeeper.aidl : 벤더가 구현해야하는 aidl
- trusty/gatekeeper/service.app : 벤더가 구현하는 HAL 등록 코드
- HAL trusty_gatekeeper.h : 벤더가 구현하는 BinderHAL 코드(예시는 Pixel 기준으로 Trusty TEE)
- HAL trusty_gatekeeper.cpp : BinderHAL 실제 구현 여기에서 Trusty IPC로 TrustyGateKeeper TA로 요청
- TA trusty_gatekeeper.cpp : 실제 Trusty TEE의 GateKeeper TA 코드
1SyntheticPasswordManager.createLskfBasedProtector(...)
2 └─ gatekeeper.enroll(fakeUserId(userId), null, null, stretchedLskfToGkPassword(...)) [Java Binder]
3 └─ IGateKeeperService.aidl (framework ↔ native binder 인터페이스)
4 └─ gatekeeperd (C++ binder 서비스) → HAL 호출(AIDL/HIDL 중 AIDL 우선)
5 └─ android.hardware.gatekeeper.IGatekeeper/default (AIDL HAL)
6 └─ android.hardware.gatekeeper-service.trusty (userspace HAL)
7 └─ Trusty IPC → Trusty Gatekeeper TA(TEE)에서 enroll 실행
- IWeaver.aidl
- weaver/aidl/default/service.cpp : 벤더가 작성해야하는 Binder HAL 샘플코드 (등록)
- weaver/aidl/default/Weaver.cpp : 벤더가 작성해야하는 Binder HAL 샘플코드 (Weaver)
1SyntheticPasswordManager.weaverEnroll()
2 → IWeaver.write(slot, key, value) // AIDL/HIDL Binder
3 → android.hardware.weaver*-service.citadel // Pixel 벤더 HAL
4 → (Citadel/Titan/TEE의 TA) // 실제 슬롯에 key/value 저장
3. sp 암호화 #
SyntheticPasswordManager.java #
createSyntheticPasswordBlob 에서 sp를 두번 암호화해서 spblob 을 만들게된다. EscrowToken 암호화 방식도 여기 루트를 타는데 의미없어서 제외
1private void createSyntheticPasswordBlob(long protectorId, byte protectorType,
2 SyntheticPassword sp, byte[] protectorSecret, long sid, int userId) {
3 final byte[] spSecret;
4 spSecret = sp.getSyntheticPassword();
5
6 // sp 암호화. alias: "synthetic_password_{protectorId}"
7 byte[] content = createSpBlob(getProtectorKeyAlias(protectorId), spSecret, protectorSecret, sid);
8
9 byte version = sp.mVersion == SYNTHETIC_PASSWORD_VERSION_V3
10 ? SYNTHETIC_PASSWORD_VERSION_V3 : SYNTHETIC_PASSWORD_VERSION_V2;
11 // 이건 그냥 blob 바이트로 만들고 *.spblob 저장하는 로직
12 SyntheticPasswordBlob blob = SyntheticPasswordBlob.create(version, protectorType, content);
13 saveState(SP_BLOB_NAME, blob.toByte(), protectorId, userId);
14}
15
16// 별 내용은 없다. SyntheticPasswordCrypto 호출래퍼
17protected byte[] createSpBlob(String protectorKeyAlias, byte[] data, byte[] protectorSecret, long sid) {
18 return SyntheticPasswordCrypto.createBlob(protectorKeyAlias, data, protectorSecret, sid);
19}
SyntheticPasswordCrypto.java #
sp 를 두겹으로 암호화하여 spblob 데이터를 만들어낸다.
spblob outer key: 여기서 생성하는 keystore 키. 그림에서synthetic_password_{protectorId}- 첫번째 암호화:
inner key = applicationId + token + hashed secret - 두번째 암호화:
outer key = synthetic_password_{protectorId}
1public static byte[] createBlob(String protectorKeyAlias, byte[] data, byte[] protectorSecret, long sid) {
2 // 바깥(Keystore)층 복호화 랜덤 AES-GCM 키 생성
3 KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES);
4 keyGenerator.init(AES_GCM_KEY_SIZE * 8, new SecureRandom());
5 SecretKey protectorKey = keyGenerator.generateKey();
6 final KeyStore keyStore = getKeyStore();
7
8 // 복호화 용도(PURPOSE_DECRYPT)로만 사용 가능
9 KeyProtection.Builder builder = new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
10 .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
11 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
12 .setCriticalToDeviceEncryption(true);
13
14 // 이 키는 인증된 사용자(HAT을 함께 제출)만 사용할 수 있다.
15 // sid에 해당하는 HAT이 살아있는 경우 && HAT이 발급된 15초 이내
16 builder.setUserAuthenticationRequired(true)
17 .setBoundToSpecificSecureUserId(sid) // 15초
18 .setUserAuthenticationValidityDurationSeconds(USER_AUTHENTICATION_VALIDITY);
19
20 // 롤백 저항옵션이 있는 키와 아닌 키 생성. 기기마다 지원되지 않을 수 있기 때문에
21 final KeyProtection protNonRollbackResistant = builder.build();
22 builder.setRollbackResistant(true);
23 final KeyProtection protRollbackResistant = builder.build();
24 final KeyStore.SecretKeyEntry entry = new KeyStore.SecretKeyEntry(protectorKey);
25 try {
26 // 롤백저항 옵션으로 키 저장
27 // 과거 키를 얻어와서 keystore에 세팅하려할 때 방지해주는 기능임
28 // 이 기능이 없다면 HAT설정이 없는 과거 키를 복원하고 과거의 spblob파일을 복호화할 수 있게된다.
29 // "synthetic_password_{protectorId}"
30 keyStore.setEntry(protectorKeyAlias, entry, protRollbackResistant);
31 Slog.i(TAG, "Using rollback-resistant key");
32 } catch (KeyStoreException e) {
33 // 예외 발생하면 NonRollbackResistant 로 진행
34 Slog.w(TAG, "Rollback-resistant keys unavailable. Falling back to "
35 + "non-rollback-resistant key");
36 keyStore.setEntry(protectorKeyAlias, entry, protNonRollbackResistant);
37 }
38
39 // protectorSecret (credential + weaver/gk)으로 암호화
40 // PROTECTOR_SECRET_PERSONALIZATION = "application-id".getBytes();
41 byte[] intermediate = encrypt(protectorSecret, PROTECTOR_SECRET_PERSONALIZATION, data);
42 // protectorKey (keystore Key)로 암호화
43 return encrypt(protectorKey, intermediate);
44}
4. Unlock 과정 #
SyntheticPasswordManager.java #
1public AuthenticationResult unlockLskfBasedProtector(IGateKeeperService gatekeeper,
2 long protectorId, @NonNull LockscreenCredential credential, int userId,
3 ICheckCredentialProgressCallback progressCallback) {
4 // 패스워드 파일 로드. 파일이 있는경우에만
5 byte[] pwdDataBytes = loadState(PASSWORD_DATA_NAME, protectorId, userId);
6 PasswordData pwd = PasswordData.fromBytes(pwdDataBytes);
7 int storedType = pwd.credentialType;
8 // 현재 들어온 credential과 pwd 파일에 저장된 내용이 같은지 확인
9 if (!credential.checkAgainstStoredType(storedType)) {
10 Slogf.e(TAG, "Credential type mismatch: stored type is %s but provided type is %s",
11 LockPatternUtils.credentialTypeToString(storedType),
12 LockPatternUtils.credentialTypeToString(credential.getType()));
13 result.gkResponse = VerifyCredentialResponse.ERROR;
14 return result;
15 }
16
17 // credential 키 스트레치. 여기서부터 이미지에 있는 순서도의 시작
18 byte[] stretchedLskf = stretchLskf(credential, pwd);
19 final byte[] protectorSecret;
20 long sid = GateKeeper.INVALID_SECURE_USER_ID;
21 int weaverSlot = loadWeaverSlot(protectorId, userId);
22 if (weaverSlot != INVALID_WEAVER_SLOT) {
23 // Weaver 가 있는경우 위버로 검증
24 result.usedWeaver = true;
25 final IWeaver weaver = getWeaverService();
26 result.gkResponse = weaverVerify(weaver, weaverSlot,
27 stretchedLskfToWeaverKey(stretchedLskf));
28 // 검증 후에는 stretchedLskf에 weaverSecret을 붙여서 protectorSecret을 만든다.
29 // HAT처럼 보이는건 그냥 같은 필드를 돌려쓴것.
30 protectorSecret = transformUnderWeaverSecret(stretchedLskf,
31 result.gkResponse.getGatekeeperHAT());
32 } else {
33 // Weaver가 없는경우 GK를 통해서 검증
34 byte[] gkPassword = stretchedLskfToGkPassword(stretchedLskf);
35 GateKeeperResponse response;
36 // 이후 passwordHandle과 gkPassword 를 비교한다.
37 response = gatekeeper.verifyChallenge(fakeUserId(userId), 0L,
38 pwd.passwordHandle, gkPassword);
39 result.gkResponse = VerifyCredentialResponse.OK;
40 if (response.getShouldReEnroll()) {
41 // GK 가 업데이트되면서 재발급받아야되는 경우 로직
42 // ...
43 }
44 sid = sidFromPasswordHandle(pwd.passwordHandle);
45 byte[] secdiscardable = loadSecdiscardable(protectorId, userId);
46 protectorSecret = transformUnderSecdiscardable(stretchedLskf, secdiscardable);
47 }
48
49 result.syntheticPassword = unwrapSyntheticPasswordBlob(protectorId,
50 PROTECTOR_TYPE_LSKF_BASED, protectorSecret, sid, userId);
51 // Perform verifyChallenge to refresh auth tokens for GK if user password exists.
52 result.gkResponse = verifyChallenge(gatekeeper, result.syntheticPassword, 0L, userId);
53 // Upgrade case: store the metrics if the device did not have stored metrics before, should
54 // only happen once on old protectors.
55 if (result.syntheticPassword != null && !credential.isNone()
56 && !hasPasswordMetrics(protectorId, userId)) {
57 savePasswordMetrics(credential, result.syntheticPassword, protectorId, userId);
58 syncState(userId);
59 }
60 return result;
61}
5. Weaver/GK verify #
SyntheticPasswordManager.java (weaver) #
weaver를 확인해보면 슬롯이랑 key를 넣어서 값을 가져온다.
위에서 봤듯 성공하면 weaverSecret 값을 GetekeeperHAT 필드에 담고 응답해준다. 여러 상태값에 따라 Throttle이 걸릴 수 있고 실패할수도 있다.
1private VerifyCredentialResponse weaverVerify(IWeaver weaver, int slot, byte[] key) {
2 final WeaverReadResponse readResponse;
3 try {
4 // Binder HAL을 통해 값을 읽어온다.
5 readResponse = weaver.read(slot, key);
6 } catch (RemoteException e) {
7 Slog.e(TAG, "weaver read failed, slot: " + slot, e);
8 return VerifyCredentialResponse.ERROR;
9 }
10 switch (readResponse.status) {
11 case WeaverReadStatus.OK:
12 // HAT 필드에 담기는건 weaverSecret
13 return new VerifyCredentialResponse.Builder()
14 .setGatekeeperHAT(readResponse.value)
15 .build();
16 case WeaverReadStatus.THROTTLE:
17 Slog.e(TAG, "weaver read failed (THROTTLE), slot: " + slot);
18 return responseFromTimeout(readResponse);
19 case WeaverReadStatus.INCORRECT_KEY:
20 case WeaverReadStatus.FAILED:
21 default:
22 return VerifyCredentialResponse.ERROR;
23 }
24}
- weaver/aidl/default/Weaver.cpp : 벤더가 작성해야하는 Binder HAL 샘플코드 (Weaver)
Weaver는 TA가 아니라 별도의 보안 칩을 이용한 HAL 통신이기 때문에 각 벤더에서 생산한 보안칩에 값을 읽거나 쓰는 기능을 HAL로 구현해두고 보안칩과 직접 통신하는 것이다.
AOSP 기준으로 hal_weaver_client 속성을 가진 system_server 에서만 Weaver HAL API를 호출할 수 있다.
system_server에서 코드인젝션으로 WeaverSecret을 얻어오는 방법도 어렵다.
슬롯과 키를 맞추지 못하면 하드웨어 자체적으로 Throttle이 걸리는데, 직접 통신한다고 해도 enroll때 credential에서 파생된 값을 키로 사용하기 때문에 credential 값을 모른다면 Weaver에 요청해봤자 의미가 없다.
trusty Gatekeeper #
gatekeeper는 enroll 처럼 데몬과 IPC를 통해서 호출되기 때문에 좀 더 복잡하다. 여기에서 자세히 다뤄보자
-
unlockLskfBasedProtector 함수에서
gatekeeper.verifyChallenge가 호출되면 객체의 함수가 호출되기 때문에 AOSP 플랫폼의 코드에서 확인할 수 있다.
AIDL 인터페이스를 통해 GatekeeperService를 상속받은 gatekeeperd 코드가 호출된다. -
gatekeeperd는 요청을 서비스에 등록된 HAL 구현체(AIDL or HIDL)로 넘기는 프록시 역할만 한다. HAL 구현체에서는
VerifyRequest를 만들어서 IPC로 TA로 요청을 보낸다.
헤더에서 Send 함수가 VerifyRequest를 인자로 받을때 command에GK_VERIFY를 담도록 오버로드 되어있다.-
Binder HAL 헤더
1gatekeeper_error_t Send(const VerifyRequest& request, VerifyResponse* response) { 2 return Send(GK_VERIFY, request, response); 3} -
Binder HAL 구현체
1gatekeeper_error_t TrustyGateKeeperDevice::Send(uint32_t command, const GateKeeperMessage& request, 2 GateKeeperMessage *response) { 3 int rc = trusty_gatekeeper_call(command, send_buf, request_size, recv_buf, &response_size); 4}
-
Binder HAL 헤더
-
Send는 TIPC(Trusty IPC)를 통해 GateKeeper TA 포트로 verify 요청을 전송한다.
항상 TIPC 형태는 아니고, 벤더마다 구현방식이 다르다. -
GateKeeper TA는 IPC로 들어오는 값을 루프로 받아서 요청에 해당하는 작업을 수행한다.
-
TA Gatekeeper IPC
1static gatekeeper_error_t handle_request(uint32_t cmd, 2 uint8_t* in_buf, 3 uint32_t in_buf_size, 4 UniquePtr<uint8_t[]>* out_buf, 5 uint32_t* out_buf_size) { 6 switch (cmd) { 7 case GK_ENROLL: 8 return exec_cmd(&GateKeeper::Enroll, in_buf, in_buf_size, out_buf, 9 out_buf_size); 10 case GK_VERIFY: 11 return exec_cmd(&GateKeeper::Verify, in_buf, in_buf_size, out_buf, 12 out_buf_size); 13 case GK_DELETE_USER: 14 return exec_cmd(&GateKeeper::DeleteUser, in_buf, in_buf_size, out_buf, 15 out_buf_size); 16 case GK_DELETE_ALL_USERS: 17 return exec_cmd(&GateKeeper::DeleteAllUsers, in_buf, in_buf_size, 18 out_buf, out_buf_size); 19 default: 20 return ERROR_INVALID; 21 } 22} -
실제 Gatekeeper : 이 코드는 platform에 있는데, 여러 벤더의 TEE 백엔드에서 공용으로 사용할 수 있는 공통 모듈이라서 그렇다.
검증하는 코드 부분인데, 정답지와 검증대상이 둘다 request에서 꺼내오는것을 알 수 있다.1void GateKeeper::Verify(const VerifyRequest &request, VerifyResponse *response) { 2 // request에서 password_handle(정답지) 꺼냄 3 const password_handle_t *password_handle = request.password_handle.Data<password_handle_t>(); 4 if (!password_handle || password_handle->version > HANDLE_VERSION) { 5 response->error = ERROR_INVALID; 6 return; 7 } 8 9 secure_id_t user_id = password_handle->user_id; 10 secure_id_t authenticator_id = 0; 11 uint32_t uid = request.user_id; 12 uint64_t timestamp = GetMillisecondsSinceBoot(); 13 uint32_t timeout = 0; 14 // 핸들 버전이 옛날버전이면 스로틀 기능이 없을수 있음 15 bool throttle = (password_handle->version >= HANDLE_VERSION_THROTTLE); 16 bool throttle_secure = password_handle->flags & HANDLE_FLAG_THROTTLE_SECURE; 17 if (throttle) { 18 // 스로틀 있는 버전인경우. 스로틀 레코드를 조회해본다. 19 failure_record_t record; 20 if (!GetFailureRecord(uid, user_id, &record, throttle_secure)) { 21 response->error = ERROR_UNKNOWN; 22 return; 23 } 24 // 이미 스로틀 상태라면 여기에서 리턴 25 if (ThrottleRequest(uid, timestamp, &record, throttle_secure, response)) return; 26 // 스로틀 상태가 아니면 미리 실패로 처리해서 retry 시간 계산 27 if (!IncrementFailureRecord(uid, user_id, timestamp, &record, throttle_secure)) { 28 response->error = ERROR_UNKNOWN; 29 return; 30 } 31 timeout = ComputeRetryTimeout(&record); 32 } else { 33 // 스로틀 없는 버전인 경우 재등록 요청한다. 이게 ShouldReEnroll 세팅 34 response->request_reenroll = true; 35 } 36 37 // 여기에서 실제로 검증한다. 인자로 전달된 두 패스워드를 검증하는것 38 // - password_handle: 정답지 39 // - provided_password: credential로 계산된 패스워드 40 if (DoVerify(password_handle, request.provided_password)) { 41 // 검증 완료된 경우. 42 SizedBuffer auth_token; 43 response->error = MintAuthToken(&auth_token, timestamp, 44 user_id, authenticator_id, request.challenge); 45 if (response->error != ERROR_NONE) return; 46 // HAT 발급 47 response->SetVerificationToken(move(auth_token)); 48 // 스로틀 초기화 49 if (throttle) ClearFailureRecord(uid, user_id, throttle_secure); 50 } else { 51 // 틀린경우 미리 계산해뒀던 retry 타임아웃 적용 52 if (throttle && timeout > 0) { 53 response->SetRetryTimeout(timeout); 54 } else { 55 response->error = ERROR_INVALID; 56 } 57 } 58} 59 60// 실제 검증코드. credential이 포함된 password를 이용해서 password_handle을 만들어본다. 61bool GateKeeper::DoVerify(const password_handle_t *expected_handle, const SizedBuffer &password) { 62 if (!password) return false; 63 SizedBuffer provided_handle; 64 if (!CreatePasswordHandle(&provided_handle, expected_handle->salt, expected_handle->user_id, 65 expected_handle->flags, expected_handle->version, password)) { 66 return false; 67 } 68 const password_handle_t *generated_handle = provided_handle.Data<password_handle_t>(); 69 return memcmp_s(generated_handle->signature, expected_handle->signature, 70 sizeof(expected_handle->signature)) == 0; 71}
-
TA Gatekeeper IPC
trusty Gatekeeper의 password_handle 생성로직 #
DoVerify에서 pwd 파일에 포함된 expected_handle의 시그니쳐와 credential로 만든 password의 handle 시그니쳐를 비교하는것을 봤다.
시그니쳐 계산은 다시 각각의 벤더 TA에서 구현되는데, TEE 내부 키를 사용해서 시그니쳐를 만들게된다. (MTK 언락 취약점이 여기서 나온걸지도 모르겠다)
계산식은 복잡할 수 있으니 흐름을 따라갈 수 있는 중요한 부분만 추가한다.
1bool GateKeeper::CreatePasswordHandle(SizedBuffer *password_handle_buffer, salt_t salt,
2 secure_id_t user_id, uint64_t flags, uint8_t handle_version, const SizedBuffer & password) {
3 // ...
4
5 // password_key를 가져옴.
6 GetPasswordKey(&password_key, &password_key_length);
7 if (!password_key || password_key_length == 0) {
8 return false;
9 }
10
11 // 여기에서 시그니쳐 계산함. 이때 password_key를 사용한다.
12 ComputePasswordSignature(password_handle.signature, sizeof(password_handle.signature),
13 password_key, password_key_length, to_sign.get(), to_sign_size, salt);
14 uint8_t *ph_buffer = new(std::nothrow) uint8_t[sizeof(password_handle_t)];
15 if (ph_buffer == nullptr) return false;
16 *password_handle_buffer = { ph_buffer, sizeof(password_handle_t) };
17 memcpy(ph_buffer, &password_handle, sizeof(password_handle_t));
18 return true;
19}
1// 패스워드 키를 가져오는 로직.
2void TrustyGateKeeper::GetPasswordKey(const uint8_t** password_key,
3 uint32_t* length) {
4 *password_key = const_cast<const uint8_t*>(password_key_.get());
5 *length = HMAC_SHA_256_KEY_SIZE;
6}
7
8// 시그니쳐 계산
9void TrustyGateKeeper::ComputePasswordSignature(uint8_t* signature,
10 uint32_t signature_length,
11 const uint8_t* key,
12 uint32_t key_length,
13 const uint8_t* password,
14 uint32_t password_length,
15 salt_t salt) const {
16 uint8_t salted_password[GATEKEEPER_MAX_BUFFER_LENGTH];
17 assert(password_length + sizeof(salt) <= sizeof(salted_password));
18 memcpy(salted_password, &salt, sizeof(salt));
19 memcpy(salted_password + sizeof(salt), password, password_length);
20 ComputeSignature(signature, signature_length, key, key_length,
21 salted_password, password_length + sizeof(salt));
22}
23
24void TrustyGateKeeper::ComputeSignature(uint8_t* signature,
25 uint32_t signature_length,
26 const uint8_t* key,
27 uint32_t key_length,
28 const uint8_t* message,
29 const uint32_t length) const {
30 uint8_t buf[HMAC_SHA_256_KEY_SIZE];
31 unsigned int buf_len;
32 HMAC(EVP_sha256(), key, key_length, message, length, buf, &buf_len);
33 size_t to_write = buf_len;
34 if (buf_len > signature_length)
35 to_write = signature_length;
36 memset(signature, 0, signature_length);
37 memcpy(signature, buf, to_write);
38}
공격방식 #
Quarkslab #
Gatekeeper #
첫번째 키로 복호화된 값 value_leaked_from_keymaster을 후킹으로 얻고나면 credential을 무차별대입해서 직접 계산한 후 복호화해보면 된다.
AES-GCM으로 암호화되어 있기 때문에 잘못된 credential이라면 복호화 자체를 실패한다.
1pwd = generate new password # Bruteforce 위치
2token = scrypt(pwd, R, N, P, Salt)
3Application_id = token || Prehashed value
4key = SHA512("application_id" || application_id)
5
6# 복호화 성공 시 credential을 잘 선택했다는 의미이다.
7AES_Decrypt(value_leaked_from_keymaster, key)
원래 credential 인증이 성공할때만 AuthToken이 생성되어 spblob의 복호화가 실행된다.
Quarkslab에서는 GK를 직접 패치해서 always로 변경했고, 복호화된 spblob 데이터를 system_server 후킹으로 얻었다.
무차별적으로 생성한 inner key를 이용해서 spblob 데이터를 한번 더 복호화하는 작업이 성공하는지에 따라 PIN을 얻을 수 있게된다.
Weaver #
gatekeeper의 쓰로틀 우회는 직접 패치해서 달성했지만, Weaver는 따로 보안칩에서 작동하기 때문에 동일한 방식으로는 불가능했다. 메모리를 추출하는 CVE-2022-20233 취약점을 이용하여 Weaver Secret을 획득하고 token이 weaver 키와 합쳐져 hashed_secret이 만들어지는 과정을 공략했다.
1pwd = generate new password
2token = scrypt(pwd, R, N, P, Salt)
3key = SHA512("weaver_key" || token)
4
5# 메모리에서 유출된 키와 비교해서 같은지 확인
6Compare with leaked Weaver key
TA 공격 벡터 확인 #
플랫폼의 unlock 코드에서 생성 및 사용되는 파일은 *.secdis, *.weaver, *.pwd, *.metrics, *.spblob, persistant.sqlite 파일이 있다. 모든 플랫폼에 웬만하면 공통적으로 포함될 것이다.
-
inner Key
credential 은*.pwd파일과 함께 token이 되고, GK에서 검증(Verify)을 받는다.
*.secdis는 해시화되어 토큰과 함께 applicationId를 만드는데 사용된다. -
outer Key
Keystore에서 synthetic_password_{handle} 키에 대한 값을 얻어서*.spblob파일을 1차적으로 복호화하는데 사용한다.
Keystore 값은persistant.sqlite에 저장되어 있고, TA의 특정한 로직으로 복호화가 되어야 값을 얻을 수 있다.
Gatekeeper Verify #
Gatekeeper의 Verify 로직에서 password_handle의 시그니쳐끼리 분석하게 된다.
1static class PasswordData {
2 byte scryptN;
3 byte scryptR;
4 byte scryptP;
5 public int passwordType;
6 byte[] salt;
7 public byte[] passwordHandle; // password_handle_t
8};
9struct __attribute__ ((__packed__)) password_handle_t {
10 uint8_t version;
11 secure_id_t user_id; // uint64_t
12 uint64_t flags;
13 salt_t salt; // uint64_t
14 uint8_t signature[32];
15 bool hardware_backed;
16};
17
18public AuthenticationResult unlockLskfBasedProtector(IGateKeeperService gatekeeper,
19 long protectorId, @NonNull LockscreenCredential credential, int userId,
20 ICheckCredentialProgressCallback progressCallback) {
21 // 패스워드 파일 로드. 파일이 있는경우에만
22 byte[] pwdDataBytes = loadState(PASSWORD_DATA_NAME, protectorId, userId);
23 PasswordData pwd = PasswordData.fromBytes(pwdDataBytes);
24
25 // 여기에서 pwd.passwordHandle 이 expected_handle 값이다.
26 response = gatekeeper.verifyChallenge(fakeUserId(userId), 0L,
27 pwd.passwordHandle, gkPassword);
Gatekeeper를 분석해서 password_handle 계산 방식을 알아낸다면, pwd.passwordHandle->signature 값과 동일한 handle을 무차별대입으로 만들어낼 수 있을것이다.
하늘색으로 칠해진 0x38 - 0x58 값이 signature이다.
Keymaster #
Quarkslab에서 사용한 방식의 확장이다.
spblob의 복호화 방식이 AES-GCM 이기 때문에(이 부분도 재구현한 플랫폼이 있을까?) 복호화를 직접 해보면 성공 실패 여부를 알 수 있고, outer Key로 한번 복호화된 spblob를 무작위로 생성해낸 inner Key로 복호화 하는방법이다.
Quarkslab은 TA를 패치해서 정상적으로 spblob이 복호화되는 과정을 후킹했지만, 이 부분이 불가능하다면 Keymaster TA를 분석하여 synthetic_password_{handle} 값을 획득하고 직접 복호화할 수 있을 것이다.
- Stretch된 Key를 Bruteforce하기는 어려우니 Key 생성 로직을 파악하고 Input 키를 Bruteforce하면 더 쉬울것이다.
- 만약 outer key가 고정키들의 모임으로 만들어지거나 Frida로 키 유출이 가능하다면 해결될듯 싶다.
게이트키퍼 키 생성 #
모듈을 후킹하는 방법을 찾아보자. 아마 TA 에 있을건데 그냥 코드로 확인해야할수도있고, 후킹이 가능해서 한꺼번에 전역적으로 할수도 있을지도???
공통모듈 CreatePasswordHandle에 “password_handle_t does not appear to be packed” 라는 문자열이 있음 이걸 기준으로 찾아도될듯