
순수 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 스크립트에 전달할 수 있습니다:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby fuzz_msgpack.rb /path/to/corpus
자세한 내용은 libFuzzer options을 참조하세요.
자신의 대상을 퍼징하려면 test_one_input lambda를 수정하여 대상 함수를 호출하도록 하세요.
Ruzzy 모듈은 최상위 진입점들을 제공합니다.
Ruzzy::FuzzedDataProvider는 원시 퍼저 바이트를 분할하여 타입이 지정된 Ruby 값으로 변환합니다.
test_one_input = lambda do |data|
fdp = Ruzzy::FuzzedDataProvider.new(data)
name = fdp.consume_random_length_string(50)
age = fdp.consume_int_in_range(0, 150)
score = fdp.consume_float_in_range(0.0, 100.0)
role = fdp.pick_value_in_list(['admin', 'user', 'guest'])
User.new(name: name, age: age, score: score, role: role).validate!
end
Ruzzy.fuzz(test_one_input)
모든 메서드는 데이터가 소진되면 기본값(0, "", false, min)을 반환합니다.
macOS에서 Ruzzy를 사용하려면 Homebrew로 설치한 LLVM(Apple Clang에는 libFuzzer가 포함되어 있지 않음)과 시스템 Ruby가 아닌 Ruby가 필요합니다(/usr/bin/ruby에 있는 시스템 Ruby는 SIP로 보호되어 Ruby가 시작되기 전에 DYLD_* 환경 변수가 제거됩니다).
brew install llvm ruby
시스템 Ruby가 아닌 어떤 Ruby든 사용할 수 있습니다(brew, rbenv, asdf). 다만 shim 기반 버전 관리자에 대해서는 아래 주의 사항을 참조하세요.
Homebrew Clang 경로와 macOS에 적합한 링커 플래그를 사용하세요:
MAKE="make --environment-overrides V=1" \
CC="$(brew --prefix llvm)/bin/clang" \
CXX="$(brew --prefix llvm)/bin/clang++" \
LDSHARED="$(brew --prefix llvm)/bin/clang -dynamic -bundle -undefined dynamic_lookup" \
LDSHAREDXX="$(brew --prefix llvm)/bin/clang++ -dynamic -bundle -undefined dynamic_lookup" \
gem install ruzzy
LD_PRELOAD 대신 DYLD_INSERT_LIBRARIES를 사용하세요:
DYLD_INSERT_LIBRARIES=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
ruby -e 'require "ruzzy"; Ruzzy.dummy'
macOS에서 Ruzzy::ASAN_PATH와 Ruzzy::UBSAN_PATH는 .dylib 파일로 확인됩니다.
asdf, rbenv)은 DYLD_* 환경 변수를 제거합니다. 이러한 shim은 #!/usr/bin/env bash를 사용하며 /usr/bin/env는 SIP로 보호되므로 macOS는 Ruby가 시작되기 전에 DYLD_INSERT_LIBRARIES를 제거합니다. Homebrew Ruby(shim이 없음)를 사용하거나 설치된 Ruby 바이너리의 절대 경로를 호출하세요:
DYLD_INSERT_LIBRARIES=$(/path/to/ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
/path/to/ruby your_fuzzer.rb
DYLD_INSERT_LIBRARIES로 ASan dylib을 주입할 때 시작 중 프로세스가 멈추는 버그가 있습니다. Ruzzy가 실행 시 멈춘다면 Homebrew LLVM을 업데이트하세요(brew upgrade llvm).Ruzzy로 발견된 버그:
toml gem: #76toml-rb gem: #150ox gem: #351, #410Marshal 가비지 컬렉터 크래시: #20941redcarpet gem: #813개발은 로컬에서 하거나 이 저장소에 제공된 Dockerfile을 사용하여 수행할 수 있습니다.
다음 명령어로 Ruzzy Docker 이미지를 빌드할 수 있습니다:
docker build --tag ruzzy .
그런 다음 다음 명령어로 컨테이너에 셸로 접속할 수 있습니다:
docker run -it -v $(pwd):/app/ruzzy --entrypoint /bin/bash ruzzy
Ruzzy의 C 확장을 컴파일하기 위해 rake-compiler를 사용합니다.
컨테이너 내부에서 다음 명령어로 C 확장을 컴파일할 수 있습니다:
rake compile
Ruby 코드를 테스트하기 위해 rake 단위 테스트를 사용합니다.
컨테이너 내부에서 다음 명령어로 테스트를 실행할 수 있습니다:
LD_PRELOAD=$(ruby -e 'require "ruzzy"; print Ruzzy::ASAN_PATH') \
rake test
Ruby 코드 린팅에 rubocop을 사용합니다.
컨테이너 내부에서 다음 명령어로 rubocop을 실행할 수 있습니다:
rubocop
Ruzzy는 새 git 태그가 푸시되면 RubyGems에 자동으로 릴리스됩니다.
새 버전을 릴리스하려면 다음 명령어를 실행하세요:
git tag vX.X.X
git push --tags
| 메서드 | 설명 |
|---|
Ruzzy.fuzz(test_one_input, args = DEFAULT_ARGS) | test_one_input(원시 바이트를 받는 proc/lambda)을 퍼징합니다. |
Ruzzy.trace(harness_script) | harness_script를 Ruby 분기 커버리지 계측으로 감싼 후 require합니다. 순수 Ruby 퍼징에 필요합니다. |
Ruzzy.dummy | 번들로 제공되는 예제 하니스를 퍼징합니다(heap-use-after-free 데모). |
Ruzzy.dummy_test_one_input(data) | 예제 하니스 자체입니다. |
| 상수 | 설명 |
|---|
Ruzzy::ASAN_PATH | ASan + 퍼저 래퍼의 경로. LD_PRELOAD(Linux) / DYLD_INSERT_LIBRARIES(macOS)와 함께 사용합니다. |
Ruzzy::UBSAN_PATH | UBSan용 동일한 래퍼. |
Ruzzy::EXT_PATH | ext/cruzzy 빌드 디렉터리의 경로. |
Ruzzy::DEFAULT_ARGS | 퍼저에 전달되는 기본 인자. |
| 메서드 | 설명 | 반환 값 |
|---|
remaining_bytes | 소비되지 않고 남은 바이트 수 | Integer |
consume_bytes(count) | 최대 count개의 원시 바이트 소비 | binary String |
consume_random_length_string(max_length) | 가변 길이 문자열을 소비하며, \ + 비\ 바이트에서 종료 | String |
consume_remaining_bytes | 남은 모든 바이트 소비 | binary String |
consume_remaining_as_string | consume_remaining_bytes의 별칭 | binary String |
consume_uint(count) | count 바이트에서 얻은 부호 없는 정수 | Integer |
consume_int(count) | count 바이트에서 얻은 부호 있는(2의 보수) 정수 | Integer |
consume_int_in_range(min, max) | [min, max] 범위에서 균일하게 분포하는 정수 | Integer |
consume_bool | 한 바이트(LSB)에서 얻은 불리언 | true/false |
consume_float | 전체 double 범위에 걸친 Float | Float |
consume_float_in_range(min, max) | [min, max] 범위의 Float | Float |
consume_probability | [0.0, 1.0] 범위의 Float | Float |
pick_value_in_list(list) | list에서 무작위 요소 선택 | 요소 |