Android 앱을 웹페이지로 바꿔본 과정

9 hours ago 3
  • 여행 일정·숙박 정보·PDF를 보여주는 Travelbound가 사실상 웹에서 받은 콘텐츠를 표시하면서 추적과 여행 광고까지 포함해버려서, 같은 정보를 제공하는 웹페이지로 대체해 봄
  • Android 가상 기기를 루팅하고 HTTP Toolkit으로 트래픽을 가로채, 사용자 이름과 비밀번호를 URL에 결합한 API가 앱의 전체 콘텐츠를 JSON으로 반환한다는 사실을 확인함
  • Ruby 스크립트와 Cron으로 최신 JSON을 주기적으로 가져와 HTML을 생성하고, 광고 데이터는 제외한 채 일정과 PDF 등 필요한 파일만 노출함
  • 웹 버전은 복사·인쇄·저장·북마크·검색이 가능하고 더 다양한 기기에서 작동하며, 원본 앱의 추적 코드와 광고도 제거함
  • 원본 앱은 43MB에서 콘텐츠 다운로드 후 124MB까지 커지지만 웹페이지는 0.05MB이고 선택적 이미지도 35MB에 그쳐, HTML과 HTTP로 전달되는 문서형 콘텐츠에는 웹이 더 적합함

웹페이지로 충분했던 Travelbound

  • 아이가 갈 공연예술학교의 Disneyland 여행 일정, 교통편, 숙박 정보를 확인하려면 Travelbound 앱을 설치해야 했음
  • 앱의 핵심 콘텐츠는 웹으로 전달되는 텍스트·이미지·PDF 링크였음
  • 웹페이지와 구별되는 기능은 사용자에게 불리한 두 가지였음
    • Google Account와 연결된 추적 데이터를 개발자에게 전송함
    • 같은 여행사가 운영하는 다른 여행 상품을 inspirations라는 이름으로 광고함
  • 웹페이지라면 콘텐츠를 복사·인쇄·저장·북마크·검색할 수 있고, 거의 모든 기기에서 이용하며 잠재적으로 접근성도 높일 수 있음

Android 앱 트래픽 가로채기

  • Android Studio의 Virtual Device Manager에서 새 가상 기기를 만들고 adb shell이 작동하는지 확인함
  • rootAVD를 이용해 Android 33 가상 기기를 루팅함 ./rootAVD.sh system-images/android-33/google_apis_playstore/x86_64/ramdisk.img
  • 루팅은 인증서 고정(Certificate Pinning) 을 사용하는 앱이 중간자 프록시의 자체 서명 TLS 인증서를 신뢰하게 만드는 데 필요했음
    • 루팅하지 않으면 Travelbound 같은 앱이 인증서를 유효하지 않다고 판단해 통신을 거부함
  • 콜드 부팅 후 Magisk를 실행하고, 요청하는 앱에 su 접근을 자동 허용하도록 설정함
    • 자동 허용하지 않으면 HTTP Toolkit이 사용자 응답을 기다리지 않고 비특권 모드로 실행됐음
  • HTTP Toolkit으로 Android 가상 기기의 트래픽을 가로챔
    • 가짜 VPN 공급자를 설치해 기기 트래픽을 프록시로 전달함
    • Android 보안 제약 때문에 설치된 루트 CA 인증서는 수동으로 추가해야 했음
  • Play Store에서 Travelbound를 설치한 뒤, 잡음을 줄이기 위해 해당 앱의 트래픽만 프록시하도록 구성함

API 호출과 데이터 구조

  • 앱은 여행 그룹의 사용자 이름과 비밀번호를 이어 붙여 다음 형식의 API URL을 호출했음 https://travelbound.api.vamoos.com/api/itineraries/…}
  • 사용자 이름과 비밀번호는 여행 그룹 전체가 공유하는 자격 증명이었음
  • API 응답 JSON에는 앱이 표시하는 모든 콘텐츠가 들어 있었음
    • 여행 일정의 각 구간을 담은 배열
    • inspirations 광고 배열
    • 다른 섹션에서 참조하는 이미지 등의 파일을 담은 상호 참조 배열
  • 앱이 표시할 HTML 코드까지 데이터에 포함돼 있어, 콘텐츠 전달 방식 자체는 웹페이지와 유사했음
  • S3 이미지 URL은 만료 시간이 비교적 짧아 콘텐츠가 바뀌지 않더라도 JSON을 주기적으로 다시 가져와야 했음
    • 다른 방법은 이미지를 로컬에 캐시하는 것이며, 원본 앱은 이 방식을 사용하는 것으로 보였음

JSON을 HTML로 변환

  • Ruby 스크립트를 작성하고 Cron 일정에 따라 최신 JSON을 가져와 HTML 페이지를 생성하도록 구성함
  • 생성 과정에서 데이터 스키마의 overlayRows에 해당하는 inspirations 광고를 완전히 제외함
  • 웹페이지에는 다음 정보만 표시함
    1. 여행 일정 항목
    2. 광고나 일정에서 참조되지 않은 모든 파일
  • 두 번째 목록은 PDF 다운로드 링크를 한곳에 모으는 간단한 방법으로 활용함
  • 생성된 페이지는 기존 여행 그룹에 제공된 것과 같은 비밀번호로 보호함
  • 원본 JSON을 <details> 요소에 넣어, 기존 스크립트가 파악하지 못한 스키마 데이터가 나중에 등장하는지 확인할 수 있게 함

더 작고 유연해진 웹 버전

  • 웹페이지는 원본 앱만큼 화려하지 않지만 크기가 훨씬 작고, 웹의 기본 기능을 별도 구현 없이 제공함
  • 콘텐츠를 복사·인쇄·저장·북마크·검색할 수 있으며 더 다양한 기기에서 접근 가능함
  • 추적 코드와 광고를 제거해 원본 앱에 있던 두 가지 불필요한 기능도 없앰
  • 접근성을 정식으로 감사하지는 않았지만, 원본 앱에는 단순한 웹 버전보다 보조 기술로 사용하기 어려워 보이는 요소가 있었음

크기와 배포 방식 비교

  • 원본 Travelbound 앱은 최초 43MB이며 추가 콘텐츠를 내려받은 뒤에는 124MB까지 커짐
  • 대체 웹페이지는 0.05MB이고, 이미지를 선택적으로 모두 내려받으면 35MB가 추가됨
  • 여행 그룹에는 두 접근 방식을 모두 공유해 사용자가 앱과 웹페이지 중 원하는 방법을 선택할 수 있게 함
  • 일부 작업에는 앱이 적절하지만, 이미 HTML로 작성돼 HTTP로 전달되는 Travelbound 콘텐츠는 앱이 필요한 사례가 아님
  • 앱스토어 배포는 비용과 개발 부담을 늘리며, 이런 문서형 콘텐츠를 앱으로 제한하면 웹에 직접 게시할 때보다 이용 가능한 사람과 기본 기능이 줄어듦
Read Entire Article