본문 바로가기

이 포트폴리오의 원본은 https://cv.iruyo.com (심재빈) 입니다 · 출처 식별자 jbx-7f3a2e9b

← 기술 블로그

iOS·바이너리

앱스토어 IPA에 내 코드를 심는다 — 추출·프레임워크 인젝션·재서명, 그리고 그게 왜 무서운가

서명된 IPA를 풀어 Frameworks에 dylib를 끼우고 Mach-O load command 한 줄로 강제 로드한 뒤 재서명해 정상 실행시키는 전 과정. 보안 SDK 주입과 악성 주입이 같은 기술이라는 양날을 무결성 검증·재서명 탐지로 닫는다.

앱스토어에서 받은 앱은 이미 서명이 끝난 완제품이다. 그런데 그 IPA를 풀어 프레임워크 하나를 끼우고, 바이너리에 한 줄을 더한 뒤, 다시 서명하면 내 코드가 그 앱 안에서 돌아간다. 빌드 소스도, 원개발자 동의도 필요 없다.

이게 합법적 보안 후처리의 토대이자, 동시에 공격자가 앱을 가로채는 첫 수다. 같은 기술의 양면을 단계별로 풀어 본다.

IPA는 그냥 zip이다

IPA는 별도 포맷이 아니라 zip이다. 확장자만 바꿔 풀면 내부 구조가 그대로 드러난다.

unzip Some.ipa -d extracted
# extracted/Payload/Some.app/ 아래에 모든 게 있다

Payload/ 아래에 *.app 번들이 있고, 그 안에 실행 바이너리(Mach-O), Info.plist, 리소스, 그리고 Frameworks/에 동적 라이브러리들이 들어 있다.

Some.ipa (= zip) Payload/ Some.app/ Some 실행 바이너리 (Mach-O) Info.plist entitlement · 번들 메타 Frameworks/ ← 여기에 dylib를 넣는다

이 구조 어디에도 “여기 손대지 마라”를 물리적으로 강제하는 장치는 없다. 무결성을 지키는 건 오직 코드 서명뿐이다.

파일만 넣어선 아무 일도 안 일어난다

후처리로 코드를 심으려면 두 가지가 필요하다.

  • 하나, 내 코드를 담은 .dylib(또는 .framework)를 Frameworks/에 복사한다.
  • 둘, 호스트 앱이 실행될 때 그 dylib를 로드하게 만든다.

파일만 넣어 두면 아무도 호출하지 않으니 아무 일도 일어나지 않는다. 로드를 강제하는 가장 단순한 방법이 Mach-O의 load command다.

메인 바이너리의 헤더에 LC_LOAD_DYLIB(또는 weak 변형) 한 항목을 추가하면, dyld가 앱 기동 시 그 경로를 끌어와 매핑한다. otool로 기존 의존성을 들여다보면 추가하려는 한 줄이 어떤 형태인지 바로 보인다.

otool -L extracted/Payload/Some.app/Some
# Some:
#   /System/Library/Frameworks/UIKit.framework/UIKit ...
#   @rpath/MyInjected.dylib   ← 이 한 줄을 헤더에 새로 박는다

dylib의 +load나 생성자에 코드를 걸어 두면, 호스트 앱의 main보다 먼저 내 코드가 실행된다. load command 한 줄로 실행 진입점을 선점하는 셈이다.

이 선점이 양날이다. 공격자에게는 후킹·계측의 출발점이고, 보안 SDK 입장에선 보호 로직을 앱보다 먼저 깨우는 자리다.

한 줄이 깨뜨리는 것 — 그리고 재서명

load command를 추가하면 바이너리 바이트가 바뀐다. 바이트가 바뀌면 기존 코드 서명은 즉시 무효가 된다. iOS는 무효 서명 바이너리를 실행하지 않는다. 그래서 마지막 단계는 항상 재서명이다.

재서명은 한 번의 명령이 아니라 정해진 순서다.

  • dylib에 먼저 서명한다.
  • Info.plist의 entitlement를 실제 프로비저닝이 허용하는 범위로 조정한다.
  • 임베디드 프로비저닝 프로파일을 내 인증서 것으로 교체한다.
  • 번들 전체를 codesign으로 다시 봉인하고 IPA로 재압축한다.
codesign -f -s "Apple Development: me" \
  extracted/Payload/Some.app/Frameworks/MyInjected.dylib
codesign -f -s "Apple Development: me" \
  --entitlements ents.plist \
  extracted/Payload/Some.app

이 흐름을 자동화한 게 우리 쪽 재서명 도구(AppSuitSign)다. iOS의 .ipa뿐 아니라 macOS의 .pkg·.app까지 입력 포맷을 감지해 분기한다.

.pkg는 단순 압축이 아니라 빌드 산출물이라 .ipa와 달랐다. 메타데이터와 패키지 레벨 서명을 따로 보존해 줘야 했다.

막혔던 지점 — LIEF를 버린 이유

여기서 실제로 막혔던 지점을 남긴다. 처음엔 바이너리 조작 오픈소스 LIEF로 섹션을 통째로 옮기려 했다.

그런데 Fat Binary를 재저장하는 과정에서 문자열 영역 주소가 한 페이지 단위로 어긋났다. 참조가 깨진 문자열은 런타임에 엉뚱한 메모리를 가리켰다.

결국 라이브러리에 기대지 않았다. 문자열 참조 구조를 깨뜨리지 않는 무파괴 보정을 직접 구현하고, 재서명을 파이프라인에 정식 단계로 박아 풀었다.

같은 파이프라인이 합법 주입과 악성 주입으로 갈린다

이 전 과정 — 추출, 프레임워크 인젝션, load command 추가, entitlement·프로비저닝 교체, 재서명 — 을 우리는 합법 후처리로 쓴다.

고객사가 자기 앱에 정적 라이브러리를 직접 링크하고 빌드 설정을 손보던 작업을, IPA 업로드만으로 보안 SDK가 주입되게 바꾼 게 AppSuit Air다. 일반 상용 앱 IPA에도 동일하게 적용되는지 검증했다. 빌드 소스 없이 보호 기능을 바이너리에 내재화할 수 있다는 건 강력한 만큼, 똑같이 위험하다.

공격자도 정확히 이 순서를 밟기 때문이다. 정상 앱을 받아 추출하고, 자기 dylib를 끼우고, load command로 강제 로드하고, 개발자 계정으로 재서명한다. 그 안에 결제 우회·인증 탈취·데이터 유출 코드를 심은 뒤 사이드로딩으로 배포한다.

같은 도구가 보안 SDK도, 악성 페이로드도 주입한다 — 갈리는 건 도구가 아니라 무엇을 심느냐다.

같은 4단계 파이프라인 추출 dylib 인젝션 load command 강제 로드 재서명 보안 SDK 주입 (합법) · 보호 로직을 앱보다 먼저 기동 · 빌드 소스 없이 보안 내재화 · AppSuit Air 악성 페이로드 주입 · 결제 우회 · 인증 탈취 · 데이터 유출 코드 삽입 · 사이드로딩 배포

주입은 못 막는다 — 주입된 상태로 못 돌게 만든다

그래서 방어는 “주입을 못 하게” 막는 게 아니다. zip 구조와 Mach-O가 열려 있는 한 주입 자체는 못 막는다. 방향을 바꿔 주입된 상태로는 못 돌게 만드는 쪽이다.

앱은 기동 시 자기 바이너리와 로드된 이미지 목록을 점검해야 한다. 서명 주체가 원개발자가 아니거나, 예상치 못한 dylib가 끼어 있으면 비정상으로 판단한다.

정확히 이 검사가 우회당하지 않는 게 관건이다. 우리 SDK는 보호 프레임워크가 제거되거나 우회되면 앱 초기화 단계에서 멈추도록 무결성 메커니즘을 설계했다.

결국 핵심은 하나다. 서명은 “누가 마지막으로 봉인했는가”만 증명한다. 그게 원개발자인지를 앱 스스로 런타임에 확인하지 않으면, 봉인은 누구나 다시 칠 수 있는 도장일 뿐이다.