작은 메모장

어플리케이션 보안 이해 본문

KISA 사이버 보안 훈련/버그헌팅 실습 중급

어플리케이션 보안 이해

으앙내눈 2024. 7. 11. 17:07

모바일 앱 해킹 개요

모바일에서 실행하는 앱(Application)의 취약점을 찾아 악용하는 과정이다.

모바일 단말기가 점점 보급화되고 있는 추세에 따라, 대부분의 회사가 자사의 웹 사이트와 모바일 앱을 가지고 있다.

때문에 모의해킹, 버그바인티를 포함한 대부분의 해커는 모바일 앱 해킹을 같이 수행하는 경우가 많다.

단, 모바일 앱 해킹은 실행하는 앱을 해킹하는 것이지, 모바일 단말기를 해킹하는 것이 아니다.

모바일 단말기에 설치되는 어플리케이션의 취약점을 찾아 기업들의 자산 시스템에 있는 취약점을 찾는 것이 이 과정의 궁극적인 목적이다.

 

모바일 앱 해킹은 앱과 모바일 서버로 크게 두 가지의 공격 파트가 있다.

모바일 앱의 공격은 모바일 앱의 흐름을 조작하여 악용하는 것이다.

가령 모바일 게임 앱을 조작한다고 가정하면, 유료 게임 아이템을 무료로 구입하거나, 로그인 없이 앱을 이용하는 등 어플리케이션의 코드를 조작하여 허용되지 않은 행위들을 하는 형태다.

이와는 반대로 모바일 앱 서버의 공격은 서버의 취약점을 악용하는 것이다.

이 형태의 공격은 모바일이라는 환경에 집중되어 있기보단, 클라이언트-서버간의 취약점에 집중하여 있기 때문에, 웹 해킹에서의 공격 기법들을 거이 그대로 사용할 수 있다.

 

모바일 앱 취약점 환경

모바일 앱 취약점을 찾기 위해서는 당연하게도 모바일 단말기가 필요하다.

각 OS마다 방법은 다르겠으나, 전체적인 목적은 특정 과정을 통해 관리자 권한(Root)을 획득해야하는 것이 목표다.

Android 환경에서는 이 과정을 루팅(Rooting), iOS 환경에서는 이 과정을 탈옥(JailBreaking)이라고 한다.

이 과정을 거치지 않는다면, 공격할 앱의 프로세스 흐름을 마음대로 조작할 수 없으며, 특정 디렉토리에 접근하는 것 조차 거부된다.


모바일 권한 탈취 (Android)

Android 루팅은 그 특징답게 워낙 제조사와 기기 모델이 다양하기 때문에, 각각에 맞는 루팅법을 찾아서 설치하는 수 밖에 없다.

전체적인 과정은 Bootloader 해금 > 커스텀 리커버리 > 커스텀 롬 설치의 과정을 거친다.

 

통상적으로 부트로더는 데스크탑의 그것과 비슷한 역할을 하며, 안드로이드 운영체제 부팅 초기에 실행되는 코드들이다.

단, 모바일 부트로더는 데스크탑과 달리 임의로 넣은 펌웨어를 실행하지 못하게 제한하기 위해 기본적으로 잠겨 있는 것이 차이다.

그러나 루팅을 해야하는 입장에서는 가공되어있는 커스텀 리커버리를 설치해야하기 때문에 부트로더를 해금하는 것이다.

 

커스텀 리커버리는 Android OS의 유지 관리 기능을 제공한다.

백업/기기 초기화 등을 시행할 때, 전원이 꺼진 상태에서 특정 버튼을 눌러 진입하는 리커버리 모드를 떠올리면 된다.

이 리커버리 모드는 AOSP(Android Open Source Project)에서 제공하며, 순정 Android 기기들에게서 필요한 최소한의 기능만 사용하도록 만든 모드다.

커스텀 리커버리는 이 순정 리커버리 모드에 몇 가지 기능을 추가해 만든 개조판 리커버리다.

말로만 "몇가지" 기능을 추가했다고 했지, 사실 제한된 기능을 우회하기 위해 추가된 기능들이다.

통상적으로 fastboot mode를 이용해 설치하며, 이 방법이 안될 시 Odin 소프트웨어를 사용해 설치한다.

 

커스텀 롬은 이름 그대로 커스텀한 Android 기기 펌웨어이다.

그렇다면 무엇을 커스텀하였는가? 답변은 의외로 간단하다.

커스텀 롬은 순정 Android ROM 이미지에서 setuid 권한을 가진 su binary를 추가한 롬이다.

알다시피 안드로이드는 그 기반이 리눅스를 따르고 있기에, UNIX 기반의 setuid 특수권한 또한 사용할 수 있다.

루팅은 이를 이용하여 일반 사용자를 관리자(su)로 권한상승하여 루팅을 한 후, root 권한을 이용하는 것이다.

이 커스텀 롬은 상기 서술한 커스텀 리커버리를 이용하여 설치한다.

 

모바일 권한 탈취 (iOS)

iOS는 폐쇄된 환경답게 그 기기가 매우 한정되어 있으나, 역시 폐쇄된 환경답게 보안적으로 완성도가 높다.

따라서 iOS 탈옥을 하기 위해선, 해당 버전의 iOS 취약점이 발견되어야한다.

즉, 기종과 버전마다 탈옥이 될 수도, 불가능할 수도 있다.

 

탈옥은 크게 3가지 종류가 있다.

  • 완전 탈옥(Untethered Jailbreak) : 탈옥 후 재부팅에 관계 없이 탈옥 상태가 계속 유지됨
  • 반 탈옥(Tethered Jailbreak) : 탈옥 후 재부팅 시 탈옥 상태가 해제, 전원 유지상태 필요.
  • 준 반탈옥(Semi-tethered Jailbreak) : 탈옥 후 재부팅 시 탈옥 상태가 해제, 그 상태에서 특정 앱 구동 시 다시 탈옥 상태로 전환.

통상적으로 탈옥하는 방법은 3uTools 프로그램을 이용하면 된다.

해당 프로그램을 실행 후 모바일 기기를 연결하면 프로그램이 자동으로 인식하고, 그에 맞는 탈옥을 추천해준다.

탈옥을 선택하면 프로그램이 알아서 탈옥 과정을 실행해주며, 탈옥이 끝나면 Cydia와 Sileo이름의 앱이 설치되어 있다.

해당 어플리케이션은 일종의 마켓 어플리케이션으로, 애플 인증을 받는 애플 스토어와는 달리 개발자 위주의 앱, 모듈인 tweak을 다운받을 수 있는 어플리케이션이다.

해당 어플리케이션에 레포지토리 주소, 저장소 주소를 입력하면 해당 tweak을 다운받고 설치가 가능하다.

 

모바일 프록시 설정 (Android)

PC 환경에서는 프록시 툴을 사용하여 클라이언트-서버간 http 통신을 잡아낸다.

그러나 모바일 환경에서는 상당히 제한적이라, 따로 프록시 설정을 해야한다.

 

안드로이드 환경에서 프록시 설정은 와이파이 설정에서 할 수 있다.

통상적으로 모바일 기기는 와이파이를 이용하여 프록시를 설정하는데, 포트포워딩으로 연결된 PC Burp Suite에 패킷을 보낸다. PC에서 Burp Suite 패킷을 통과시키면, 패킷이 다시 모바일로 전달된 후 전송되는 구조다. 수신 과정도 마찬가지.

따라서, 연결한 와이파이의 프록시 메뉴에 컴퓨터 IP 주소와 리스너로 설정한 포트 번호를 적으면 PC로 패킷이 전송된다.

 

Https 통신은 알다시피 인증서가 필요하다.

문제가 있다면, 모바일에서 프록시를 쓸 때 Burp Suite를 사용하여 통신을 한다는 것.

PC에서야 Burp Suite가 자동으로 인증서를 등록하여 편하게 사용했지만, 모바일에서는 따로 등록해 주어야한다.

따라서 Burp Suite가 제공하는 인증서를 시스템 레벨로 설치해야하는데, 이 과정에 루팅이 필요하다(안드로이드 7이상).

 

이렇게 설정한 후, Adb shell을 이용하여 모바일 기기에 접속하면 된다.

 

모바일 프록시 설정 (iOS)

iOS의 프록시 설정또한 Android와 동일하게 포트포워딩 형태의 프록시를 사용한다.

설정법도 동일하게 와이파이 탭에 프록시 구성을 수동으로 전환 한 후, IP 주소와 포트 번호를 입력한다.

 

iOS의 인증서 설치는 Android보다 간단한데, 프록시 리스너에서 원하는 포트 번호를 열고 모든 IP 주소에서 수신하게 설정한다.

그 후, cast라는 인증서를 다운로드하여 설치하면 끝이다.

 

이렇게 설정한 후, 3uTools의 SSH 기능을 이용하여 접속한다.


어플리케이션 이해 (Android)

안드로이드의 어플리케이션은 Java 언어 기반으로 개발된 프로그램이다.

 

으레 그렇듯, Java 언어로 개발된 프로그램은 JVM 위에서 실행된다.

다시말해, Java 코드가 ByteCode로 변환된 후 JVM 위에서 실행되는 구조다.

JVM이 실행 환경을 제공하는 이러한 구조 때문에, 운영체제에 상관없이 동일한 소스코드로 실행 가능한 것이 Java의 장점이다.

하지만, 안드로이드 어플리케이션은 오직 안드로이드 환경에서만 사용가능한데, 이유가 무엇일까?

 

안드로이드가 어플리케이션 구조를 제작할 당시 JVM과 라이선스 문제가 있었다.

때문에 구동기를 JVM를 채택하지 못했고, 굉장히 유사한 구조의 Dalvik VM이라는 새 구동기를 제작하였다.

JVM과 Dalvik VM의 큰 차이점은, ByteCode를 실행하기 전, 네이티브 코드로 컴파일 한다는 것이다.

이 과정을 JIT(Just In Time)컴파일이라고 하며, 실행할 때 마다 컴파일하는 과정이다.

JIT 컴파일러의 한계점은 컴파일러가 동작하는 동안 하드웨어 전체적으로 상당한 부하가 발생, 배터리 시간이 저하되는 문제가 생긴다.

실행 직전에 코드 대부분을 RAM에 올려놓고, 실행하는 부분을 실시간으로 캐싱하여 실시간으로 그걸 꺼내오는 방식인데, 실행하는 앱이 많아질수록, 그리고 필요한 연산이 많아질수록 연산 효율과 저장 효율이 급격하게 떨어지는 구조였다.

 

이러한 문제를 해결하고자, 구글은 새 컴파일러를 개발하였고, 이것이 ART(Android Run Time)다.

ART 컴파일러는 AOT 컴파일러를 기반으로 제작된 것으로, 프로그램 최초 실행시가 아닌, 프로그램 최초 설치시 네이티브 코드로 컴파일 해놓고, 필요할 때마다 이를 꺼내쓰는 과정을 채택했다.

위 과정을 AOT(Ahead Of Time)컴파일이라고 하며, 최초 설치시에만 컴파일하는 과정이다.

ART 컴파일러는 JIT 컴파일러에 비해 압도적인 속도 개선과 공간 효율, 최적화를 보이나,

저장 공간을 1.5~2배 가량 더 차지하고, 설치 속도가 Dalvik VM에 비해 더 느리다는 단점이 있다.


안드로이드 실행 파일은 DEX(Dalvik Executable)의 확장자를 가진다.

그 이름에서 나와있듯, Dalvik VM에 호환되는 Android 실행 파일이다.

Android 런타임에서 최종적으로 실행되는 코드이므로, 기계어로 작성되어 있다.

 

Java 코드가 곧바로 dex 파일로 변환되어 실행되는 것은 아니다.

C/C++ 코드가 어셈블리어로 변환 된 후, 기계어로 실행되는 것처럼 Java 코드도 중간 언어가 있다.

그것이 바로 Smali 코드로 dex 코드를 위한 어셈블리 언어이다.

즉, Java 코드를 컴파일 하여 Smali 코드로, 이걸 다시 컴파일 하여 dex 코드로 변환되는 것이다. 이는 반대의 과정도 된다.


안드로이드 어플리케이션의 위변조 방지 장치는 코드 서명이 있다.

이 코드 서명은 앱 개발자가 자신을 인증하기 위해 자신의 코드 서명 인증서로 서명하는 과정이다.

 

원리는 다음과 같다.

개발자가 만든 코드 전문을 Hash화 하여 Hash값을 도출한다.

그 후, 개발자의 비밀 키와 Hash값을 암호화 하여, 코드 서명 값(Signed Hash)를 만든다.

이렇게 만들어진 서명 값은 배포 소프트웨어에 포함되며,

소프트웨어코드 서명 값, 그리고 개발자의 공개키인 코드서명인증서가 압축되어 APK 파일로 만들어지게 되는 것이다.


APK 파일은 Android Package의 줄임말로, 안드로이드 어플리케이션을 실행시키기 위한 패키지 파일이다.

그 용량과 기능에 따라서 조금 달라질 수 있지만, 기초적인 구조는 아래를 따른다.

  • (폴더) assets : 앱 실행에 필요한 자원(동영상 등의 큰 파일)을 관리하는 폴더
  • (폴더) res : 앱 실행에 필요한 자원(아이콘 등의 작은 파일)을 관리하는 폴더
  • (폴더) META-INF : 인증 서명(Sign)과 관련한 정보가 담겨있는 폴더
  • (폴더) libs : 라이브러리 파일(*.so)이 들어있는 폴더
  • (파일) AndroidManifest.xml : Android 정의 파일로 Android App 정보(접근권한, 버전 등)가 들어 있는 파일
  • (파일) resources.arsc : 파일 리소스 배열로, res 정보가 들어있는 파일
  • (파일) class.dex : Dalvik VM이 인식할 수 있도록, *.class 파일을 바이트 코드로 변환 시킨 소스 파일

해당 파일 중에서 가장 중요한 파일은 AndroidManifest.xml 파일로, 해당 안드로이드 어플리케이션의 프로필이라고 생각하면 된다.

여기에는 어플리케이션의 매우 중요한 정보들이 담겨져 있다.

가령, Frida 후킹 시 아주 중요하게 사용되는 고유 패키지 이름(ID)라던가,

안드로이드 앱에서 어떤 화면을 제공하고 있는 지를 보여주는 Activity라던가,

해당 어플리케이션이 필요로 하는 각종 권한 등이라던가,

사용하고 있는 외부 라이브러리 정보 등을 알 수 있다.

 

또, 안드로이드에서 중요하게 여기는 정보는 Activity 화면인데, 상기 설명하였듯 사용자와 상호작용을 하는 UI를 제공하는 화면이다.

쉽게 말해, Android App의 화면을 제공하는 것이다.

Activity는 하나의 클래스로 구성되어 있으며, 해당 화면에서 구현하려는 기능을 클래스 안에 메소드 형태로 구현한다.

 

Activity는 클래스 이므로, 사용자 조작에 따른 생명 주기가 존재한다.

기본적으로, 어플리케이션을 시작하면 onCreate() 메소드를 통해 클래스가 생성되고,

onStart()onResume() 메소드를 통해 어플리케이션이 실행된다.

해당 과정을 모두 끝마친 Activity 클래스는 자신의 기능을 실행할 준비가 끝났으며, 사용자에게 화면 제공과 함께 해당 화면에서 사용할 수 있는 기능을 지속적으로 제공한다.

 

이 때, 사용자가 "다른 화면으로 변경" 하거나, "다른 어플리케이션을 실행"하는 등의 <다른 액티비티가 최상단에서 보여지는> 경우, 인터럽트가 발생한 것으로 간주하고 onPause() 메소드를 통해 해당 클래스를 일시적으로 멈춘다. 이 경우, 기존 액티비티로 전환한다면 다시 onResume() 메소드를 통해 클래스가 실행된다.

 

그러나, 사용자가 "다른 어플리케이션으로 실행 변경"하는 등의 <액티비티가 완전히 보이지 않는>경우, onStop() 메소드를 통해 해당 클래스를 정지시킨다. 이 경우, 해당 어플리케이션을 다시 실행하면 onRestart() 메소드를 실행, onStart() 메소드부터 다시 실행한다.

 

만약, 사용자 혹은 시스템에 의해서 <액티비티가 종료>되는 경우, onDestory() 메소드를 통해 액티비티가 종료된다.

 

더 많은 종류와 기능 설명은 공식 문서를 확인하면 된다.


마지막으로 눈여겨 볼 점은 Broadcast Receiver이다.

안드로이드는 기기에서 이벤트가 발생하였을 시, 이를 안드로이드 시스템에 알려주는 기능을 가지고 있다.

이벤트는 여러 종류가 있는데, 충전기를 연결하였거나, 시스템콜이 발생하였거나, 메시지 수신 등의 각종 이벤트를 다룬다.

문제는 이런 이벤트가 너무 많다보니, 어플리케이션이 이 수많은 이벤트를 처리하면 너무 불필요한 작업을 수행하게 된다.

따라서, 어플리케이션은 Broadcast Receiver를 통해, 자신이 원하는 이벤트만을 수신하여 처리하는 작업을 한다.


어플리케이션 이해 (iOS)

iOS에서의 어플리케이션 패키지는 IPA(iOS Package App Store) 패키지 파일이다.

APK와 마찬가지로, IPA 패키지는 여러 파일이 모인 압축 파일로, 압축 해제하여 내용물을 볼 수 있다.

 

그 용량과 기능에 따라서 조금 달라질 수 있지만, 기초적인 구조는 아래를 따른다.

 

  • (폴더) Payload : 어플리케이션 데이터를 관리하는 폴더
  • (파일) iTunesArtwork : 어플리케이션 아이콘 PNG 이미지 파일
  • (파일) iTunesMetadata.plist : 개발자 이름, ID, 앱 식별자, 저작권 정보 등의 어플리케이션 기본 정보 저장 파일

iOS에 설치된 어플리케이션은 특정 디렉토리 구조에 저장된다.

크게 총 3가지가 있으며, Bundle Container, Data Container, iCloud Container의 공간에 저장된다.

 

Bundle Container는 어플리케이션의 디렉토리가 저장되는 공간이다.

즉, 어플리케이션의 핵심 바이너리가 저장되는 공간으로, 실행 파일이다.

각 어플리케이션은 번들 ID로 디렉토리 이름이 설정되며, 절대 경로는 버전마다 조금씩 달라질 수 있다.

해당 디렉토리에는 바이너리 파일 뿐 아니라, 소리, 이미지, 기타 리소스 파일도 저장된다.

또한, Info.plist라는 파일도 저장되는데, 이는 어플리케이션의 권한, 식별자 등 중요 정보를 저장하는 파일로 Androidmanifast.xml 파일과 비슷한 역할을 한다.

 

Data Container는 어플리케이션 실행에 있어 필요한 데이터들을 저장하는 디렉토리다.

다음과 같은 구조로 되어 있다.

  • Documents : 유저가 앱을 통해 생성한 문서, 데이터, 혹은 외부 앱을 통해 전송한 컨텐츠를 저장하는 디렉토리. 디렉토리 자체는 삭제가 불가능하며, 설정에 따라 직접 파일 추가 및 삭제 가능.
  • Library : 유저 데이터 파일 및 임시 파일을 제외한 모든 파일을 관리하는 디렉토리. 유저에게 노출되는 일이 매우 적으며, 앱의 기능이나 관리에 필요한 파일을 저장.
  • tmp : iCloud 드라이브 관련 / Files앱 / DocumentsPicker등에서 공유되는 파일들이 임시로 저장되는 디렉토리. 디렉토리와 파일을 다루는 제약이 없어 임의로 삭제, 복사, 이동이 가능하며, 시스템에 의해서 삭제될 수 있음.

iCloud Container는 iCloud를 사용하는 어플리케이션의 파일들을 저장하기 위한 디렉토리다.


plist(Property list) 파일은 iOS 어플리케이션에서 매우 중요한 파일 중 하나로, 앱 속성, 설정 데이터 등을 저장하는 파일이며, 대표적인 파일은 상기 설명했던 Info.plist다.

plist 파일은 크게 XML 포맷과 바이너리 포맷의 2 종류가 존재한다.

XML 포맷이야 쉽게 읽으면서 분석이 가능하나, 바이너리 포맷은 기계어로 작성되어 있어 plutil을 이용하여 XML 포맷으로 변경 후 분석해야한다.

 

plist 파일의 내용으로는 다음의 내용이 있다.

- CFArray, CFString 등 Core Foundation의 데이터 타입 정보

- 사용자의 개인정보, 인증 정보 등의 평문 정보

- 앱의 작동 방식을 바꿀 수 있는 정보

또한, Info.plist 파일의 중요 내용으로는 다음의 내용이 있다.

- CFBundleExecutable : 앱 실행 바이너리 파일 이름

- CFBundleIdentifier : 앱 식별자

- NSAppTransportSecurity : ATS(App Transport Security) 설정, 설정 시 HTTP 통신 불가


iOS 어플리케이션의 생명주기는 크게 5가지 상태로 나뉜다.

  1. Not running : 종료상태. 앱이 실행되지 않았거나, 완전히 종료된 상태.
  2. Inactive (Foreground) : 비활성상태. 앱이 실행되어 Foreground로 진입했지만, 아무 이벤트도 받지 않는 상태. 앱의 준비 및 상태 전환 과정에서 잠깐 머무는 단계.
  3. Active (Foreground) : 실행상태. 앱이 Foreground로 진입, 실행중이며 이벤트를 받고 있는 상태.
  4. Background : 대기상태. 다른 앱 전환, 홈 버튼으로 인해 앱이 백그라운드에 있는 상태. 일정 시간이 지나면 자동으로 Suspended 상태로 전환된다.
  5. Suspended : 휴면상태. 앱이 Background에 있으며, 아무 코드도 실행하고 있지 않은 상태. 메모리 상에는 올라가 있지만 코드를 실행하지 않고 있으며, 메모리 부족 시 자동으로 메모리에서 제거되어 Not running 상태로 전환.

어플리케이션 취약점 (Android)

안드로이드 어플리케이션에서 여러 설정들이 잘못되었을 때 발생할 수 있는 문제는 여러가지가 있으며, 대표적으로 데이터 유출, 권한 탈취 등이 있다.

 

Data Leak : Logging

로깅으로 인한 데이터 유출은, Logcat을 이용하여 어플리케이션의 로그를 탐색, 데이터를 추출하는 과정이다.

개발자들이 개발 과정 중, 테스트를 위해 로그를 남기고 정식 배포 시 로깅 코드를 제거하지 않아 데이터가 유출되는 취약점이다.

로깅에는 다양한 데이터를 테스트해본다. 사용자에게서 입력받는 아이디, 패스워드 등의 개인정보나, 어플리케이션 내부에서 오고가는 다양한 데이터를 로그로 남기기도 한다.

 

이 경우, logcat을 이용하면 각종 변수에 대한 로그를 확인할 수 있다.

이를 통해 사용자의 각종 정보를 유추 가능하며, 해당 정보로 크리덴셜 스터핑 공격을 추가로 진행할 수 있다.

 

Data Leak : 하드코딩

개발자들이 어플리케이션을 개발하다 보면, 여러 변수와 데이터를 처리하는 과정이 너무 길어져 테스트가 길어지는 경우가 많다.

때문에 하드 코딩을 통해 해당 과정을 생략하고 테스트를 하는 경우가 굉장히 많다.

여기서 로깅 취약점과 마찬가지로, 정식 배포시 하드코딩한 데이터를 제거하지 않아 데이터가 유출되는 취약점이다.

 

Data Leak : Data Storage

안드로이드 환경에서 데이터를 저장할 때 Shared preperance 인스턴스를 생성하여 저장하는 경우가 많다.

이렇게 저장한 데이터는 해당 어플리케이션 저장 경로에 xml 파일 형태로 저장된다.

만약 적절한 암호화 없이 Shared preperance 인스턴스로 저장했다면, 저장된 데이터 평문이 그대로 노출되는 취약점이 있다.

 

Data Leak : API Key

종종 하드코딩으로도 처리하기 귀찮을 정도로 긴 데이터가 존재하는데, API 키가 대표적인 사례다.

API 키의 경우, SSL 인증, 클라우드 접속, 외부 저장소 접근 등 다양한 곳에 사용되기 때문에, 그 종류도 상당히 다양하고 갯수도 상당히 많다.

따라서 상당히 입력하기 귀찮은 데이터는 따로 xml 데이터로 저장하여 필요할 때마다 불러오는 형태를 취한다.

배포 시 이 데이터를 따로 삭제하거나 처리해놓지 않았다면, 해당 API가 전부 노출되게 된다.

 

AWS Cognito 취약점

AWS는 Cognito 서비스를 제공하고 있는데, 이는 간단하게 구현할 수 있는 인증/인가 시스템이다.

로그인 시스템을 직접 만들 경우, 사용자 DB를 포함 인증/인가 기능도 구현해야한다.

AWS는 이러한 불편 사항을 Cognito를 통해 해소하고 있으며, 많은 서비스가 이를 사용중이다.

 

Cognito는 크게 User Pool과 Identity Pool로 구분된다.

User Pool은 인증의 역할을 한다.

해당 서비스를 이용하는 사용자가 맞는지 확인 후, JWT 토큰을 받아 인증을 받는다.

Identity Pool은 인가의 역할을 한다.

인증을 받고 해당 정보를 바탕으로 서비스에 대한 적절한 권한을 부여한다.

 

이 과정에서 Identity Pool에 제출하는 JWT 토큰을 이용하면, 임시 AWS 크리덴셜이 부여되는데, 이 임시 크리덴셜에 대한 권한이 너무 많으면 문제가 발생할 수 있다.

당장 아마존 서비스를 사용할 수 있는 권한이 너무 많이 부여되면, 악용할 수 있는 가능성이 높아진다.

 

Identity Pool에 제출하는 ID는 소스코드나 리소스에서 발견한다.

HTTPS가 아닌 HTTP 통신을 사용하는 서비스라면, 트래픽에서 발견 가능하기도 하다.

 

Deep Link 취약점

Deep Link란, 특정 링크를 클릭하면 모바일 어플리케이션의 기능이 실행되게 하는 기능이다.

정확히는, 특정 링크를 클릭하였을 시, 어플리케이션의 특정 Activity가 실행되게 하는 기능이다.

 

사실 Deep Link는 링크로 인해 앱이 호출되었기보단, 링크가 눌린 어플리케이션으로 인해 호출되는 경우에 가깝다.

때문에, Androidmanifast 파일에서 인텐트 필터를 설정하면, 이 인텐트를 통해 데이터가 전달되며, 데이터 태그를 통해 Deep Link를 설정할 수 있다.

 

문제는, 이 Deep Link로 넘어오는 요청을 잘못 처리하게 되면, 공격자가 만들어놓은 피싱 페이지로 리다이렉트 될 수 있다.

이 상황에서 공격자는 중간자 공격으로 세션/토큰을 탈취하거나 XSS 공격 또한 가능하다.

 

BroadCast Receiver 취약점

브로드캐스트 리시버는 특정 이벤트가 발생하였을 시 특정 채널을 통해 이벤트를 수신, 이를 처리하는 과정이다.

문제는, 이 이벤트 처리 과정을 안전하게 하지 않는다면, 브로드캐스트 이벤트만으로 타겟 어플리케이션의 행동을 이끌어낼 수 있다는 것이다.

 

따라서, 브로드캐스트 리시버 취약점은 브로드캐스트 메시지를 수신하고 싶은 채널을 직접 설정, 특정 명령을 해당 채널에 브로드캐스팅하여 타겟 앱이 악성 행위를 유도하는 방법으로 악용 가능하다.


어플리케이션 취약점 (iOS)

취약한 안드로이드 어플리케이션에 비해 iOS 어플리케이션은 취약점이 상당히 적은 편이다.

iOS 운영체제 자체 보안 기능이 뛰어나 악성 어플리케이션을 확용하기 쉽지 않기 때문이다.

또한, iOS에서 가능한 취약점은 안드로이드에서도 가능하니, 그 수준이 어느정도인지 짐작이 간다.

 

Data Leak : 하드코딩

안드로이드와 마찬가지로, 하드코딩 취약점이 존재한다.

어플리케이션 소스코드, 리소스 내에 개발 서버 주소나 크리덴셜, 인증서를 하드코딩하여 저장하는 경우, 취약점이 된다.

따라서 동일한 문제와 위협을 안드로이드와 공유한다.

 

URL Scheme 취약점

해당 취약점은 URL을 통해 앱을 특정 행위로 실행할 수 있는 취약점이다.

더욱 자세히는, URL을 통해 특정 어플리케이션에 로직을 분석, 취약한 코드를 통해 특정 행동을 취하도록 조작할 수 있는 취약점이다.

사실 해당 취약점은 취약점 패턴과 문제, 위협이 안드로이드의 Deep Link 취약점과 동일하다.

따라서 동일한 문제와 위협을 안드로이드와 공유한다.

 

UIWebView 취약점

UIWebView는 애플 사파리 앱에서 웹 컨텐츠를 보여주기 위한 요소이다.

애플 사파리 앱은 상당한 보안 수준을 가지고 있으나, 문제는 사파리가 가진 UIWebView 라이브러리는 JS 웹 해킹 공격에 취약한 문제가 있다.

따라서, XSS 공격이 들어오는 경우 사파리가 완벽하게 방어할 수 없어 위험성이 존재한다.


어플리케이션 로직 조작

루팅 혹은 탈옥을 통해 root 권한을 획득한 단말기에서 공격자는 앱의 흐름을 자유롭게 조작할 수 있다.

함수 후킹은 그 대표적인 예시로, 함수가 실행된 직후와 반환되기 직전에 임의의 코드를 삽입, 공격자 임의의 코드를 실행하도록 하는 조작방법이다.

 

논리적으로 취약한 부분을 찾아 함수를 후킹하여 공격하는 방식이기에,  어떠한 함수를 후킹할 것인지는 공격자의 선택에 따라 다양해진다.

가령, 클라이언트 측 인증 우회를 담당하는 함수를 후킹하여 로그인 성공 유무를 조작하거나,

특정 변수의 인자값을 조절하는 함수를 조작하여 원하는 데이터 수치를 변화 시키거나

개인정보 같은 크리덴셜을 다루는 함수를 조작하여 민감한 데이터를 빼오는 행위도 할 수 있다.


Frida 이해

Frida는 안드로이드를 타겟으로 한 DBI Framework (Dynamic Binary Instrumentation)의 일종이다.

DBI Framework란, 바이너리의 동작을 동적으로 조사하는 프레임워크로, 특정 프로그램의 흐름을 직접 개입하고 분석/조작할 수 있는 프레임워크다.

Frida는 안드로이드 프로세스의 바이너리를 동적으로 제어할 수 있는 프레임워크며, 이를 이용하여 어플리케이션 로직 조작을 구현할 수 있다.

 

Frida는 여러 독특한 특징을 가지고 있다.

  • Client/Server 구조
  • Javascript를 통한 바이너리 조작
  • 멀티 플랫폼 (Windows, Linux, iOS, Android 등)

Frida는 클라이언트-서버 구조로 이루어져 있다.

굉장히 독특한 방식인데, 이를 어떻게 구현하였는가.

 

Frida는 클라이언트와 서버를 다음과 같이 두고 있다.

함수를 후킹하여 조작하는 공격자의 PC를 클라이언트로 한다. 해당 클라이언트는 서버가 후킹하여 가져온 함수를 받고, 다시 서버로 전송할 수 있다.

모바일 디바이스에는 Frida 서버가 실행된다. 클라이언트에서 어플리케이션에 대한 요청이 들어올 시, 서버 위에서 어플리케이션이 실행되며 제어를 위해 Frida 에이전트가 삽입되어 실행된다.

이렇게 삽입된 Frida 에이전트는 디바이스에서 구동되는 Frida 서버와 지속적으로 연결되며, 클라이언트 요청 처리 및 어플리케이션 함수 후킹 등의 역할을 한다.

 

이런 구조 때문에, Frida를 실행하기 위해서는 안드로이드 단말에서 Frida Server를 실행시켜야 한다.

Frida는 해당 서버 파일을 Github로 공유하고 있으며, 각각의 디바이스 환경에 맞게 서버를 실행하면 된다.

안드로이드의 경우, adb_shell을 통해 서버 파일을 이동, 설치하면 된다.

iOS의 경우 deb 확장자의 서버 파일을 직접 설치해도 되고, Cydia를 통해 설치하는 방법도 있다.


Frida Agent Injection은 파이썬과 JS로 이루어진다.

보통 어플리케이션에 에이전트를 주입시키는 과정은 파이썬으로, 후킹한 함수를 수정하는 과정은 JS로 이루어진다.

즉 Frida는 파이썬으로, Frida 에이전트는 JS로 동작하는 것이다.

 

사실 에이전트 주입 자체는 adb와 Frida 콘솔을 이용하여 수동으로 할 수 있다.

그러나 과정이 상당히 복잡하고 귀찮은데다, 연결시키는 작업 자체는 단순 반복이기 때문에 파이썬 스크립트를 통해 이 작업을 자동으로 하는 것이다.


아래는 Frida 에이전트 주입하여 에이전트로부터 데이터를 수신하는 스크립트의 예시다.

더보기
=[Frida]=

import frida, sys
import argparse

def on_message(message, data):
	if message['type'] == 'send':
    	print(message['payload'])
    elif message['type'] == 'error':
    	print(message['stack'])

js_script = """
console.log("[+] JS Inject!");
"""

# Package Name Here
package_name = "com.android.sample"

device = frida.get_usb_device()
p1 = device.spawn([package_name])

process_session = device.attach(p1)

script = process_session.create_script(js_script)
script.on('message',on_message)
script.load()

device.resume(p1)

sys.stdin.read()
=[Frida Agent]=

send("[*] Hook Method : " + hook_method);

상기 코드는 다음과 같은 순서로 동작한다.

  1. 원하는 어플리케이션의 패키지 이름을 하드코딩으로 입력받는다.
  2. Frida를 사용하여 디바이스의 핸들러를 확보, spawn 함수로 해당 패키지 이름에 해당하는 프로세스를 가져온다.
  3. 가져온 프로세스를 attach 함수를 이용하여 세션을 생성한다.
  4. 해당 세션을 사용하여 JS 스크립트와 메시지를 전송한다.
  5. 전송한 JS 스크립트와 메시지를 실행한다.
  6. 파이썬 스크립트가 데이터를 받아오기 전 종료되지 않게 EOF까지 대기한다.

가장 핵심이 되는 부분은 spawn함수와 attach함수이다.

이는 파이썬으로 Frida 에이전트를 삽입하는 두 가지 방식으로, 각각의 방법에 차이가 존재한다.

  • Spawn 함수는 앱 프로세스를 메모리에 적재한 뒤 실행 전, 스크립트를 로드하고 실행시키는 방식이다.
    해당 함수는 보통 앱프로세스 시작에 동작하는 루팅/탈옥을 탐지하거나, 무결성 검증 우회에서 사용할 수 있다.
    보통 다음과 같이 사용한다.
    더보기
    ...

    device = frida.get_usb_device()
    p1 = device.spawn(["com.example.com"]) # process Spawn

    process_session=device.attach(p1)

    # Script Load
    script = process_session.create_script(get_script(args.script))
    script.on('message', on_message)
    script.load()

    # process Resume
    device.resume(p1)

    ...
  • Attach 함수는 앱 프로세스가 실행 된 후, 실행하고 있는 프로세스에 스크립트를 Inject 시키는 방식이다.
    해당 함수는 앱 프로세스에서 동적으로 로딩되는 라이브러리 함수를 후킹할 때 사용할 수 있다.
    보통 다음과 같이 사용한다.
    더보기
    ...

    device = frida.get_usb_device()

    process_session = device.attach(int(args.pid)) # process Attach

    # Script Load
    script = process_session.create_script(get_script(args.script))
    script.on('message', on_message)
    script.load()

    ...

이와는 반대로, Frida 에이전트 주입하여 에이전트에게 데이터를 수신하는 스크립트의 예시다.

더보기
=[Frida]=

...

self.script_list[intercept_pid][func_name].post({'type':'input','payload':strHex})

...
=[Frida Agent]=

...

var op = recv('input', function(value){
	user_write_data = value.payload;
});

op.wait();

상기 코드는 동작의 핵심 부분만 기술한 것이다.

self.script_list에서 프로세스 pid와 함수 이름을 기술하면, 원하는 프로세스의 함수를 선택하는 것이 가능하다.

해당 상태에서 post 함수를 이용하여 전송 형태와 함께 전달한다.

 

이후, Frida 에이전트에서 recv함수를 이용하여 해당 값을 받아오면, 그 값을 프로세스의 원하는 변수에 넣은 후 이를 wait 함수로 다음 데이터 주입을 기다리게 한다.


어플리케이션 함수 후킹 (Android)

앞서 설명하였듯, 안드로이드는 자바 언어로 개발된다.

이를 분석하는 Frida도 자바 VM을 지원하며, 사용되는 후킹 함수 또한 Java 펑션을 사용하게 된다.

더보기
Java.perform(function () {
	var MainActivity = Java.use('com.example.MainActivity');
    
    // Button is clicked
    var onClink = MainActivity.onClink;
    onClink.implementation = function (v) {
    	send('onClink');
        
        // Call handler
        onClink.call(this, v);
        
        // Set values after handler
        this.m.value = 0;
        this.n.value = 1;
        this.cnt.value = 999;
        
        // Logging
        console.log('Done:' + JSON.stringify(this.cnt);
    };
});

해당 코드는 Java.perform을 통해 Java VM 안에서 실행되도록 하는 코드로, 함수 후킹 코드가 적혀있다.

Java VM에서 실행하는 첫 코드는 Java.use로, 함수를 후킹하고 싶은 타겟 프로세스의 클래스를 기입하여 그 클래스의 객체를 MainActivity 변수에 저장한다.

이렇게 가져온 MainActivity 객체의 onClink 메소드를 또 변수에 저장하고, 해당 변수를 implementation을 통해 해당 메소드를 새롭게 재정의한다. 가져온 객체의 메소드 내용을 원하는대로 덮어씌우는 것이다.


자바에는 오버로딩이라는 개념이 있다.

쉽게 말해, 똑같은 메소드를 인자의 종류와 갯수에 따라 다르게 선언할 수 있는 개념이다.

우리가 후킹을 할때 메소드의 이름을 기준으로 선택을 하는데, 오버로딩이 되어 있는 경우에 Frida에서 에러가 날 수 있다.

이 경우, Frida는 overload 옵션을 제공하여 해당 상황에 대비를 해두었다.

더보기
Java.perform(function () {
	var StringBuilder = Java.use('Java.lang.StringBuilder');
    
    var ctor = StringBuilder.$init.overload('java.lang.String');
    ctor.implementation = function(arg) {
    	var partial = '';
        var result = ctor.call(this, arg);
        if (arg != null) {
        	partial = arg.toString().replace('\n', '').slice(0,10);
        }
        console.log('new StringBuilder("' + partial + '");');
        return result;
    };
    console.log('[+] new StringBuilder(java.lang.String) hooked');
});

해당 코드는 상기 설명한 코드와 유사한 구조를 가지고 있으나, 오버로딩을 처리를 해두었다.

선택된 클래스의 객체에서 메소드를 선택할 때, overload('java.lang.String')을 통해 문자열 데이터만을 입력받는 메소드만을 선택하겠다고 선언한 후, implementation을 통해 재정의를 하였다.


상기 설명한 후킹은 생성되어 있지 않은 객체를 생성하면서 후킹하는 방식의 코드다.

그러나, 이미 생성되어 있는 객체를 후킹하는 방법도 Frida는 지원한다.

더보기
...
var instances_array = [];
function callSecretFun() {
	Java.perform(function () {
    	if (instances_array.length == 0) {
        	Java.choose("com.example.test.activity", {
            	onMatch: function (instance) {
                	console.log("Found instance: " + instance);
                    instances_array.push(instance)
                    console.log("Result of secret func: " + instance.secret());
                },
                onComplete: function () { }
            });
        }
        else {
        	for (i = 0; i < instances_array.length; i++) {
            	console.log("Result of secret func: " + instances_array[i].secret());
            }
        }
    });
}

생성되어 있는 객체를 후킹할 때는 choose를 사용한다.

choose를 사용할 때는 인자를 여러개 받는데, 이때 프로세스 객체만 고르는게 아니라 객체 안의 메소드까지 한번에 선택한다.

발견 시의 행동은 다른 인자로 설정하며, 해당 과정에서 instance 변수를 이용하여 객체를 제어할 수 있다.


어플리케이션 함수 후킹 (iOS)

iOS에서 함수를 후킹하기 위해선 interceptor를 사용한다.

interceptor는 여러가지 매개변수로 함수 또한 받아들이는데, 문제는 이 함수의 주소, 즉 포인터 값을 받아들인다.

따라서 이 포인터 값을 따로 구해야하는데, 이는 Module.getExportByName()으로 포인터를 가져온다.

더보기
var hookPtr = Module.findExportByName("iOS_App_Binary", "Binary_Function_Name");

Interceptor.attach(hookPtr, {
	onEnter: function(args){
    	send("Target Function called!");
    },
    onLeave: function(retVal){
    	send("Target Function Leave");
        
        send("[=] return Origin Value : " + retVal);
        retVal.replace(1);
        send("[=] return Modified Value : " + retVal);
    }
});

iOS는 앞서 이야기하였듯 상태 변환에 따른 메소드를 구현하는 방식이다.

따라서, Frida로 객체를 후킹 시 onEnter, onLeave 등 상태 변환에 따라 메소드를 구현해야한다.

해당 코드는 특정 함수의 리턴 값을 onLeave 상태에서 변환하는 코드다.


이전 코드의 경우 함수의 네이티브 주소를 알고 있다는 가정으로 실행되는 코드다.

그러나 간혹 함수의 네이티브 주소를 모르는 경우가 존재하며, 이 경우 수동으로 직접 알아내야한다.

네이티브 주소, 함수 오프셋을 알아내는 방법은 메모리에 올라간 프로세스를 IDA Tool 등을 통해 알아내고, 그 주소를 적으면 된다.

더보기
var base_addr = Module.findBaseAddress('normalApp');
var func_addr = base_addr.add(0x123456);

Interceptor.attach(func_addr, {
	onEnter: function(args){
    	console.log("func Called!");
    },
    onLeave: function(ret){
    	console.log("func Leave!");
    }
});

 

'KISA 사이버 보안 훈련 > 버그헌팅 실습 중급' 카테고리의 다른 글

Bug Bounty 사례  (0) 2024.07.14
Metasploit  (0) 2024.07.13
보안 기술 우회  (0) 2024.07.12
향상된 타겟 정보 수집  (0) 2024.07.01
버그 헌팅이란?  (0) 2024.07.01