본문 바로가기

Mobile App Hacking : Android/Frida 탐지 우회

모듈 검사 우회

필자가 개발한 TodoList 앱의 Frida 모듈 검사 기능은 앱 프로세스 내에 Frida 관련 모듈이 로드되어 있는지 검사한다.

Frida는 앱 프로세스 내 메모리를 간편하게 후킹하기 위해 내부적으로 자체 모듈을 프로세스 내에 로드한다.  따라서 프로세스의 메모리 맵을 조회했을 때 Frida 관련 모듈이 존재한다면 Frida가 사용 중일 확률이 높다.


이번 포스팅에서는 TodoList 앱의 Frida 모듈 검사 기능의 소스 코드를 살펴보면서 Frida 탐지 기법 중 앱 프로세스 내의 Frida 모듈을 탐지하는 로직과, 이를 우회하는 스크립트를 구현해 보았다.

 

탐지 로직

checkModule 함수 분석

public boolean checkModule() {

    // **현재 프로세스의 메모리 맵 정보를 담고 있는 파일 경로**
    // /proc/self/maps:
    // - 현재 앱 프로세스에 로드된 라이브러리(.so), 메모리 영역 정보를 확인 가능
    // - Frida가 attach되면 frida-agent.so 등이 여기에 나타남
    File f = new File("/proc/self/maps");

    try {
        // 파일을 읽기 위한 입력 스트림 생성
        FileInputStream fio = new FileInputStream(f);

        // **파일 전체 내용을 문자열로 누적**하기 위한 객체
        StringBuilder resultStringBuilder = new StringBuilder();

        // BufferedReader를 통해 한 줄씩 읽기 위한 준비
        // InputStreamReader: 바이트 스트림 -> 문자 스트림 변환
        try (BufferedReader br = new BufferedReader(new InputStreamReader(fio))) {

            String line;

            // 파일 끝까지 한 줄씩 읽는다
            while ((line = br.readLine()) != null) {

                // 읽은 각 줄을 StringBuilder에 추가(파일 내용 문자열로 누적)
                // "\\n"을 붙여 **원본 파일 구조 유지**
                resultStringBuilder.append(line).append("\\n");
            }

            // 명시적으로 스트림 닫기 (try-with-resources라 사실 자동 close됨)
            fio.close();

        } catch (IOException ignore) {
            // 파일 읽기 중 예외 발생 시 무시
            // (탐지 로직 실패해도 앱 크래시 방지 목적)
        }

        /*
	      ===================== 핵심 탐지 로직 =====================
        **전체 문자열에서 "frida"라는 문자열이 포함되어 있는지 검사**
        - 포함되어 있으면 -> Frida 관련 라이브러리 로드됨 -> true 반환
        - 포함되어 있지 않으면 -> 정상 상태로 판단 -> false 반환
        */
        return resultStringBuilder.toString().contains("frida");

    } catch (FileNotFoundException ignore) {
        // /proc/self/maps 파일 접근 실패 시 예외 무시
        // (보통은 존재하지만, 환경에 따라 예외 처리)
    }

    // 예외 발생 시 기본적으로 false 반환 (탐지 실패로 간주)
    return false;
}
  • checkModule 함수는 현재 프로세스의 메모리 맵 정보를 가지고 있는 /proc/self/maps 파일에서 frida 문자열의 여부를 검사한다.
  • /proc/self/maps 파일은 보편적으로 어느 공유 라이브러리들이 함께 로드되어있고, 베이스 주소를 찾기 위해 이용한다.
  • 일반적으로 Frida를 통해 앱이 실행되면 Frida 서버는 앱 프로세스 내의 frida-agent-{arch}.so 라이브러리를 로드한다.

따라서 Frida를 통해 실 행된 앱은 반드시 메모리 맵 내에 "frida" 라는 이름이 포함된다.

 

Frida 사용 시:

frida-agent-xxx.so

같은 문자열이 반드시 포함된다.

 

Frida 모듈 탐지는 이와 같이 앱 프로세스의 메모리 맵 파일을 참조하여 Frida가 함께 실 행 중인지 탐지한다.

 

1. File 객체로 /proc/self/maps 파일에 접근한다.

File f = new File("/proc/self/maps");

 

2. FileInputStream 클래스와 BufferedReader 클래스를 이용해 해당 파일의 내용을 모두 읽어온다.

FileInputStream fio = new FileInputStream(f);
StringBuilder resultStringBuilder = new StringBuilder();
try (BufferedReader br
             = new BufferedReader(new InputStreamReader(fio))) {
    String line;
    while ((line = br.readLine()) != null) {
        resultStringBuilder.append(line).append("\\n");
    }
    fio.close();
}

 

3. 파일 내용 내에 "frida" 라는 문자열이 존재하는지 검사한다.

만약 문자열이 있다면 Frida 모듈이 실행중인 것으로 판단하여 True를 반환하고, 문자열 이 없다면 False를 반환한다.

    return resultStringBuilder.toString().contains("frida");

 

탐지 우회

Frida 서버를 실행한다.

generic_x86_64_arm64:/data/local/tmp # ./frida-server-16.1.3-android-x86_64

설정 확인

Frida 모듈 검사 우회 아이디어

public boolean checkModule() {
    File f = new File("/proc/self/maps");

    try {
        FileInputStream fio = new FileInputStream(f);
        StringBuilder resultStringBuilder = new StringBuilder();

        try (BufferedReader br = new BufferedReader(new InputStreamReader(fio))) {
            String line;

            while ((line = br.readLine()) != null) {
                resultStringBuilder.append(line).append("\\n");
            }

            fio.close();
        } catch (IOException ignore) {
        }

        return resultStringBuilder.toString().contains("frida");
    } catch (FileNotFoundException ignore) {
    }

    return false;
}

checkModule 함수 반환값 조작

  • checkModule 함수의 반환 값은 Frida 모듈 탐지 여부를 의미한다.
  • 따라서 해당 함수가 항상 False를 반환하도록 후킹해준다면 Frida 관련 포트가 열려 있더라도 이를 탐지되지 않은 것처럼 조작할 수 있다.

BufferedReader.ReadLine 함수 반환값 조작

  • 이 외에도 fridaDetector.checkModule 함수가 내부적으로 사용하는 BufferedReader.readLine 함수를 후킹하여 반환 값 내에 "frida"라는 문자열이 포함되어있는지 여부를 조작할 수 있다.
  • 이를 통해 메모리 맵 내에서 Frida 모듈이 로드되지 않은 것처럼 보이도록 할 수 있다.

참고 : 스트림과 BufferedReader 개념

https://hanadoescoding.tistory.com/25

 

[JAVA] BufferedReader 사용 이유와 과정 (read, 스트림(Stream), 버퍼(Buffer))

단계별로 알아보는 Byte → Char → String 입력 받기(System.in,read() , 스트림(Stream), 버퍼(Buffer))  예전에 백준을 풀면서 수행시간이 빠르다는 이유로 원리도 모르고 사용했던 기억이 있어 이번 기회

hanadoescoding.tistory.com

 

우회 스크립트 작성

function modifyCheckModuleRet(){
    Java.perform(function() {
        var FridaDetector = Java.use("com.example.todolist_for_frida.FridaDetector");
    
        FridaDetector.checkModule.implementation = function() {
            return false;
        };
    });
}

function modifyReadLineRet(){
    Java.perform(function() {
        var File = Java.use("java.io.File");
        var constructor = File.$init.overload('java.lang.String');
    
        constructor.implementation = function(pathname) {
            if (pathname == "/proc/self/maps") {
                var BufferedReader = Java.use("java.io.BufferedReader");
                var readLine = BufferedReader.readLine.overload();
    
                readLine.implementation = function() {
                    var ret = readLine.call(this);
                    if (ret.includes("frida"))
                        ret = "";
                    return ret;
                };
            }
            return constructor.call(this, pathname);
        };
    });
}

modifyCheckModuleRet();
modifyReadLineRet();

modifyCheckModuleRet 함수 : fridaDetector.checkModule 반환 값 변조

먼저 Java.use 함수를 사용해 Frida 내에 서 FridaDetector 클래스에 접근할 수 있도록 Wrapper를 제공받아 그것을 통해 클래스 내의 checkModule 함수에 접근할 수 있다.

Frida에서 자바 단 함수를 후킹할 때에는 일반적으로 클래스 Wrapper를 제공받은 후, 후킹할 함수의 implementation을 자바스크립트 함수로 덮는 방식을 사용한다.

  • 따라서 checkPath 함수의 implementation에 항상 False를 반환하도록 작성된 함수를 전달하여 checkModule 함수가 매번 False를 반환하도록 후킹할 수 있다.

modifyReadLineRet 함수 : BufferedReader.readLine 반환 값 변조

BufferedReader.readLine 함수는 스트림으로부터 개행으로 끝나는 한 줄의 문자열을 읽을 때 사용하는 함수이다.

TodoList 앱은 메모리 맵 파일의 내용을 전부 읽기 위해 해당 함수를 사용한다.

File.$init.overload('java.lang.String')
  • String 하나 받는 생성자만 정확히 지정
var File = Java.use("java.io.File");
var constructor = File.$init.overload('java.lang.String');
  • java.io.File 클래스 가져옴

그 중에서:

File(String pathname)

이 생성자만 선택

 

따라서 디렉토리 식별도 없이 "frida"라는 다섯 문자열이 포함 되어 있다고 해서 무조건 "frida"문자열을 지우도록 후킹하면 앱이 원래 수행해야 하는 동작도 문제가 생길 위험이 존재한다.

이를 위해 File 클래스의 생성자를 먼저 후킹한 후, 생성자로 전달된 파일 이름이 "/proc/self/maps"인 경우에만 BufferedReader.readLine 함수를 후킹하도록 작성하였다.

 

한 가지 주의해야 할 점은 기존 후킹을 통한 파라미터 변조와 반대로 이번에는 반환 값을 변조해야 하기 때문에 원본 함수를 먼저 호출하고, 반환 값 내에 "frida"라는 문자열이 존재하는지 검사하는 방식으로 후킹해야 한다.

    if (pathname == "/proc/self/maps") {
        var BufferedReader = Java.use("java.io.BufferedReader");
        var readLine = BufferedReader.readLine.overload();

        readLine.implementation = function() {
            var ret = readLine.call(this);	// 원본 함수를 먼저 호출하고
            if (ret.includes("frida"))	// 반환 값 내에 "frida"라는 문자열이 존재하는지 검사
                ret = "";
            return ret;
        };
    }

this 없이 호출하면 생기는 문제

return constructor.call(this, pathname);

만약 이렇게 하면:

constructor(pathname);

문제 발생:

  • 어떤 객체에 적용할지 모름
  • 새로운 객체가 만들어질 수도 있음
  • 현재 생성 중인 객체(this)가 아닌 다른 컨텍스트에서 실행됨

결과:

  • 정상적인 객체 생성 흐름 깨짐
  • 앱 크래시 가능성 높음

스크립트 실행 결과

> frida -U -f com.example.todolist_for_frida -l ./Bypass_Frida_checkModule.js
     ____
    / _  |   Frida 16.1.3 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Android Emulator 5554 (id=emulator-5554)
Spawned `com.example.todolist_for_frida`. Resuming main thread!
[Android Emulator 5554::com.example.todolist_for_frida ]->

'Mobile App Hacking : Android > Frida 탐지 우회' 카테고리의 다른 글

경로 검사 우회  (0) 2026.05.27
포트 검사 우회  (0) 2026.05.27