ReactNative main.jsbundle 암호화
소개
안녕하세요. STEALIEN에서 모바일 앱 보안 솔루션 AppSuit 시리즈의 iOS 파트 개발을 담당하고 있는 김상현입니다. AppSuit 시리즈는 iOS, Android OS 앱 외에도 React Native(이하 “RN”), Flutter, Unity 등의 다양한 환경의 앱 또한 지원하는 통합 보안 솔루션입니다.
본 포스팅에서는 앱 위변조 방지 및 난독화 기능을 제공하는 AppSuit Premium 제품이 RN 으로 제작된 iOS 앱의 보안성을 제고하는 방법에 대하여 설명하려고 합니다. 이해를 돕기 위해 iOS 파일 시스템과 IPA의 구조에 대한 설명으로 시작하도록 하겠습니다.
iOS 파일 시스템
iOS 환경의 앱에는 SandBox 라는 이름의 보안 모델이 적용되어 있습니다.
SandBox 란 커널 수준에서 앱의 권한들을 제한하는 보안 모델입니다. 제한하는 권한에는 시스템 자원의 접근, 네트워킹, 하드웨어 사용, 타 앱과의 통신 등이 포함됩니다.
간혹, iOS 앱을 사용하다보면 아래 이미지 같이 “앱에서 알림을 보내고자 합니다.” 와 같은 메시지를 볼 수 있는데 SandBox 에 의해 제한된 권한에 대하여 제한적 접근을 요청하는 메시지로 이해할 수 있습니다.
SandBox는 앱의 파일 시스템 권한 또한 제한합니다. 앱이 파일 시스템에서 접근할 수 있는 권한을 커널 레벨에서 제한하여 파일 시스템을 보호합니다.
SandBox의 보호 아래에서 iOS 앱의 데이터는 총 세가지 Container 로 분류하여 저장할 수 있습니다.
- Bundle Container
- Bundle Direcrory 가 이 곳에 포함되며 파일 시스템에서 한개만 존재합니다.
- 앱의 실행파일, info.plist, resource 파일들 등이 저장되는 Container입니다.
- IPA의 주요 파일들이 저장되는 공간으로, 읽기 만 가능합니다.
- Data Container
- 앱이 실행되는 동안 생성된 문서, 데이터, 캐시 정보 등의 컨텐츠들이 저장되는 Container 입니다.
- Documents, Library, temp, System Data 등의 디렉토리가 포함됩니다. 각 디렉토리마다 용도가 다르며 다른 특성을 지니고 있으므로 주의할 필요가 있습니다.
- Data Container 에 포함되는 Documents 디렉토리의 경우 파일 읽기, 쓰기가 모두 가능하며 Foundation 의 FileManager API 를 통해 코드 상에서 접근 또한 가능하므로 활용도가 높습니다.
- iCloud Container
- iCloud 와 동기화 시킬 데이터를 저장하는 Container 입니다.
- key value, iCloud Documents, CloudKit 등의 서비스가 있습니다.
-
Project / Build Settings / signing & Capabilities 에서 iCloud 서비스를 추가해야 사용 가능합니다.
앱 IPA 구조
이어서 IPA 에 관한 설명을 하도록 하겠습니다.
우선 IPA란 iOS 앱의 패키지 파일 형식 앱의 실행 파일과 info.plist, 이미지와 같은 리소스 파일들을 포함한 압축 아카이브 파일입니다. IPA 는 그 자체로 AppStore 에 배포가 가능하며 실제 디바이스에 직접 설치 또한 가능한 파일입니다.
IPA 파일이 어떻게 구성되어 있는지 더 자세하게 알기 위해 Terminal 에서 아래 명령어를 입력하여 IPA 를 뜯어보도록 하겠습니다.
unzip -q "IPA 파일 명"
압축을 풀면 Payload 디렉토리가 생성되고 그 아래에 앱의 패키지 파일이 존재합니다.
해당 파일을 “우클릭” - “패키지 내용 보기” 하여 IPA 의 내부 파일들을 확인하도록 하겠습니다.
설명한 단계들을 차례대로 진행하면 위 이미지와 같이 IPA 의 내부 파일들을 확인할 수 있습니다.
즉, IPA 의 디렉토리 구조를 정리하자면 아래와 같습니다.
StealPlateSwift.ipa/
├── Payload/
│ ├── StealPlateSwift.app/ # 앱 실행 파일과 관련 리소스
│ ├── StealPlateSwift # 앱의 바이너리 실행 파일
│ ├── Info.plist # 앱의 메타데이터
│ ├── Assets.car # 앱의 UI 리소스
│ ├── [email protected] # 앱 아이콘 이미지
│ ├── AppIcon76x...2x-ipad.png # iPad용 앱 아이콘 이미지
│ ├── Frameworks/ # 포함된 프레임워크들
│ ├── PlugIns/ # 확장 기능 플러그인
│ ├── _CodeSignature/ # 코드 서명 관련 파일
│ ├── embedded.mobileprovision # 배포 프로필
│ ├── PrivacyInfo.xcprivacy # 개인정보 보호 정보
│ ├── Gotham-Black.otf # 앱에서 사용하는 폰트 파일
│ ├── Gotham-Bold.otf
│ ├── Gotham-BookItalic.otf
│ ├── Gotham-Light.otf
│ ├── Gotham-Thin.otf
│ ├── Gotham-ThinItalic.otf
│ ├── Gotham-Ultralitalic.otf
│ ├── Gotham-XLight.otf
│ ├── Gotham-XLightItalic.otf
│ ├── loading_circle_gray.json # UI 애니메이션 데이터
│ ├── loading_circle_white.json
│ ├── Stealien.png # 앱에서 사용하는 이미지 파일
│ ├── sample.json # 샘플 JSON 데이터 파일
│ ├── Restaurants...ribution.json # 음식점 관련 데이터 파일 (추정)
│ ├── Base.lproj/ # 다국어 지원 관련 파일
│ ├── PkgInfo # 패키지 정보 (예전 iOS 앱 구조에서 사용됨)
내부 파일들을 보면 코드 서명 관련 파일, 앱 실행 파일, 설정 관련 파일, 리소스 파일, 데이터 파일, Framework 등 으로 분류할 수 있습니다. 여기서 Framework, PlugIns 등을 제외한 대부분의 파일들은 디바이스에 설치될 시 Bundle Container 에 설치됩니다.
이번에는 같은 방법으로 RN 로 빌드된 ipa를 뜯어보도록 하겠습니다.
RN 으로 빌드된 샘플 IPA 의 디렉토리 구조를 정리하자면 아래와 같습니다.
AwesomeProject.ipa/
├── Payload/
│ ├── AwesomeProject.app/ # 앱 실행 파일과 관련 리소스
│ ├── _CodeSignature/ # 코드 서명 관련 파일
│ ├── assets/ # 앱에서 사용하는 정적 리소스
│ ├── AwesomeProject # 앱의 실행 바이너리 파일
│ ├── boost_privacy.bundle # 추가 번들 파일 (보안 관련)
│ ├── embedded.mobileprovision # 앱 서명 및 프로비저닝 정보
│ ├── glog_privacy.bundle # glog 라이브러리 관련 번들
│ ├── Info.plist # 앱의 메타데이터
│ ├── LaunchScreen.storyboardc # 런치스크린 UI 파일
│ ├── main.jsbundle # JavaScript 번들 파일 (React Native 코드)
│ ├── PkgInfo # 패키지 정보 파일
│ ├── PrivacyInfo.xcprivacy # 개인정보 보호 관련 파일
│ ├── RCT-Folly_privacy.bundle # React Native RCT-Folly 관련 번들
│ ├── React-Core_privacy.bundle # React Native Core 관련 번들
│ ├── React-cxxre_privacy.bundle # React Native C++ Runtime 관련 번들
내부 파일들을 보면 코드 서명 관련 파일, 앱 실행 파일, 설정 관련 파일, 리소스 파일, 데이터 파일, Framework 그리고 main.jsbundle 등 으로 분류할 수 있습니다.
플랫폼이 다르기에 추가적으로 필요한 파일들을 제외하면 대부분의 파일들이 StealPlateSwift.ipa 와 비슷하게 구성되어 있음을 확인할 수 있습니다.
RN 환경 IPA 에서 추가되는 파일로는 main.jsbundle, [RN 관련 패키지].bundle 등이 있습니다.
[RN 관련 패키지].bundle 의 경우 해당 RN 앱의 빌드에 사용되는 라이브러리들의 데이터가 포함되어 있습니다.
본 포스팅의 주인공인 main.jsbundle 파일의 경우 다음 목차에서 더 자세히 다뤄보도록 하겠습니다.
main.jsbundle
main.jsbundle은 RN 앱을 개발하는데에 사용된 JavaScript 코드가 (TypeScript 로 개발했어도 마찬가지) 번들링되어 단일 파일로 묶인 결과물입니다.
RN 앱은 기본적으로 JavaScript로 작성되지만, 실제로는 여러 네이티브 환경(iOS/Android)에서 실행되어야 하므로 각 네이티브 환경에 맞는 적절한 변환이 필요합니다.
RN 앱은 iOS 네이티브 환경의 경우 AppDelegate 코드에서 main.jsbundle을 로드하여 실행하는 방법으로 JavaScript 코드를 실행합니다.
즉, main.jsbundle 파일이란 실제로 실행되는 소스코드 자체가 담긴 파일이라고 볼 수 있습니다.
이번엔 main.jsbundle 파일의 컨텐츠를 더 자세히 확인해보도록 하겠습니다.
어떤 실행 엔진을 사용하냐에 따라 내용물이 바뀔 수 있지만 본 포스팅에서는 JSC 엔진을 사용하는 IPA를 기준으로 분석을 진행하도록 하겠습니다.
먼저, Terminal 에 file 명령어를 입력하여 main.jsbundle 파일이 무엇으로 이루어져 있는지 확인해 보겠습니다.
출력된 결과에 의하면 main.jsbundle 파일은 ASCII text 로 이루어진 파일이라고 합니다.
ASCII text 로 이루어진 파일이라면 디컴파일러와 같은 별다른 툴 없이도 내용물을 확인할 수 있습니다.
다음으로는 cat 명령어를 입력하여 내용물을 출력해보겠습니다.
기대한대로 사람이 바로 읽을 수 있는 ASCII 문자열이 출력되었습니다.
다음은 main.jsbundle에 포함된 실제 데이터를 확인해보도록 하겠습니다.
위 이미지는 해당 IPA의 메인 화면입니다.
앱의 메인화면에서 “Step One” 섹션의 “App.tsx” 문자열을 cat 명령어로 출력한 내용물에서 검색해보도록 하겠습니다.
검색에 성공한 “App.tsx” 문자열을 확인할 수 있습니다. 그리고 뿐만 아니라 “App.tsx” 문자열이 속한 “Step One” 섹션, 그리고 그 아래의 “See Your Changes”, “Debug”, “Learn More” 섹션들 관련 정보들도 모두 평문으로 출력되어 있음을 확인할 수 있습니다.
main.jsbundle 파일은 앱 빌드시에 별도의 암호화 과정없이 그대로 노출되어 배포됩니다. RN 자체적으로 일부 난독화를 적용할 수 있으므로 그대로 분석하고 수정하는 것은 불가능하지만 위에서 확인한 문자열과 같이 그대로 노출되는 데이터 또한 존재하므로 확실한 보안성 강화를 위해 암호화 과정이 필요합니다.
main.jsbundle 의 암호화
ASCII text로 노출되는 main.jsbundle 파일을 배포하기 전에 암호화하고 앱이 단말기에서 실행되는 시점에 복호화하는 방법으로 보안성을 강화할 수 있습니다.
main.jsbundle은 앱 런타임에 실제로 실행되는 코드 정보를 담고 있으므로 앱 실행시 로드되는 시점과 로드하는 객체에 주의하여 암호화를 적용해야 합니다.
아래부터 RN 앱에게 main.jsbundle 암호화를 적용하는 기술을 차례대로 설명하겠습니다.
암호화, 복호화
먼저, IPA의 main.jsbundle 파일을 암호화하고, ResourceHash.plist 파일을 IPA에 추가합니다. ResourceHash.plist 파일에 대한 설명은 다음 단계에서 마저 하도록 하겠습니다.
IPA 수정을 마친 뒤에 메인 바이너리에 main.jsbundle 파일을 복호화하는 로직을 추가하여 배포합니다.
위 작업들을 수행하며 IPA 파일이 수정되면 기존 코드 서명이 무효화되기 때문에 재서명을 진행한 후에 배포합니다.
배포한 앱이 사용자의 단말기에 설치되면 암호화된 main.jsbundle 파일은 다른 resources 와 함께 Bundle Container 에 설치됩니다.
앱이 사용자의 단말기에서 실행되면 앱의 메인 바이너리에 삽입된 복호화 로직은 암호화된 main.jsbundle 파일을 복호화합니다. 그리고 복호화 완료된 데이터를 쓰기가 가능한 Data Container 에 NewMain 이란 이름으로 저장합니다. 이제 부터 AppDelegate는 main.jsbundle 대신 NewMain을 로드하여야 정상적인 실행이 가능하므로 AppDelegate가 NewMain 을 로드하게 만듭니다.
Resources 복사
RN 앱은 각 resources의 경로를 work-directory 를 기준으로 계산합니다. 그리고 work-directory는 AppDelegate 코드가 로드하는 main.jsbundle 이 속한 디렉토리를 의미합니다. main.jsbundle 대신 NewMain을 로드하게된 AppDelegate는 work-directory를 NewMain을 기준으로 계산하게 됩니다. 그러므로, NewMain 파일을 생성할때 resources 또한 함께 Data Container로 옮기는 작업을 수행하도록 구현합니다.
그리고 이 모든 작업들은 메인 바이너리의 AppDelegate 코드가 NewMain을 로드하기 전에 모두 완료되어야 합니다.
Data Container 는 읽기, 쓰기 모두가 가능한 디렉토리이며 또 iOS 에서 실행시에 자체적으로 무결성 검사를 진행하지 않습니다. 그러므로, 자체적으로 무결성 검사 과정 또한 추가하여 resources를 안전하게 관리해줘야 합니다.
복사된 Resources 의 무결성 검증
이미 위에서 언급했듯이, IPA를 배포하기 전에 ResourceHash.plist 라는 이름의 파일을 생성합니다.
ResourceHash.plist 파일은 모든 resources 각각의 해시 데이터를 담고 있습니다.
해당 IPA가 배포되어 사용자의 단말기에 설치되면 ResourceHash.plist는 main.jsbundle과 같은 경로인 Bundle Container에 저장됩니다.
앱이 실행되고 복호화 과정, resources 복사 과정이 모두 마무리되면, 마지막으로 복사된 resources에 대한 해시 값과 ResourceHash.plist에 담겨 있는 모든 원본 해시값을 각각 비교하여 복사된 resources에 대한 무결성 검사를 수행합니다.
“ResourceHash.plist” 의 신뢰성
그렇다면 마지막으로 ResourceHash.plist 파일의 무결성만 검증이 된다면 암호화된 main.jsbundle 을 위한 새로운 보안 체인을 완성할 수 있습니다.
ResourceHash.plist 파일 자신에 대한 무결성은 iOS에서 제공하는 _CodeSignature/CodeResources의 체크섬에 의해 검증됩니다.
또, ResourceHash.plist는 커널 레벨의 권한 보호를 받는 Bundle Container 에 위치하여 안전합니다. 메인 바이너리에 복호화 로직을 삽입함과 함께 탈옥 탐지, 위협 탐지 로직 또한 추가하면 과도한 권한을 가진 탈옥 기기들에 대한 차단 또한 가능하여 신뢰할 수 있습니다.
이로써 새로 추가된 파일들 또한 안전히 보호하는 새로운 보안 체인이 완성됩니다.
정리
본 포스팅에선 iOS의 파일 시스템에 대한 설명과 네이티브앱, ReactNative 앱 각각의 IPA 구조를 사전 지식으로 설명하였습니다. 그리고 ReactNative IPA 의 main.jsbundle 이 가지는 보안상의 문제점과 이를 안전히 보호하는 기술을 설명하였습니다.
본 포스팅에서 설명한 것과 같은 보안 기술들은 조금만 환경이 바뀌어도 무의미해지기도 합니다. 보안 솔루션을 개발하는 입장에서, 막으면 뚫고 뚫으면 막으며 기술을 발전시켜 나가다 보면 점점 더 견고한 보안성을 가진 기술이 완성되는 것을 보곤 합니다. 이런 검증 과정을 거친 기술들 또한 공개된 자료들이 많으니 소중한 정보 자산을 보호하기 위해서는 보호 기술들에 대한 지속적인 관심과 연구가 필요합니다.
AppSuit Series 에서는 이번 포스팅에서 설명드린 main.jsbundle 암호화와 같은 보안 기능 외에도 더 많은 보안 기능들을 제공하고 있으니 한번 확인해주시면 감사하겠습니다.
긴 글 읽어주셔서 감사합니다.