
Frida 기반의 인프로세스 퍼징 스위트로, AFL++ 프록시, 독립형 능동/수동 모드, 공유 메모리 통신을 통해 플랫폼 전반에서 고성능 커버리지 기반 취약점 발견을 지원합니다.
fpicker는 Frida 기반의 퍼징 스위트로, AFL++ 모드나 수동 추적 모드와 같은 다양한 퍼징 모드를 프로세스 내 퍼징(in-process fuzzing)을 위해 제공합니다. Frida가 지원하는 모든 플랫폼에서 실행되어야 합니다.
fpicker의 배경 정보와 생각 및 아이디어는 제가 쓴 블로그 포스트에서 확인할 수 있습니다.
Fpicker는 제 석사 논문 중에 개발된 ToothPicker의 이전 작업을 기반으로 합니다. fpicker의 대부분은 제 고용주(ERNW)에서 근무 시간 중에 개발되었습니다.
fpicker 실행에 필요한 항목:
frida-core-devkit은 Frida 릴리스 페이지에서 확인 가능.
libfrida-core-ios.a, libfrida-core-macos.a, 또는 libfrida-core-linux.a로 저장합니다.frida-core.h)도 마찬가지입니다. 플랫폼에 따라 frida-core-linux.h 또는 frida-core-ios.h로 저장합니다.update_frida_version.sh를 실행하면 됩니다.AFL++ 모드에서 실행할 때만 필요:
CFLAGS="-DUSEMMAP=1"로 컴파일.CFLAGS="-DUSEMMAP=1 -DTARGET_OS_IOS"로 컴파일.Fpicker는 macOS, iOS 또는 Linux용으로 빌드할 수 있습니다. 현재 Makefile은 macOS에서 iOS용 빌드만 지원하지만, Linux의 iOS 툴체인을 사용하여 fpicker를 빌드하는 것도 완전히 가능합니다.
원하는 대상에 따라 다음을 실행하십시오:
make fpicker-macos
make fpicker-ios
make fpicker-linux
fpicker를 빌드합니다.
fpicker가 빌드되면 다음으로 퍼징 하네스를 빌드해야 합니다:
다양한 샘플 퍼징 케이스에 대해서는 examples 폴더를 참조하십시오. 일반적인 접근 방식은 다음과 같습니다:
examples/test/test.js) (하네스에 대한 자세한 내용은 여기 참조)frida-compile test.js -o harness.js이제 fpicker가 퍼징을 시작할 수 있습니다. 정확한 명령은 설정 및 구성에 따라 크게 달라집니다. 다음은 몇 가지 예제 케이스입니다. 이들은 대부분 examples 폴더의 예제에 해당합니다.
afl-fuzz -i examples/test-network/in -o ./examples/test-network/out -- \\
./fpicker --fuzzer-mode afl -e attach -p test-network -f ./examples/test-network/harness.js
./fpicker --fuzzer-mode standalone -e attach -p server-process -f harness.js --input-mode cmd \\
--command "./client-send @@" -i indir -o outdir
./fpicker --fuzzer-mode active --communication-mode shm -e attach -p server-process -f harness.js \\
-i indir -o outdir --standalone-mutator cmd --mutator-command "radamsa"
./fpicker --fuzzer-mode passive --communication-mode send -e attach -p server-process -o outdir -f harness.js
./fpicker --fuzzer-mode active -e attach -p test -D remote -o examples/test/out/ -i examples/test/in/ \\
-f fuzzer-agent.js --standalone-mutator cmd --mutator-command "radamsa"
각 대상에는 고유한 퍼징 하네스가 필요합니다. 이 하네스에서 가장 중요한 부분은 Frida의 Stalker 진입 함수를 정의하는 것입니다. 이 함수는 계측이 삽입되는 지점을 결정합니다. in-process 모드에서는 간단합니다. 일반적으로 각 퍼징 반복에서 호출되는 함수입니다. 그러나 다른 함수일 수도 있습니다.
최소한의 하네스 구현(command 모드에서)은 다음과 같을 수 있습니다:
// Import the fuzzer base class
const Fuzzer = require("harness/fuzzer.js");
// The custom fuzzer needs to subclass the Fuzzer class to work properly
class TestFuzzer extends Fuzzer.Fuzzer {
constructor() {
// The constructor needs to specify the address of the targeted function and a NativeFunction
// object that can later be called by the fuzzer.
const FUZZ_FUNCTION_ADDR = Module.getExportByName(null, "FUZZ_FUNCTION");
const FUZZ_FUNCTION = new NativeFunction(
FUZZ_FUNCTION_ADDR,
"void", ["pointer", "int64"], {
});
super("test", FUZZ_FUNCTION_ADDR, FUZZ_FUNCTION);
}
}
const f = new TestFuzzer();
exports.fuzzer = f;
이 하네스는 FUZZ_FUNCTION 함수를 따라가도록 계측을 구성합니다. 계측은 이 함수가 진입될 때 시작되고 함수가 반환될 때 중지됩니다. 이 함수는 비용이 많이 들기 때문에 신중하게 선택해야 합니다. 프로세스의 더 많은 (잠재적으로 중요하지 않은) 부분을 계측할수록 퍼저 속도가 느려집니다. 물론 이것은 속도와 원하는 커버리지 사이의 고려 사항입니다. 또한, 퍼저는 현재 한 번의 퍼징 반복 중에 한 번만 진입되는 함수만 지원합니다. 즉, 함수가 한 번의 퍼즈 케이스 중에 두 번 이상 호출되어서는 안 됩니다. 그렇지 않으면 커버리지 정보가 신뢰할 수 없게 될 수 있습니다.
in-process 모드를 사용할 때는 퍼저 스크립트에 또 다른 함수가 필요합니다. 바로 fuzz 메서드입니다. 이 메서드는 각 반복마다 호출됩니다. 두 개의 매개변수(버퍼에 대한 포인터와 버퍼의 길이)로 호출됩니다. 우리의 예시 대상 함수는 버퍼에 대한 포인터와 그 길이라는 두 개의 매개변수를 받습니다. 따라서 fuzz 메서드에서 받은 매개변수를 그대로 전달하면 됩니다.
fuzz(payload, len) {
this.target_function(payload, parseInt(len));
}
passive 모드에서는 필요한 데이터를 처리하는 콜백을 지정해야 합니다. 퍼저는 페이로드 버퍼와 그 길이를 받을 것으로 예상합니다. 대상 함수에 따라 이 데이터를 추출해야 합니다. 다음 예제에서는 버퍼에 대한 포인터와 그 길이라는 두 개의 매개변수를 가진 함수를 다시 사용합니다. args 매개변수에는 대상 함수가 받는 모든 잠재적 매개변수가 포함되므로, 길이 매개변수(우리의 경우 두 번째)는 args[1]로 접근할 수 있습니다. 그런 다음 버퍼를 Uint8Array로 읽고 sendPassiveCorpus 메서드를 사용하여 퍼저로 다시 보냅니다.
passiveCallback(args) {
const len = args[1];
const data = new Uint8Array(Memory.readByteArray(args[0], parseInt(len)));
// this encodes the data and sends it back to the fuzzer
this.sendPassiveCorpus(data, len);
}
퍼저가 시작되기 전에 대상에 어떤 종류의 준비가 필요한 경우, fpicker는 퍼저 초기화 중에 호출되는 prepare 메서드를 제공합니다. 준비는 객체 인스턴스화와 같은 상태 설정이 될 수 있습니다. 이러한 준비 함수는 다음과 같을 수 있습니다:
prepare() {
// the object can be attached to the fuzzer instance so that it can be used within the
// fuzz() method later on.
this.required_object = call_native_function_that_creates_object();
}
pficker는 아래에서 설명하는 다양한 모드와 설정을 제공합니다. 이러한 모드의 대부분은 다양한 방식으로 조합될 수 있습니다. 이 섹션의 끝에는 어떤 옵션을 조합할 수 있고 구현 상태가 어떻게 되는지 보여주는 표가 있습니다.
Fpicker에는 AFL++ 모드, 독립 실행형 능동 모드, 독립 실행형 수동 모드의 세 가지 퍼징 모드가 있습니다:
AFL++ 모드: AFL++ 모드에서 fpicker는 AFL++와 대상 프로세스 간의 프록시 역할을 합니다. Frida의 계측 기능을 사용하여 대상이 AFL++에서 생성된 입력 데이터로 퍼징되는 동안 AFL의 커버리지 비트맵이 채워집니다.
독립 실행형 능동 모드: 독립 실행형 능동 모드에서 퍼저는 Frida의 Stalker 호출 요약을 사용하여 반복 중에 실행되는 기본 블록 형태의 커버리지를 수집합니다. 이것은 새로운 것이 아니며 이전에도 다양한 형태로 구현되었습니다. 그러나 다른 퍼저 설정과 결합하면 다양한 이점을 가질 수 있습니다. 또한 주어진 환경이나 경우에 AFL++를 적용할 수 없거나 원하지 않을 때 좋은 대안입니다.
독립 실행형 수동 모드: 수동 모드는 퍼저라기보다는 트레이서에 가깝습니다. 기본적으로 독립 실행형 능동 모드와 동일한 작업을 수행합니다. 그러나 자체 입력을 보내지 않습니다. 단지 특정 함수에 연결하여 커버리지를 수집합니다. 새로운 커버리지가 관찰되면 커버리지와 입력이 모두 저장됩니다.
Fpicker는 주로 프로세스 내 퍼저로 설계되었지만 외부 명령을 통한 퍼징도 지원합니다. 이를 위해 fpicker는 두 가지 입력 모드를 제공합니다.
프로세스 내 입력 모드: 프로세스 내 입력 모드에서 하네스는 대상 프로세스의 지정된 함수를 직접 호출합니다. 퍼저는 페이로드를 하네스로 보내고 하네스는 대상 함수를 호출할 수 있도록 페이로드를 준비합니다.
CMD 입력 모드: 명령 입력 모드에서 페이로드는 외부 명령으로 리디렉션됩니다. 이는 대상 함수를 직접 호출할 때 매개변수나 다른 상태를 준비하기 너무 복잡할 때 유용합니다. 커버리지 수집은 여전히 특정 함수에 연결되어야 합니다. 페이로드를 제공하면 대상 함수를 트리거하는 클라이언트가 있을 수 있습니다.
통신 모드는 주입된 하네스가 퍼저와 통신하는 방식을 결정합니다. 이는 대상 애플리케이션에 크게 의존합니다. Frida는 주입된 에이전트 스크립트에서 메시지를 보내고 받을 수 있는 API를 제공합니다. 이러한 유형의 통신은 비용이 많이 듭니다. 요인 중 하나는 전송된 메시지를 JSON으로 인코딩해야 한다는 것입니다. 따라서 바이너리 데이터를 보내는 것은 간단합니다. 따라서 fpicker는 공유 메모리를 통한 두 번째 통신 모드를 제공합니다. 그러나 이는 퍼저와 대상 애플리케이션 간에 공유 메모리를 설정할 수 있는 경우에만 작동합니다. 즉, 대상이 USB를 통해 퍼저 호스트에 연결된 경우 이 모드를 사용할 수 없습니다. CMD 입력 모드에서 통신 모드는 페이로드 전송 방식이 아니라 커버리지 정보가 퍼저로 다시 전달되는 방식만을 의미합니다. 페이로드 전송은 외부 명령에 위임되기 때문입니다.
Send 통신 모드: Send 통신 모드에서 페이로드는 Frida의 RPC 호출 메커니즘을 사용하여 전송됩니다. 이를 통해 퍼저는 주입된 하네스 스크립트 내에서 JavaScript 함수를 실행할 수 있습니다. 하네스 내부의 이 함수는 대상 함수를 호출하기 위해 필요한 모든 준비를 수행할 수 있습니다. 대상 함수가 반환되면 커버리지 수집이 중지되고 하네스는 Frida의 send API를 사용하여 커버리지 정보를 다시 퍼저로 보내 반복이 완료되었음을 알릴 수 있습니다.
SHM 통신 모드: SHM 통신 모드에서 퍼저와 하네스 스크립트는 공유 메모리와 세마포어를 통해 통신합니다. 공유 메모리의 버퍼는 페이로드를 보내고 커버리지 정보를 수신하는 데 사용됩니다. 보내고 받는 대신 두 구성 요소는 세마포어에서 대기 및 게시를 사용합니다. 시스템과 대상에 따라 이는 상당한 성능 향상을 가져옵니다. 특히 바이너리 페이로드가 메모리에 한 번만 기록되고 인코딩 및 디코딩하거나 다른 메모리 위치로 복사할 필요가 없기 때문입니다. 불행히도 이 모드는 AFL++로 실행할 때 안정성이 낮아지는 경우가 있습니다. 아직 이유는 확실하지 않습니다.
실행 모드는 spawn 또는 attach일 수 있습니다. 이는 자명합니다. fpicker는 실행 중인 프로세스에 연결하거나 프로세스를 생성할 수 있습니다. 두 모드 간의 주요 차이점 중 하나는 연결된 대상이 충돌할 경우 fpicker가 다시 생성하려고 시도하지 않는다는 것입니다.
독립 실행형 모드에서 fpicker는 세 가지 다른 입력 변이 전략을 제공합니다. 좋게 말하면 입력 변이에는 개선의 여지가 많습니다.
NULL 독립 실행형 변이기: 이 변이기는 페이로드를 변이하지 않고 동일한 페이로드의 복사본을 반환합니다. 주로 테스트 목적으로 사용됩니다. 그 외에는 실제로 유용하지 않습니다.
Rand 독립 실행형 변이기: 매우 나쁜 무작위 변이기입니다. 원래 페이로드의 무작위 위치에 값을 무작위로 바꾸는 것만 수행합니다. 페이로드 길이는 변경하지 않습니다.
사용자 정의 독립 실행형 변이기: 이 변이기는 외부 명령을 호출하여 페이로드를 변이할 수 있습니다. 페이로드를 stdin에 쓰고 변이된 페이로드를 stdout에서 받습니다. 구현이 얕기 때문에 성능에 상당한 영향을 미칩니다.
-D usb 장치 옵션을 사용하면 Frida가 첫 번째 로컬 USB 장치(예: iPhone 또는 Android 전화)를 선택합니다.
-D remote 옵션을 사용하면 네트워크 장치에서 실행 중인 프로세스를 퍼징할 수 있습니다. 이를 위해 원격 장치에서 frida-server가 실행 중이어야 합니다. 샘플 구성으로 SSH와 포트 포워딩을 사용하여 원격 장치의 frida-server 기본 수신 포트 27042를 로컬 클라이언트의 소켓에 바인딩합니다.
ssh -N [email protected] -L 127.0.0.1:27042:127.0.0.1:27042
iPhone에서는 iproxy를 사용하여 USB 연결에서 포트를 전달할 수도 있습니다.
이는 비표준 포트에서 Frida gadget과 함께 Frida를 실행하는 탈옥되지 않은 장치에서 특히 유용할 수 있습니다. Frida gadget으로 작업할 때 사용 가능한 유일한 프로세스 이름은 대상 앱 이름과 관계없이 Gadget이 됩니다.
iproxy 27042 27042
그런 다음 frida-ps를 사용하여 원격 장치의 프로세스를 나열하여 구성을 확인합니다:
frida-ps -R