
순수 Ruby 코드와 Ruby C 확장을 위한 커버리지 기반 퍼저
순수 Ruby 코드와 Ruby C 확장을 위한 커버리지 기반 퍼저입니다.
Ruzzy는 Python 퍼저인 Google의 Atheris에서 큰 영감을 받았습니다. Ruzzy는 커버리지 계측과 퍼징 엔진을 위해 libFuzzer(또는 LibAFL)를 사용합니다. 또한 Ruzzy는 C 확장을 퍼징할 때 AddressSanitizer와 UndefinedBehaviorSanitizer를 지원합니다. Ruzzy의 영감에 대해 더 자세히 알고 싶다면 저희 논문인 Design and Implementation of a Coverage-Guided Ruby Fuzzer를 참조하세요.
목차:
Ruzzy는 Linux(x86-64, AArch64/ARM64)와 macOS(Apple Silicon)를 지원합니다. Windows에서는 Dockerfile을 빌드하거나 개발 환경을 사용할 수 있습니다. Ruzzy는 최신 버전의 clang(14.0.0까지 테스트됨)이 필요하며, 가급적 최신 릴리스를 권장합니다. macOS 관련 설정은 macOS 사용자 참고 사항을 참조하세요.
다음 명령어로 Ruzzy를 설치하세요:
MAKE="make --environment-overrides V=1" \
CC="/path/to/clang" \
CXX="/path/to/clang++" \
LDSHARED="/path/to/clang -shared" \
LDSHAREDXX="/path/to/clang++ -shared" \
gem install ruzzy
여기에는 많은 내용이 포함되어 있으니 하나씩 설명하겠습니다:
MAKE 환경 변수는 Ruzzy C 확장을 컴파일할 때 make 명령을 재정의합니다. 이는 익스텐션 컴파일 시 후속 환경 변수들을 make가 존중하도록 지시합니다.clang 바이너리를 사용하도록 보장합니다. 이는 제대로 된 퍼징에 필요한 최신 clang 기능을 사용할 수 있게 해 줍니다.설치 중 문제가 발생하면 다음 명령어를 실행하여 디버깅 출력을 확인할 수 있습니다:
RUZZY_DEBUG=1 gem install --verbose ruzzy
순수 Ruby 퍼징 대상이 정규식을 많이 사용한다면 regexp_parser를 설치하세요:
gem install regexp_parser
설치 후 Ruzzy는 퍼징 중 만나는 정규식을 샘플링(즉, 해결)하기 위해 이 기능을 자동으로 사용합니다. 이를 통해 퍼저는 정규식 조건을 통과하여 추가 커버리지를 확보할 수 있습니다.
Ruzzy는 동작 방식을 보여주는 예제를 포함하고 있습니다. 먼저 다음 환경 변수를 설정하세요:
export ASAN_OPTIONS="allocator_may_return_null=1:detect_leaks=0:use_sigaltstack=0"
ASAN_OPTIONS그런 다음 다음 명령어로 예제를 실행할 수 있습니다:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
LD_PRELOAD는 Atheris와 동일한 이유로 필요합니다. 그러나 ASAN_OPTIONS와 달리 다른 프로그램을 방해할 수 있으므로 export하지 않는 것이 좋습니다.
그러면 곧 다음과 같은 크래시가 생성됩니다:
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2527961537
...
==45==ERROR: AddressSanitizer: heap-use-after-free on address 0x50c0009bab80 at pc 0xffff99ea1b44 bp 0xffffce8a67d0 sp 0xffffce8a67c8
...
SUMMARY: AddressSanitizer: heap-use-after-free /var/lib/gems/3.1.0/gems/ruzzy-0.8.0/ext/dummy/dummy.c:18:24 in _c_dummy_test_one_input
...
==45==ABORTING
MS: 4 EraseBytes-CopyPart-CopyPart-ChangeBit-; base unit: 410e5346bca8ee150ffd507311dd85789f2e171e
0x48,0x49,
HI
artifact_prefix='./'; Test unit written to ./crash-253420c1158bc6382093d409ce2e9cff5806e980
Base64: SEk=
메모리 위반을 일으킨 입력("HI")을 정확히 찾아냈음을 확인할 수 있습니다. 이 위반이 발생한 이유에 대한 자세한 내용은 dummy.c를 참조하세요.
다음 명령어로 크래시 케이스를 다시 실행할 수 있습니다:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy' \
./crash-253420c1158bc6382093d409ce2e9cff5806e980
다음과 같은 sanitizer를 사용할 수 있습니다:
Ruzzy::ASAN_PATH — AddressSanitizer용Ruzzy::UBSAN_PATH — UndefinedBehaviorSanitizer용예시로 작은 Ruby 스크립트를 퍼징해 보겠습니다. 순수 Ruby 코드를 퍼징하려면 트레이서 스크립트(tracer script)와 퍼징 하니스(fuzzing harness)라는 두 개의 Ruby 스크립트가 필요합니다. 트레이서 스크립트가 필요한 이유는 Ruby 인터프리터의 구현 세부 사항 때문입니다.
먼저 트레이서 스크립트를 작성합니다. 이름을 test_tracer.rb라고 하겠습니다:
# frozen_string_literal: true
require 'ruzzy'
Ruzzy.trace('test_harness.rb')
다음으로 퍼징 하니스를 작성합니다. 이름을 test_harness.rb라고 하겠습니다:
# frozen_string_literal: true
require 'ruzzy'
def fuzzing_target(input)
if input.length == 4
if input[0] == 'F'
if input[1] == 'U'
if input[2] == 'Z'
if input[3] == 'Z'
raise
end
end
end
end
end
end
test_one_input = lambda do |data|
fuzzing_target(data) # Your fuzzing target would go here
return 0
end
Ruzzy.fuzz(test_one_input)
다음 명령어로 이 파일을 실행하여 퍼징을 시작할 수 있습니다:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby test_tracer.rb
그러면 곧 다음과 같은 크래시가 생성됩니다:
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2311041000
...
/app/ruzzy/bin/test_harness.rb:12:in `block in <top (required)>': unhandled exception
from /var/lib/gems/3.1.0/gems/ruzzy-0.8.0/lib/ruzzy.rb:15:in `c_fuzz'
from /var/lib/gems/3.1.0/gems/ruzzy-0.8.0/lib/ruzzy.rb:15:in `fuzz'
from /app/ruzzy/bin/test_harness.rb:35:in `<top (required)>'
from bin/test_tracer.rb:7:in `require_relative'
from bin/test_tracer.rb:7:in `<main>'
...
SUMMARY: libFuzzer: fuzz target exited
MS: 1 CopyPart-; base unit: 24b4b428cf94c21616893d6f94b30398a49d27cc
0x46,0x55,0x5a,0x5a,
FUZZ
artifact_prefix='./'; Test unit written to ./crash-aea2e3923af219a8956f626558ef32f30a914ebc
Base64: RlVaWg==
예외를 발생시킨 입력("FUZZ")을 정확히 찾아냈음을 확인할 수 있습니다.
자신의 대상을 퍼징하려면 test_one_input lambda를 수정하여 대상 함수를 호출하도록 하세요.
예시로 msgpack-ruby 라이브러리를 퍼징해 보겠습니다. 먼저 gem을 설치하세요:
MAKE="make --environment-overrides V=1" \
CC="/path/to/clang" \
CXX="/path/to/clang++" \
LDSHARED="/path/to/clang -shared" \
LDSHAREDXX="/path/to/clang++ -shared" \
CFLAGS="-fsanitize=address,fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g" \
CXXFLAGS="-fsanitize=address,fuzzer-no-link -fno-omit-frame-pointer -fno-common -fPIC -g" \
gem install msgpack
Ruzzy 컴파일 시 사용된 환경 변수 외에도 CFLAGS와 CXXFLAGS를 지정하고 있습니다. 이 플래그들은 퍼징 과정을 돕습니다. address sanitizer와 같은 유용한 기능과 개선된 스택 트레이스 정보를 활성화합니다. 자세한 내용은 AddressSanitizerFlags를 참조하세요.
다음으로 msgpack용 퍼징 하니스가 필요합니다. 아래 코드는 libFuzzer 경험이 있는 분들에게 익숙할 것입니다:
# frozen_string_literal: true
require 'msgpack'
require 'ruzzy'
test_one_input = lambda do |data|
begin
MessagePack.unpack(data)
rescue Exception
# We're looking for memory corruption, not Ruby exceptions
end
return 0
end
Ruzzy.fuzz(test_one_input)
이 파일의 이름을 fuzz_msgpack.rb라고 하겠습니다. 다음 명령어로 이 파일을 실행하여 퍼징을 시작할 수 있습니다:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb
libFuzzer 옵션은 다음과 같이 Ruby 스크립트에 전달할 수 있습니다: