반응형

최근 들어 프리다를 탐지하여 앱을 종료시키는 솔루션들이 꽤 많습니다. 이렇게 되면, 진단하는 입장에서는 애를 먹을 수 밖에 없습니다.

그래서, AOS에서 몇몇 기본적인 프리다 탐지원리 및 우회방안을 정리해봤습니다.

 

(1) /proc/<pid>/maps

프리다가 앱 프로세스에 attach 하게 되면, 다음 그림과 같이 /proc/<pid>/maps 파일에 frida-agent native library 정보가 표시됩니다.

 

/proc/<pid>/maps 파일을 읽어들이기 위해서는 파일 오픈이 먼저 필요하므로, "fopen" 함수를 후킹하여 "/proc/self/maps" 문자열을 "/proc/self/statm" 등으로 바꿔치기하여 우회가 가능합니다.

 

 

(2) /proc/<pid>/fd/<No>

"/proc/<pid>/fd/" 디렉터리에서는 심볼릭링크에 대한 정보를 볼 수 있습니다.

프리다가 앱 프로세스에 attach 하게 되면, "linjector" 라는 파일로 연결되는 특이한 심볼릭링크가 하나 생성되는데, 이는 프리다 탐지의 실마리를 제공합니다.

 

lstat 함수는 심볼릭링크 파일의 원본파일에 대한 정보를 구조체에 저장합니다. 따라서, 위 그림의 경우 lstat 함수가 원본파일이 "linjector-1121"인 심볼릭링크 파일("/proc/31302/fd/557")을 읽지 못하도록 하면 우회가 가능합니다.

 

다음은 "/proc/<pid>/maps파일 및 "/proc/<pid>/fd/<No>" 파일을 읽어들여 프리다를 탐지하는 로직을 우회하는 예시 스크립트입니다.

function nativeTrace(nativefunc) {
    
    var readlinkPtr = Module.getExportByName("libc.so", "readlink");
    var readlink = new NativeFunction(readlinkPtr, 'int', ['pointer','pointer','int']);

    var nativefunc_addr = Module.getExportByName(null,nativefunc)
    var func = ptr(nativefunc_addr);

    Interceptor.attach(func, {   // set hook
       onEnter: function (args) {
           console.warn("\n[+] " + nativefunc + " called");  // before call
           
           // /proc/self/fd/<NO> 파일들은 모두 symlink 파일들인데, readlink 함수를 이용해 original file path를 가져올 수 있고, 그 중에 frida attach 로 인한 "linjector" 문자열이 포함되어 있는지 확인하고, 포함되어 있다면 해당 인자를 변경
           if(nativefunc == "lstat" && args[0].readUtf8String().match(/\/proc.*fd.*/)) {
                // readlink 함수를 이용한 original file path 가져오기
                var buf = Memory.alloc(64);
                readlink(args[0], buf, 64);
                console.log(ptr(buf).readUtf8String());
                // original file path에 "linjector" 문자열이 포함되어 있는 경우 args[0] 인자를 "/proc/self/fd/1"로 변경
                if(ptr(buf).readUtf8String().indexOf("linjector") > -1){
                    args[0] = args[0].writeUtf8String("/proc/self/fd/1");
                    console.log(args[0].readUtf8String());
                }
           }
           else if(nativefunc == "fopen"){
                // fopen 함수가 "/proc/self/maps" 파일을 열려고 한다면, args[0]인자를 "proc/self/statm"으로 변경
                if(args[0].readUtf8String() == "/proc/self/maps"){
                    args[0] = args[0].writeUtf8String("/proc/self/statm");
                }
                console.log("\n\x1b[31margs[0]:\x1b[0m \x1b[34m" + args[0].readUtf8String() + ", \x1b[32mType: ");
           }
       },
       onLeave: function (retval) {
           console.warn("[-] " + nativefunc + " Exiting");
       }
    });
}

nativeTrace("lstat");
nativeTrace("fopen");

 

 

(3) /proc/<pid>/task/<thread id>/status

"/proc/<pid>/task/<thread id>/status" 파일에서는 특정 thread 에 대한 정보를 볼 수 있습니다.

프리다가 앱 프로세스에 attach 하게 되면, 다음과 같이 "gmain" 또는 "gum-js-loop" 문자열을 포함하는 프리다 특유의 thread가 생성됩니다.

 

해당 로직은 아직 접해보지는 않았지만 이 역시 fopen 함수 후킹을 통한 인자 바꿔치기로 우회가 가능할 것으로 생각됩니다.

 

 

(4) Iterate through all the TCP open ports and pass D-Bus AUTH message to see if frida-server responds

모든 열린 TCP 포트에 D-Bus AUTH 메세지를 보내서 응답값을 관찰하여 프리다 서버 구동여부를 탐지하는 로직입니다. 해당 탐지로직 예시 코드는 "https://github.com/b-mueller/frida-detection-demo/blob/master/AntiFrida/app/src/main/cpp/native-lib.cpp" 에서 확인하실 수 있습니다.

 

다음은 위 깃헙의 예시코드를 aarch64로 빌드한 프리다 탐지 실행파일입니다.

fridaDetectionTest
0.58MB

 

frida server를 구동한 상태에서 위 실행파일을 실행하게 되면 다음과 같이 "REJECT" 메세지를 볼 수 있습니다.

 

해당 로직은 아직 접해보지는 않았지만  "connect" 또는 "send" 또는 "recv" 메서드를 후킹해서 우회 가능할 것으로 판단됩니다.

 

 

※ 참고

https://darvincitech.wordpress.com/2019/12/23/detect-frida-for-android/

https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/0x05j-testing-resiliency-against-reverse-engineering

https://www.romainthomas.fr/post/20-09-r2con-obfuscated-whitebox-part1/

https://github.com/b-mueller/frida-detection-demo/blob/master/AntiFrida/app/src/main/cpp/native-lib.cpp

 

반응형

+ Recent posts