이것저것/논문 리뷰

[NDSS] WINNIE: Fuzzing Windows Applicationswith Harness Synthesis and Fast Cloning

https://www.ndss-symposium.org/wp-content/uploads/ndss2021_6A-3_24334_paper.pdf 내용 정리

1. Background

1.1. 현재 상황

  • `Unix-like`OS 들에 특화
  • Windows system에서 발생하는 70%의 보안 취약점들은 “memory safety issues”

주로 ‘end-user system’을 사용하기 때문에 더욱 공격자들에게 매력적인 target이 될 수 있다.

 

1.2. Windows가 Fuzzing에 최적화되지 않은 이유

현재의 Windows Application들은 fuzzing하기에 적합하지 않다. Windows program 또한 memory safety issue에 대해 취약한 점들이 많이 있고, unix system 보다 공격자들의 입장에서 더 공격하고 싶은 타겟임에도 불구하고 Linux system에서의 Fuzzer만큼 효율적인 fuzzer가 없다.

 

1.2.1. A Predominance of Graphical applications

Windows system은 GUI에 강하게, 심하게 의존하고 있다.

  • GUI는 fuzzing에 장애물이 된다.
  • CLI에서 기존에 실행하던 Fuzzer보다 실행속도가 훨씬 느리다.

[EX] XnView.exe

  • input: 사용자로부터 graphical interface를 통해 input값을 요청
  • input 결정 이후, file parsing, library 결정, library 동적 로딩의 과정 필요
  • 어엄청 오래걸린다.

 

1.2.1.1. 해결방법 - harness

GUI의 문제를 해결하기 위해서는 fuzzing을 위해 간단한 프로그램-harness-을 작성해야 한다.

장점

  • input을 command line으로 받으면서 사용자와의 소통은 필요하지 않게 된다.
  • GUI 초기화 등에 낭비되던 자원을 프로그램의 핵심적인 기능들에 집중하는데 사용할 수 있다.

단점: windows fuzzing이 직면하게 되는 딜레마

  • 구현하기 어렵다
  • 전문가 수준의 in-depth 리버스 엔지니어링 기술을 필요로 한다.

(ex.) Fudge, Fuzzgen

open source 프로젝트에 대해 자동으로 harnesses를 생성해준다.

→ Windows는 closed source가 대다수라 어렵다.

 

[EX] XnView.exe

  • dialog window창이 뜨는 것을 걱정할 필요 없이 실행 가능
  • command line으로 input을 받고, decoder library를 로딩

 

1.2.2. A Closed-Source ecosystem

Windows 프로그램들은 여전히 ‘closed-source’, ‘commercial-off-the-shelf(COTS)’ 의 특징을 가지고 있다.

  • <1.2.1.1.>과 같이 “harness”를 구현하더라도 harness는 source code를 주로 필요로 하기 때문에 많은 closed-source인 windows program에 대해 적용하기 어렵다

1.2.2.1. 해결방법

  • binary 파일에서 직접 harness를 퍼징할 수 있도록 개발!

 

1.2.3. The Lack of Fast Cloning Machinery

Windows에는 Linux에서의 fork()와 같이 빠른 cloning machinery가 없다

  • Unix-like OS
  • Windows kernel

2. WINNIE

2.1. WINNIE의 목표

WINNIE의 목표는 1)source code가 없는 상황에서도 fuzzing harnesses를 만드는 과정을 2)자동화하는 것이다. 

 

2.1.1. Fuzzing Harnesses의 과정

높은 수준의 Harness를 생성하기 위해서는 아래의 4가지 단계를 거쳐야한다. 

2.1.1.1. ① Target Discovery

사용자의 input을 처리하는 가장 의미있는 target (function/library)을 찾는 것이다.

  • time consuming; 시간이 많이 든다
  • specified file in a variety of ways; 다양한 방식으로 파일들을 구분할 수 있다

 

2.1.1.2. ② Call-sequence recovery

  • target library와 연관된 모든 함수들의 호출 순서를 올바르게 해야한다.

 

2.1.1.3. ③ Argument recovery

harness는 각 함수 호출에 대해 알맞은 인자들을 전달해야한다.

  • 함수가 어떤 일을 할지를 예상 및 결정하여, 각 API에 대한 모든 인자들을 조사해야한다.

 

2.1.1.4. ④ Control-flow and data-flow dependency

때로는 함수 호출 리스트를 잘 작성하는 것이 충분하지 않을 때가 있다.

그리고 라이브러리에서 API들 사이에 암시적인 관계를 정의를 하기도 한다.

  • API 들의 관계는 `control-flow`와 `data-flow`에 의존하게 된다.

 

2.1.2. 현재 존재하는 harness의 한계

Windows 시스템에서는 Linux의 fork() 만큼 빠른 process cloning machinery를 제공하지 않기 때문에 fuzzer는 시작할 때마다 완전 처음부터 fuzzing을 시작하게 된다.

  • 프로그램을 재시작하는데에 시간을 굉장히 많이 쓰게 된다
    → 재시작에 의한 오버헤드↑

현재의 이런 문제를 해결하기 위해 “persistent mode”를 사용한다.

  • 반복할 때 마다 매번 초기화되는 과정을 없애고, 한 번의 프로세스에서 루프 내에 있는 함수를 계속 호출한다.
  • 한계점: loop body에 대한 제약이 많다.
  • 부작용: 디버깅하기 어렵고 우회하기 어렵다. 

타겟 함수가 호출한 부분으로 리턴하지 않는다면 (-> loop가 끝나게 된다면) “persistent mode”를 지속할 수 없다.

파일을 열 때 exclusive mode로 열고는 닫지 않는다. 

 

2.1.3. WINNIE에서는?

  • harness generator: closed-source인 Windows 프로그램들을 대상으로 harness generator 구현
  • fork implementation: fuzzing을 할 때에 효율적으로 실행될 수 있도록 하는 fork 구현

프로그램과 샘플 입력 값이 주어졌을 때, WINNIE의 tracer는 프로그램을 실행하면서 API 호출, 인수, 메모리 내용 등 대상 응용 프로그램에 대한 동적 정보를 수집한다.

⇒ 각 퍼징 타겟에 대해서 harness generator는 trace를 추적하고, 관련된 API 시퀀스를 작업 harness로 사용한다. 그리고 이 생성된 harness들은 효율성과 견고함(안정성)을 시험해보고, 버그를 찾기 위해 우리의 fork-server를 이용해서 fuzzing instance를 실행시킨다.

 

2.2. Harness Generation

‘XnView’의 코드를 참고하여 미리 4가지 단계를 분석해봤다.

  • target discovery: XnView를 분석해서

2.2.1. ① Fuzzing Target Identification

WINNIE는 이 단계에서 프로그램(target)이 퍼징될 수 있는지 판단하고 의미있는 target function은 어떤 것일지 찾아내려고 한다.

주어진 input 값에 대해 동적 분석을 먼저 시작했고, 아래와 같은 정보들을 얻을 수 있었다.

  • 로드된 모든 모듈의 이름과 base address를 기록
  • 모듈들 간에 call 또는 jump로 copntrol flow가 발생하면, 현재의 스레드 ID, 호출 및 피호출 함수의 주소, 심볼과 인자들을 기록
  • “ret”(return) 명령어를 만나게 되면 리턴 값을 기록
  • 어떠한 값이 되었든 접근가능 메모리에 있다면, 이를 포인터로 취급하고 이후 분석에서 참조된 메모리를 덤프한다.

파일을 스스로 읽거나 열 수 없는 라이브러리를 인식하고, 대신 fd나 내부 메모리 버퍼를 input으로 읽어들인다.

  • tracer 

 

2.2.2. ② Call-sequence Recovery

fuzzing할 대상인 기능에 올바르게 도달하고 trigger할 수 있는 일련의 API 호출을 재현하는 단계다.

  • “harness skeleton” = trigger를 위해 필요한 일련의 API 호출 관계
  • 해당 라이브러리(target?!)에 연관된 함수 호출들을 추적하고 이를 harness skeleton에 복사 저장한다.
  • hybrid 분석(static+dynamic)을 통해 함수의 prototype을 복원하고 재설계한다.
  • 다중 스레드를 사용하는 응용 프로그램의  경우 더욱 주의해야한다!

 

2.2.3. ③ Argument Recovery

트레이스에 기록된 일반 값들로 전달된 인자값들을 변수와 상수들로 상징화한다.

  1. pointer인자값들을 식별해낸다.
  2. 인자값이 고정적(static)인지 혹은 동적인지(변하는지)한지 결정한다.

 

2.2.4. ④ Control-flow and Data-flow Reconstruction

harness에서 control-flow와 data-flow를 반영해 프로그램을 분석한다.

  • Control-flow dependency
  • Data-flow dependency

 

2.2.5. ⑤ Harness Validation and Finalization

WINNIE는 불확실한 harness를 제시했을 경우, 더 나은 방향을 제공하기 위해 사용자에게 도움을 요청한다.

  • 두 번째 API의 호출 지점과 첫 번째 API의 호출 지점이 많이 떨어져 있는 API 호출을 기록한다.

(논리적 버그 발생?!)

  •  사용자에게 코드(를 가리키는) 포인터 인수를 강조 표시한다.
  • harness를 제작하는 동안 파일 작업이 중요하기 때문에 파일 작업과 관련한 정보를 제공한다.



2.2.6. [Evaluation] Efficacy of Harness Generation 

- “how well WINNIE helps users create effective fuzzing harnesses”

 

강점

  • 실행 추적기(tracer)는 target 프로그램에 대해 의미있는 fuzzing target 등의 유용한 정보를 많이 제공한다.
  • WINNIE에서 생성하는 API 시퀀스들도 사용자에게 유용한 정보를 많이 제공한다

→ 사용자의 시간을 아껴준다.

 

약점

  • 대부분의 harness 들은 큰 변화없이 사용할 수 있었지만, ACDSee와 HWP-jpeg은 특히나 수정을 많이 해야했다.
  • 디컴파일 코드를 추가해서 수동적으로 콜백 함수를 추출해야 했다.

2.3. Fast Process Cloning on Windows

실제로는 Windows에도 ‘fork()’는 있지만, 안정적인 구현이 어렵기 때문에 사용되지 않아왔던 것이다. 보다 효과적인 fuzzing을 위해, Windows API와 서비스들의 내부를 역공학을 통해 분석하고, 불안정한 요인을 찾아냈다. 

  • Windows Fork()를 구현하고 나면 Windows 환경의 기본 계층을 제어하는 user-mode 프로세스인 ‘CSRSS’와 관련된 문제가 해결된다.
  • process가 CSRSS에 연결되어 있지 않다면 Win32 API에 접근하려고 할 때 process는 중단된다.

WINNIE에서 사용하기 위해 수정한 fork()

  • CSRSS에 새로 생성된 자식 프로세스를 올바르게 알려준다.
  • CSRSS에 연결하고 API 호출에 성공하려면 하위 프로세스가 연결되기 전에 문서화되어있지 않은 여러 변수들을 수동을 초기화 취소를 해야한다.
  • WINNIE에서 제안하는 fork의 최종적인 목표는 전부 재실행되는 것을 막는 것이다.

 

[+] CSRSS
Client/Server Runtime SubSystem; 클라이언트/서버 런타임 하위 시스템
윈도우 시스템에서 기본으로 동작하는 프로세스로, 클라이언트의 요청에 대한 작업자 스레드를 만들거나 삭제한다. 
메모리에 상주하면서 여러 프로세스로 실행되기 때문에 항상 주의해야한다.

 

2.3.1. Verifying the Fork implementation (Fork 구현)

  1. 만든 fork-server 아래에서 몇 가지 테스트 프로그램들을 실행시켜 정확성을 확인한다.
    • 자식 프로세스가 글로벌(전역) 프로그램 상태의 올바른 복사본을 수신하는지 확인한다.
    • fork의 구현이 CoW (Copy-on-Write)인지 확인하기 위해 forking 전에 상위(부모) 프로세스에서 대량의 메모리를 초기화 시킨다
      =>fork가 CoW 방식으로 작동하는 것으로 결론
  2. WinAFL에 내장된 테스트 프로그램으로 간단히 fork의 속도를 측정한다.
    • 여전히 Linux 시스템에서의 fork()보다는 속도가 느리지만, 재실행되는 것을 방지함으로써 시간을 아낄 수 있게 되었다. 

2.3.2. Idiosyncrasies of Windows Fork (Windows Fork의 특이한 점)

구현한 fork는 Windows 운영체제의 설계로 인해 몇 가지 특이점들이 있다.

  • 상위 프로세스에 다중 스레드가 있는 경우, fork를 호출하는 스레드만 복사된다.
  • Unix fd(file descriptor)와 동등한 Windows 윈도우의 개체(객체)들은 기본적으로 하위 프로세스에서 상속되지 않는다.
  • fork와 관련한 API들에 포함되는 데이터의 구조는 Windows 버전에 따라 다르기 때문에 가능한 모든 Windows의 설치를 지원하는 것은 거의 불가능하다.

 

2.3.3. [Evaluation] Benefits of Fork

  • 구현한 fork가 fuzzing을 훨씬 효율적으로 만드는지
  • 구현한 fork가 fuzzing을 훨씬 안정적으로 만드는지

 

SMALL