iOS 앱 무결성 검증 코드
iOS 앱 무결성 검증 코드

iOS 앱 무결성 검증은 앱이 배포된 이후 실행 파일이나 보안 로직이 변조되었는지 확인하기 위한 중요한 보안 작업입니다.

iOS 앱에 탈옥 감지, 디버깅 감지, Frida 탐지, 결제 검증 로직을 넣어두어도 앱 자체가 변조되면 해당 로직은 우회될 수 있습니다. 예를 들어 보안 체크 함수를 호출하지 않게 만들거나, 탈옥 감지 결과를 항상 정상으로 바꾸거나, 결제 검증 로직을 수정하는 방식이 가능합니다.

처음에는 TEAM_IDBundle ID를 조합해 해시값을 만들고 비교하는 방식을 생각했습니다. 하지만 이 값들은 앱의 실행 코드 자체를 검증하는 값이 아니기 때문에, 실제 코드 변조 여부를 판단하기에는 부족했습니다.

그래서 이번 글에서는 Mach-O의 __TEXT.__text 영역을 기준으로 앱 변조 가능성을 탐지하는 방법과 그 한계를 정리해보려고 합니다.


이 글에서 다룰 내용

이번 글에서는 다음 4가지를 중심으로 정리합니다.

1. __TEXT.__text가 무엇인지
2. __TEXT.__text 해시값을 어떻게 만들 수 있는지
3. 해시 기준값을 앱 내부에 어떻게 숨길 수 있는지
4. Apple에서 기본적으로 제공하는 무결성 보호는 어디까지인지

이 방식은 앱 변조를 완벽하게 막는 방법이라기보다, 변조 가능성을 탐지하고 서버 검증과 함께 활용하기 위한 보조적인 방어 전략에 가깝습니다.


1. iOS 앱 무결성 검증에서 __TEXT.__text를 보는 이유

iOS 앱의 실행 파일은 Mach-O라는 형식으로 되어 있습니다. Mach-O는 Apple 플랫폼에서 사용하는 실행 파일 포맷이며, 앱 실행 파일뿐 아니라 Framework, 동적 라이브러리도 이 형식으로 구성됩니다.

Apple 문서에서도 Mach-O는 실행 코드와 데이터를 메모리에 어떤 순서로 배치할지 정의하는 실행 파일 형식으로 설명하고 있습니다. Apple Mach-O Executable Format 문서

Mach-O 구조를 간단히 보면 다음과 같습니다.

Mach-O 파일 구조

┌─────────────────┐
│   Mach Header   │  ← 파일 메타데이터
├─────────────────┤
│  Load Commands  │  ← 세그먼트, 라이브러리 로드 정보
├─────────────────┤
│   __TEXT 영역    │  ← 실행 코드 + 읽기 전용 데이터
├─────────────────┤
│   __DATA 영역    │  ← 전역 변수 + 초기화된 데이터
├─────────────────┤
│   __LINKEDIT    │  ← 심볼 테이블 + 재배치 정보
└─────────────────┘

여기서 __TEXT 세그먼트는 실행 코드와 읽기 전용 데이터를 포함합니다. 그리고 __TEXT 안의 __text 섹션에는 실제 실행 명령어, 즉 기계어 코드가 들어갑니다.

따라서 앱의 주요 로직이 수정되거나, 보안 함수 호출이 제거되거나, 결제 검증 로직이 바뀌면 __TEXT.__text 영역의 바이트가 달라질 가능성이 있습니다.

이 영역을 해시로 만들어 비교하면 앱 실행 코드가 기존과 달라졌는지 확인할 수 있습니다.


2. 앱 실행 중 로드되는 Mach-O는 하나가 아니다

iOS 앱이 실행될 때 로드되는 Mach-O 이미지는 메인 앱 실행 파일 하나만 있는 것이 아닙니다.

예를 들어 앱 실행 중에는 다음과 같은 이미지들이 함께 로드될 수 있습니다.

[0] MyApp.app/MyApp
[1] /usr/lib/libobjc.A.dylib
[2] /System/Library/Frameworks/UIKit.framework/UIKit
[3] /System/Library/Frameworks/Foundation.framework/Foundation
[4] MyApp.app/Frameworks/PaymentSDK.framework/PaymentSDK
[5] MyApp.app/Frameworks/SecurityCore.framework/SecurityCore

UIKit, Foundation 같은 시스템 라이브러리까지 모두 검사할 필요는 없습니다.
보통 1차 검증 대상은 메인 앱 실행 파일입니다.

MyApp.app/MyApp

즉, 메인 앱 Mach-O의 __TEXT.__text 영역을 읽고 SHA256 같은 방식으로 해시값을 만든 뒤, 기준값과 비교하는 방식으로 접근할 수 있습니다.


3. Mach-O 정보 확인하는 방법

iOS 앱 무결성 검증 Mach-O 정보 확인하는 방법
iOS 앱 무결성 검증 Mach-O 정보 확인하는 방법

Xcode로 앱을 빌드한 뒤 터미널에서 .app 파일을 찾을 수 있습니다.

find ~/Library/Developer/Xcode/DerivedData -name "MyApp.app"

예를 들어 다음과 같은 경로가 나왔다고 가정합니다.

/Users/user/Library/Developer/Xcode/DerivedData/MyApp-abcxyz/Build/Products/Debug-iphoneos/MyApp.app

이제 앱 실행 파일에 대해 otool을 실행합니다.

otool -hv "/Users/user/Library/Developer/Xcode/DerivedData/MyApp-abcxyz/Build/Products/Debug-iphoneos/MyApp.app/MyApp"

구조는 아래와 같습니다.

otool -hv 앱번들경로.app/앱실행파일명

시뮬레이터 빌드라면 보통 Debug-iphonesimulator 경로가 나옵니다.

otool -hv ".../Build/Products/Debug-iphonesimulator/MyApp.app/MyApp"

실기기 빌드라면 보통 Debug-iphoneos 또는 Release-iphoneos 경로가 나옵니다.

otool -hv ".../Build/Products/Release-iphoneos/MyApp.app/MyApp"

Archive 파일에서 확인할 수도 있습니다.

cd ~/Library/Developer/Xcode/Archives
find . -name "MyApp.app"

예상 경로는 다음과 비슷합니다.

./2026-05-28/MyApp 2026-05-28, 10.30.xcarchive/Products/Applications/MyApp.app

경로에 공백이 있으면 반드시 따옴표로 감싸야 합니다.

otool -hv "./2026-05-28/MyApp 2026-05-28, 10.30.xcarchive/Products/Applications/MyApp.app/MyApp"

4. iOS 앱 무결성 검증 __TEXT.__text 해시값을 만드는 흐름

실제 구현 흐름은 다음과 같이 정리할 수 있습니다.

1. 현재 실행 중인 메인 Mach-O 이미지를 찾는다.
2. 해당 이미지의 __TEXT.__text 섹션을 찾는다.
3. __text 섹션의 시작 주소와 크기를 구한다.
4. 해당 메모리 영역을 SHA256으로 해시한다.
5. 앱 내부 또는 서버에 있는 기준 해시값과 비교한다.
6. 값이 다르면 앱 변조 가능성으로 판단한다.

그림으로 표현하면 아래와 같습니다.

앱 실행
  ↓
메인 Mach-O 이미지 확인
  ↓
__TEXT.__text 섹션 위치 확인
  ↓
메모리 바이트 읽기
  ↓
SHA256 해시 생성
  ↓
기준 해시값과 비교
  ↓
정상 / 변조 의심 판단

이 방식은 앱의 실제 실행 코드 영역을 기준으로 하기 때문에, 단순히 Bundle ID나 Team ID를 비교하는 것보다 코드 변조 탐지에 더 직접적입니다.


5. 앱 전체를 해시하지 않는 이유

처음에는 앱 번들 전체를 해시하는 방법도 생각할 수 있습니다.

하지만 앱 전체를 대상으로 하면 변수가 너무 많습니다. 코드가 바뀌지 않았는데도 해시값이 달라질 수 있기 때문입니다.

예를 들어 다음 요소들이 영향을 줄 수 있습니다.

1. Debug / Release 빌드 차이
2. Dev / Prod Scheme 차이
3. Archive 재생성 여부
4. 코드 서명 / 프로비저닝 차이
5. App Store 배포용 최종 처리
6. 앱 슬라이싱
7. 기기별 바이너리 차이
8. 포함 Framework 버전 차이
9. 리소스 파일 변경
10. 빌드 번호 변경

앱 전체를 해시하면 이미지, 리소스, 서명 정보, 빌드 산출물 차이까지 모두 반영될 수 있습니다.

그래서 전체 앱 번들보다는 실행 코드가 들어있는 __TEXT.__text 영역을 대상으로 하는 것이 더 현실적입니다.


6. 기준 해시값을 어디에 둘 것인가

가장 어려운 부분은 해시 계산 자체보다 기준 해시값 관리입니다.

예를 들어 앱 내부에 다음과 같이 기준값을 넣는다고 가정해보겠습니다.

let expectedHash = "abc123..."

문제는 이 값도 결국 앱 안에 들어간다는 점입니다.

공격자가 앱을 분석하면 기준 해시 문자열을 찾거나, 비교 로직 자체를 우회할 수 있습니다.

if currentHash != expectedHash {
    blockApp()
}

이런 단순 분기는 변조 대상이 되기 쉽습니다.

따라서 기준 해시값을 앱 내부에 평문으로 보관하는 방식은 피하는 것이 좋습니다.


7. 해시 기준값을 숨기는 방법

앱 내부에 기준 해시값을 넣어야 한다면 최소한 그대로 넣는 것은 피해야 합니다.

예를 들어 다음 방법을 고려할 수 있습니다.

1. 해시 문자열을 여러 조각으로 나눠 저장
2. 실행 시점에 조합
3. 단순 문자열 검색으로 찾기 어렵게 처리
4. 일부 값은 서버 응답과 조합
5. 비교 로직을 여러 위치에 분산
6. 실패 결과를 단순 if문 하나로 처리하지 않기

예를 들어 기준 해시를 한 문자열로 두지 않고 여러 조각으로 나눌 수 있습니다.

let p1 = "abc"
let p2 = "123"
let p3 = "def"

let expectedHash = p1 + p2 + p3

물론 이 방식만으로 강력한 보안이 되는 것은 아닙니다.
다만 평문 문자열 하나로 넣는 것보다는 분석 난이도를 조금 높일 수 있습니다.

실무적으로는 앱 내부에만 기준값을 두기보다 서버 검증과 함께 사용하는 방식이 더 안전합니다.


8. Apple 코드 서명과 앱 자체 검증의 차이

iOS는 기본적으로 강력한 코드 서명 구조를 가지고 있습니다. Apple 문서에서도 코드 서명은 앱이 개발자에 의해 생성되었음을 인증하고, 서명된 코드의 변경을 감지할 수 있는 기술이라고 설명합니다. Apple Code Signing 문서

즉, 정상적인 iOS 환경에서는 Apple의 코드 서명과 앱 샌드박스 구조가 앱 변조를 상당 부분 방어합니다.

하지만 여기서 주의해야 할 점이 있습니다.

Apple의 기본 보안 모델은 정상적인 실행 환경을 전제로 합니다. 탈옥된 기기, 런타임 후킹 도구, 디버깅 가능한 환경에서는 앱 내부 로직이 조작될 가능성이 있습니다.

따라서 앱 개발자가 별도로 수행하는 해시 검증은 Apple의 코드 서명을 대체하는 개념이 아닙니다.

정리하면 다음과 같습니다.

Apple 코드 서명:
앱이 신뢰할 수 있는 개발자에 의해 서명되었는지 확인하는 기본 보안 체계

앱 내부 해시 검증:
실행 중인 앱의 특정 코드 영역이 기대한 값과 같은지 추가로 확인하는 보조 방어 로직

둘은 같은 목적을 일부 공유하지만, 역할은 다릅니다.


9. TestFlight와 App Store 배포 시 주의할 점

로컬에서 만든 해시값을 앱 내부에 넣고 바로 비교하는 방식은 위험할 수 있습니다.

왜냐하면 로컬 Archive, TestFlight, App Store 배포 과정에서 최종 바이너리가 달라질 가능성을 확인해야 하기 때문입니다.

특히 다음 요소들이 영향을 줄 수 있습니다.

1. TestFlight 배포 과정
2. App Store 배포 과정
3. 앱 슬라이싱
4. 기기 아키텍처 차이
5. 코드 서명 처리
6. 포함 Framework 처리
7. 배포 채널별 빌드 차이

따라서 처음부터 해시값이 다르면 앱을 차단하는 방식으로 적용하면 정상 사용자까지 차단될 수 있습니다.

먼저 로그 수집부터 하는 것이 안전합니다.


10. 먼저 해야 할 실험

제가 생각하는 적용 순서는 다음과 같습니다.

1. 로컬 Archive 앱에서 __TEXT.__text 해시 계산
2. TestFlight 앱에서 런타임 해시 계산
3. App Store 배포 앱에서 런타임 해시 계산
4. 서버에 앱 버전, 빌드 번호, 해시값 저장
5. 동일 버전에서 해시값이 실제로 같은지 비교
6. 기기별, OS별, 배포 방식별 차이 확인

바로 차단하지 않고 먼저 로그로 남기면 정상 사용자에게 영향을 주지 않으면서 실제 배포 환경의 해시 패턴을 확인할 수 있습니다.

운영 적용은 다음 단계로 나누는 것이 좋습니다.

1단계: 해시 계산만 수행
2단계: 서버에 로그만 저장
3단계: 정상 배포 버전의 해시 패턴 수집
4단계: 비정상 해시 탐지 기준 설계
5단계: 민감 기능부터 제한 적용
6단계: 최종적으로 앱 차단 여부 결정

11. 보안 Framework만 따로 검증하면 충분할까?

보안 체크용 Framework를 따로 만들고 해당 Framework만 검증하는 방식도 생각해볼 수 있습니다.

예를 들어 다음과 같은 구조입니다.

MyApp.app/MyApp
MyApp.app/Frameworks/SecurityCore.framework/SecurityCore

그리고 SecurityCore.framework__TEXT.__text만 해시로 검증하는 방식입니다.

하지만 이 방법도 완전하지 않습니다.

Framework 자체가 변조되지 않았더라도 메인 앱에서 해당 Framework를 호출하지 않도록 바꿀 수 있기 때문입니다.

정상 앱에서는 다음처럼 호출할 수 있습니다.

SecurityCore.checkIntegrity()
SecurityCore.checkJailbreak()
SecurityCore.checkDebugger()

하지만 변조된 앱에서는 이 호출 자체가 제거될 수 있습니다.

따라서 보안 Framework 분리는 구조적으로 도움이 될 수 있지만, 그것만으로 충분하다고 보기는 어렵습니다.


12. 현실적인 앱 변조 탐지 전략

실무적으로는 하나의 보안 로직에 의존하기보다 여러 신호를 조합하는 방식이 더 현실적입니다.

예를 들어 앱에서는 다음 정보를 수집할 수 있습니다.

1. 앱 버전
2. 빌드 번호
3. Bundle ID
4. Team ID
5. __TEXT.__text 해시값
6. 탈옥 감지 결과
7. 디버깅 감지 결과
8. Frida, Cycript 등 런타임 후킹 도구 탐지 결과

그리고 서버에서는 다음과 같이 판단할 수 있습니다.

1. 정상 앱 버전인지 확인
2. 알려진 해시값과 일치하는지 확인
3. 특정 버전에서 허용되는 해시인지 확인
4. 비정상 해시가 반복되는 사용자 또는 기기 탐지
5. 결제, 포인트, 쿠폰 등 민감 기능 제한

앱에서 모든 것을 막으려고 하기보다, 앱은 탐지 신호를 만들고 서버가 최종 위험도를 판단하는 구조가 더 안전합니다.


13. iOS 앱 무결성 검증 적용 시 결론

이번 작업을 하면서 느낀 점은 명확합니다.

탈옥 감지 함수나 디버깅 체크 함수를 넣는 것은 필요하지만, 그것만으로 앱 변조를 막을 수는 없습니다.

공격자가 앱을 변조할 수 있다면 보안 체크 함수 호출을 제거하거나, 결과를 항상 정상으로 바꾸거나, 결제 검증 로직을 수정할 수 있습니다.

__TEXT.__text 해시 검증은 이런 문제를 탐지하기 위한 하나의 방법이 될 수 있습니다. 하지만 이 방식도 완벽한 해결책은 아닙니다.

한계는 다음과 같습니다.

1. 기준 해시값을 앱 내부에 안전하게 보관하기 어렵다.
2. App Store 배포 후 로컬 Archive와 해시값이 다를 수 있다.
3. 앱 내부 비교 로직 자체가 우회될 수 있다.
4. Framework만 검증해도 호출 자체가 제거되면 의미가 없다.
5. 정상 사용자 차단을 막기 위해 로그 수집이 먼저 필요하다.

따라서 제가 생각하는 현실적인 방향은 다음과 같습니다.

앱 내부 탐지 + 서버 로그 수집 + 서버 판단 + 민감 기능 제한

앱 내부에서는 해시 검증, 탈옥 감지, 디버깅 감지, 런타임 후킹 탐지를 수행합니다.
그리고 서버에서는 앱 버전별 정상 해시와 탐지 결과를 비교하여 위험도를 판단합니다.

모바일 앱 보안은 하나의 코드로 끝나는 문제가 아닙니다. 여러 방어 로직을 분산시키고, 서버 검증과 결합하며, 로그를 기반으로 점진적으로 강화하는 방식이 실무적으로 가장 현실적인 접근이라고 생각합니다.


iOS 앱 무결성 검증 정리

이번 글의 핵심은 다음과 같습니다.

1. 탈옥 감지 함수만으로 앱 변조를 막을 수는 없다.
2. 앱 변조가 가능하면 보안 체크 함수 호출 자체가 제거될 수 있다.
3. TEAM_ID와 Bundle ID 해시 검증은 코드 변조 탐지용으로 부족하다.
4. Mach-O의 __TEXT.__text에는 앱의 실행 코드가 들어있다.
5. __TEXT.__text 해시를 비교하면 코드 변경 여부를 탐지할 수 있다.
6. 기준 해시값을 앱 내부에만 두는 것은 위험하다.
7. App Store 배포 후 해시값 차이를 확인하기 위해 로그 수집이 먼저 필요하다.
8. 최종 판단은 앱이 아니라 서버에서 하는 구조가 더 안전하다.

현재 단계에서는 바로 차단 로직을 넣기보다, Xcode Archive 앱과 TestFlight 앱, App Store 배포 앱의 __TEXT.__text 해시값이 실제로 동일한지 먼저 비교해볼 계획입니다.

그 이후 서버 로그를 통해 정상 해시 패턴을 확인하고, 앱 변조 탐지 로직을 점진적으로 강화하는 방식으로 접근하는 것이 안전하다고 판단했습니다.


같이보면 좋은 글

https://hoonit.kr/ios-%ed%83%88%ec%98%a5-%eb%b0%a9%eb%b2%95-%eb%b0%8f-%ed%83%88%ec%98%a5-%ed%83%90%ec%a7%80jailbreak/