AFL qemu mode를 이해하고 사용하기위해 아래링크의 글을 읽어본다.
AFL++의 qemu mode를 사용하면 Snapshot mode, Deferred initialization, Persistent mode를 사용할 수 있다.
참고로 필자는 바이너리의 실행시점에서 initialize 과정을 bypass해서 퍼징의 성능을 높이고 싶어, Snapshot mode를 사용하는 방법을 익히고싶어 이 글을 읽게되었다.
https://github.com/AFLplusplus/AFLplusplus/blob/stable/qemu_mode/README.md
High-performance binary-only instrumentation for afl-fuzz
1) Introduction
AFLplusplus/qemu_mode에 있는 코드는 QEMU의 "user emulation" 모드를 활용해서 block-box, closed-source 바이너리에 대한 instrumentation 출력 결과를 얻을 수 있게 해준다. 이 방법은 afl-fuzz에서 afl-cc로 빌드할 수 없는 타겟을 테스트하는데 사용할 수 있다.
일반적인 성능비용(usual performance)는 2-5배 수준으로 DynamoRIO나 PIN과 같은 도구와의 실험에서 상당히 좋은 결과를 보여줬다고 한다.
2) How to use QEMU mode
이 기능은 패치된 QEMU를 사용해서 구현되었으며, 사용하는 가장쉬운 방법은 ./build_qemu_support.sh 를 실행하는 것이다. 이걸 실행하면 알아서 QEMU 바이너리를 download, configure, and compile 해준다.
QEMU는 큰 프로젝트라서 시간이 좀 걸리고, 몇가지 dependencies를 해결해야할 수도 있다. (가장 대표적으로 libtool 과 glib2-devel)
바이너리가 컴파일되면 afl-fuzz와 다른 관련 유틸리티들을 -Q 옵션과 함께 qemu 도구를 사용할 수 있다.
이때 ./build_qemu_support.sh 를 실행하기전에 CPU_TARGET을 세팅해주면 내가가진 CPU의 네이티브 바이너리가 아닌 바이너리를 실행할 수 있는 빌드를 얻을 수도있다.(ex. CPU_TARGET = arm) 이거는 64비트 시스템에서 32비트 바이너리를 실행할때도 필요하다 (CPU_TARGET = i386). 다른 아키텍처에서 QEMU를 실행하려면 HOST를 cross-compiler prefix로 설정할 수 있다. (ex. HOST=arm-linux-gnueabi) 또 다른 일반적인 타겟은 CPU_TARGET=aarch64이다.
STATIC=1을 설정해서 정적으로 링크된 바이너리를 컴파일할 수도 있다. 퍼저를 돌리는 시스템이랑 타겟 프로그램이 있는 시스템이 다른경우에 QEMU를 컴파일할때 유용하며, 주로 HOST 변수와 함께 쓰인다.
참고: i386 아키텍처를 타겟으로 할 때, 일부 바이너리에서는 예약된 메모리가 부족하여 forkserver handshake가 실패할 수 있다. 이때 아래와 같이 해결하면 된다.
export QEMU_RESERVED_VA=0x1000000
For an example, see README.deferred_initialization_example.md.
3) Deferred initialization
AFL++의 fork server initialization 시점을 미루는 기법을 의미한다. 일반적으로 AFL++의 포크서버는 프로그램의 시작 시점에 초기화되고 이는 프로그램의 명령줄 인자 처리, 설정 파일 로드 등 다양한 초기화 작업이 완료되기 전에 수행된다.
export AFL_ENTRYPOINT=0x123456
위와 같이 AFL_ENTRYPOINT 변수를 설정하면, 프로그램이 해당 메모리 주소에 도달했을 때 포크 서버가 초기화된다. 이로 인해, 프로그램이 명령줄 인자 처리 및 설정 파일 로드 등을 완료한 후에 포크 서버가 초기화되어, 퍼징 효율성이 높아질 수 있다.
지연 초기화(Deferred Initialization)는 프로그램이 시작될 때 곧바로 특정 리소스나 객체를 초기화하지 않고, 필요할 때까지 초기화를 미루는 기법을 의미한다. 이 기법은 특히 프로그램의 초기 로딩 시간을 단축시키고, 초기화가 불필요할 수 있는 경우를 피할 수 있도록 해준다.
4) Persistent mode
AFL++에서 제공하는 최적화 기법으로, 퍼징 과정의 효율성을 크게 높이기 위해 프로그램을 반복적으로 재시작하지 않고 지속적으로 실행하는 방식이다.
별도로 프로그램에 코드를 삽입해주어야하지만 성능이 많이 올라간다고 한다.
For details, see README.persistent.md.
5) Snapshot mode
이제 필자가 주목하고있는 Snapshot mode이다. Snapshot mode는 persistent mode의 확장 버전이고, qemuafl을 사용하면 memory state와 brk()를 snapshot을 찍고 복원(restore)하며 퍼징할 수 있다.
역시나 자세한 내용은 아래 링크에서 확인할 수 있다고 한다.
For details, see README.persistent.md.
아래의 코드를 참고하여 살펴보면
#include <stdio.h>
#include <unistd.h>
#define AFL_QEMU_SNAPSHOT_ADDR 0x12345678
void snapshot_restore() {
// 스냅샷을 복원하는 코드
}
void snapshot_create() {
// 스냅샷을 생성하는 코드
}
int main(int argc, char **argv) {
int count = 0;
// 초기화 코드
printf("Initialization code\n");
// 스냅샷 생성
snapshot_create();
while (1) {
// 스냅샷 복원
snapshot_restore();
char buf[100];
ssize_t len = read(0, buf, sizeof(buf) - 1);
if (len > 0) {
buf[len] = '\0';
// 입력 처리 코드
printf("Processing input %d: %s\n", ++count, buf);
}
// 테스트 후 스냅샷 상태로 복원
}
// 종료 코드
printf("Cleanup code\n");
return 0;
}
위와 같이 snapshot을 찍고 다시금 restore하는 시점을 정해주어 초기화 단계를 반복하지않으며 테스트를 실행해갈 수 있다.
snapshot mode를 사용하기위해 준비하는 방법은 환경변수 AFL_QEMU_SNAPSHOT를 세팅해주는 것이다. 이때 스냅샷 진입 지점인 hex 주소를 값으로 사용한다.
export AFL_ENTRYPOINT=0x123456
snapshot mode는 모든 쓰기가능한 pages들을 restoring할 수 있으며, 일반적으로 fork() mode보다 느리지만 multicore인 경우 더 쉽게 확장할 수 있다. 또 AFL++ snapshot kernel module이 로드된경우 fork보다 빠르고 더 확장이 가능할 수 있다.
필자는 persistent mode중 snapshot mode에 관심있기 때문에 아래 문서를 이어서 읽어볼 예정이다.
https://github.com/AFLplusplus/AFLplusplus/blob/stable/qemu_mode/README.persistent.md
How to use the persistent mode in AFL++'s QEMU mode
1) Introduction
persistent mode를 사용하면 두 주소 사이에서 타겟을 지속적으로 퍼징할 수 있으며, 각 퍼징 시도마다 포크를 하지 않습니다. 이로 인해 속도가 2배에서 5배까지 증가하므로 매우 유용합니다.
현재 persistent mode는 x86/x86_64, arm 및 aarch64 타겟에서만 사용할 수 있습니다.
2) How use the persistent mode
2.1) The START address
지속루프(persistent loop)의 시작점을 AFL_QEMU_PERSISTENT_ADDR 로 설정해주어야 한다.
이 주소는 어떤 instruction의 주소여야한다. 함수의 시작 부분으로 이 주소를 설정하면 사용이 간단하다. 그러나 이 주소가 함수 내에 있는 경우, RET, OFFSET, 또는 EXITS(아래 2.2, 2.3, 2.6 참조)를 설정해야한다.
이 주소(및 아래의 RET 주소)는 0x로 시작하는 16진수로 정의하거나 10진수 값으로 정의해야한다.
RET와 EXITS가 모두 설정되지 않으면, QEMU는 START가 함수의 시작을 가리킨다고 가정하고 return address(스택 또는 링크 레지스터)를 START로 패치한다(WinAFL과 유사).
참고: 타겟이 위치 독립 코드 position independent code 보호기법(PIE/PIC)로 컴파일된 경우 QEMU는 이를 a specific base address에 로드한다.
amd64의 경우 주소에 0x4000000000(9개의 0)을 추가
32비트의 경우 0x40000000(7개의 0)을 추가
aarch64에서는 보통 0x5500000000이다.
QEMU가 PIE 실행 파일에 대해 설정한 기준 주소는 설정에 따라 다를 수 있다.
AFL_QEMU_DEBUG_MAPS=1 afl-qemu-trace TARGET-BINARY를 사용하여 프로세스 맵을 출력하여 확인할 수 있다.
이 주소가 유효하지 않으면, afl-fuzz는 포크 서버를 찾을 수 없다는 메시지와 함께 시작 시 오류를 발생시킨다.
2.2) The RET address
RET address는 지속루프(persistent loop)의 마지막 instruction이다. 에뮬레이터는 RET에서 명령을 번역할 때 START로 점프를 생성한다. RET address는 START 주소가 가리키는 함수의 끝이 아니라 다른 지점에서 반환이 필요할 때에만 필요하다. 즉, 이건 선택사항이다.
이 주소는 AFL_QEMU_PERSISTENT_RET을 설정하여 정의되고 타겟이 위치 독립적이라면 (PIE/PIC 걸려있다면) 0x4000000000도 설정해줘야한다.
2.3) The OFFSET
이 옵션은 x86/x86_64에서만 유효하다. (arm/aarch64는 스택에 반환 주소를 저장하지 않는다.)
START 주소가 함수의 시작이 아니고, RET가 설정되지 않은 경우(따라서 루프의 끝이 함수의 끝에 있지만 START는 함수는 시작 부분에 있지 않음), 반환 주소를 패치하려면 ESP 포인터에서의 오프셋이 필요하다.
ESP 포인터를 보정해야 하는 값은 AFL_QEMU_PERSISTENT_RETADDR_OFFSET 변수에 설정해야한다.
이 값을 정확히 설정하는 방법은 다음과 같다:
- gdb를 사용하여 타겟을 디버깅합니다.
- "main"에 브레이크포인트를 설정합니다(PIE/PIC 바이너리의 경우 주소가 설정되도록 필수).
- 유효한 명령줄로 타겟을 "run"합니다.
- START가 포함된 함수에 브레이크포인트를 설정합니다.
- START 주소에 브레이크포인트를 설정합니다.
- 함수 시작 브레이크포인트까지 "continue"합니다.
- print $esp로 ESP 값을 출력하고 기록합니다.
- 두 번째 브레이크포인트까지 타겟을 "continue"합니다.
- 다시 ESP 값을 출력합니다.
- 두 값의 차이를 계산합니다 - 이 값이 오프셋입니다.
2.4) Resetting the register state
새로운 루프를 시작할때, 범용 레지스터 상태를 복원해야할 가능성이 매우높다. 그래서 대부분의 경우 AFL_QEMU_PERSISTENT_GPR=1를 세팅해주어야한다.
지속 모드에서 프로그램이 반복적으로 실행되므로, 각 반복 실행 시 이전 실행의 레지스터 상태가 유지될 수 있다. 이는 특히 함수의 매개변수나 전역 변수를 레지스터를 통해 전달하는 경우, 두 번째 실행 이후 이러한 값들이 손실되거나 예상치 못한 값으로 변경될 수 있다. 따라서, 매 반복 실행 시 범용 레지스터(GPR) 상태를 초기화하여 일관된 초기 상태를 보장하는 것이 중요하다.
이 환경 변수를 설정하면, 각 루프 반복 시점에서 레지스터 상태가 초기화된다. 이를 통해 프로그램이 매번 동일한 초기 상태에서 시작되며, 레지스터에 저장된 함수 매개변수나 다른 중요한 값들이 정확히 유지된다.
예를 들어 아래 코드에
int main(int argc, char **argv) {
if (argc < 2) return 1;
// do stuff
}
save & restore register 세팅을 안해주면은 argc 파라미터가 두번째 루프 실행할때에 lost 될 수 있다.
2.5) Resetting the memory state
이 옵션은 AFL++ snapshot LKM이 로드된 경우 메모리 상태를 복원한다. 그렇지않으면 모든 쓰기가능한 페이지들을 (writeable pages) restore한다.
이 옵션을 활성화하려면 AFL_QEMU_PERSISTENT_MEM=1을 설정해주면 된다.
2.6) Reset on exit()
사용자는 program counter를 executing the exit_group syscall and exit the program 하는 대신에 START로 설정할 수 있다. 환경 변수는 AFL_QEMU_PERSISTENT_EXITS 이다.
2.7) Snapshot
AFL_QEMU_SNAPSHOT=address
는 아래처럼 여러변수들을 한번에 세팅해주는 환경 변수이다.
AFL_QEMU_PERSISTENT_ADDR=address
AFL_QEMU_PERSISTENT_GPR=1
AFL_QEMU_PERSISTENT_MEM=1
AFL_QEMU_PERSISTENT_EXITS=1
스냅샷 모드는 지속 모드의 설정을 포함하므로, AFL_QEMU_PERSISTENT_ADDR, AFL_QEMU_PERSISTENT_GPR, AFL_QEMU_PERSISTENT_MEM, AFL_QEMU_PERSISTENT_EXITS 변수를 자동으로 설정해주는 것이다.
'공부 > JUN STUDY' 카테고리의 다른 글
Vim을 이용한 Line Breaks (자동 줄바꿈 하기) set tw 이용 gqap (2) | 2024.09.03 |
---|---|
Mysql Access denied Error 해결법 Enter password: ERROR 1698 (28000): Access denied for user 'root'@'localhost' (0) | 2024.08.22 |
AFL++ custom mutator 이해하기 (1) - file input으로 넣기 (9) | 2024.07.24 |
Fuzzotron 을 이용한 NASA cFS(Core Flight System) 퍼징하기 (0) | 2024.05.12 |
ubutnu에서 firebase 세팅법 (간단한 백엔드 웹서버 만들기) (0) | 2024.01.05 |