mirror of
https://github.com/Kowntaewook/MCPTrace.git
synced 2026-11-02 04:45:09 +01:00
Merge pull request #3 from Kowntaewook/test/real-linux-ram
실제 Windows·Linux RAM 검증 및 큰 빈 주소 영역 읽기 개선
This commit is contained in:
@@ -10,5 +10,8 @@ dist/
|
||||
*.vmem
|
||||
*.dmp
|
||||
*.mem
|
||||
*.lime
|
||||
*.vmsn
|
||||
*.vmss
|
||||
evidence/
|
||||
output/
|
||||
|
||||
@@ -4,9 +4,15 @@ Windows·Linux 프로세스 메모리에서 MCP/JSON-RPC 메시지 후보를 추
|
||||
수집된 메모리 이미지를 분석하고, 메시지 원문·주소·도구 인자·정책 검사 결과를 남깁니다.
|
||||
|
||||
**상태: 초기 구현.** 프레임워크 패키지 버전은 `volatility3==2.28.2`로 고정했습니다.
|
||||
분석 대상은 Windows x64와 Linux x86-64입니다. Windows의 첫 실증 대상은 Windows 11 x64입니다.
|
||||
현재 검증은 합성 데이터, 실제 Volatility 메모리 레이어·출력기, CLI 플러그인 로딩까지입니다.
|
||||
실제 Windows/Linux RAM 이미지에서의 복원율과 OS 빌드·커널별 호환성은 아직 검증하지 않았습니다.
|
||||
분석 대상은 Windows x64와 Linux x86-64입니다. 합성 테스트와 함께 공개 Windows 11 x64
|
||||
빌드 22000 이미지 1개와 Linux 이미지 2개를 시험했습니다.
|
||||
Windows에서는 선택한 프로세스 4개의 VAD 1,561개를 검사했고 MCP 후보는 0개였습니다.
|
||||
[Windows 실제 RAM 검증](docs/windows-memory-validation.md)에는 실행 범위와 대형 빈 주소 영역
|
||||
읽기 개선을 기록했습니다. Linux MCP 활동 샘플의 Python 프로세스
|
||||
2개에서 후보 41개를 추출하고 원문·해시·VMA 범위를 다시 대조했습니다. 이 샘플은 VMware
|
||||
메타데이터가 없어 물리 메모리 배치를 추정해 변환한 조건부 결과입니다.
|
||||
[실제 RAM 검증 기록과 재현 절차](docs/real-memory-validation.md)를 참고하세요.
|
||||
전체 메시지 복원율·정밀도, Windows의 실제 MCP 메시지 추출, 다른 OS 빌드의 호환성은 아직 검증하지 않았습니다.
|
||||
|
||||
| 분석할 이미지 | 명령 | 프로세스 메모리 영역 |
|
||||
| --- | --- | --- |
|
||||
@@ -22,6 +28,8 @@ Windows·Linux 프로세스 메모리에서 MCP/JSON-RPC 메시지 후보를 추
|
||||
- Linux는 프로세스 대표 태스크만 열거하고 Volatility의 `get_vma_iter()`로 메모리 영역 접근.
|
||||
사용자 주소 공간이 없는 태스크와 프로세스 레이어를 만들지 못한 태스크는 건너뛴 이유를 기록.
|
||||
- 문자열 안의 중괄호, 이스케이프, 중첩 객체, 청크 경계 처리. 누락된 페이지와 VAD/VMA 경계는 연결하지 않음.
|
||||
- Intel 페이지 테이블에서 부재가 확인된 큰 영역은 경계까지 건너뛰고 누락 바이트를 집계.
|
||||
물리 메모리 읽기 실패는 페이지 단위로 재시도해 일부 남아 있는 데이터를 보존.
|
||||
- MCP 메서드, `tools/call` 도구명과 인자, 응답의 `result`/`error` 보존.
|
||||
- PID·VAD/VMA 시작 주소·프로세스 가상 주소·인코딩·원문 SHA-256 표시.
|
||||
- 동일 PID와 **타입까지 같은 요청 ID**를 기준으로 요청·응답 후보 제시. ID 충돌은 모호함으로 표시.
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# 공개 Linux RAM 이미지 검증
|
||||
|
||||
검증일: 2026-10-27. 분석 코드: `62b0f655c35044869400577cdaf53b5a3472a4c7` (v0.2.0).
|
||||
분석 호스트: Ubuntu 24.04.3, Python 3.12.14, `volatility3==2.28.2`.
|
||||
아래 최초 실행에서는 플러그인 코드를 수정하지 않았습니다. 이후
|
||||
[Windows 실제 RAM 검사](windows-memory-validation.md)에서 큰 빈 주소 영역 읽기를 개선한 뒤
|
||||
두 Linux 이미지도 다시 실행했으며, 모든 JSONL 파일이 최초 결과와 바이트 단위로 같았습니다.
|
||||
메모리 내용은 실행하지 않았습니다.
|
||||
|
||||
**결과:** 공개 MCP 활동 샘플의 Python 프로세스 2개에서 완전한 JSON-RPC 형태의 MCP 후보
|
||||
41개를 추출했습니다. 41개 모두 해당 PID의 가상 주소에서 다시 읽은 바이트, SHA-256,
|
||||
VMA 범위와 일치했습니다. 다만 이 샘플의 VMware 메타데이터가 공개되지 않아, 아래의
|
||||
**추정한 물리 메모리 배치**를 적용한 조건부 결과입니다. 복원율이나 정밀도 측정은 아닙니다.
|
||||
|
||||
## 입력과 전처리
|
||||
|
||||
| 입력 | 출처 | 크기 | 확인한 커널 / 역할 |
|
||||
| --- | --- | ---: | --- |
|
||||
| 13Cubed `memory.vmem` + `memory.vmsn` | [제작자 설명](https://www.youtube.com/watch?v=IHd85h6T57E), [원본 ZIP](https://cdn.13cubed.com/downloads/linux_challenge.zip) | VMEM 4,294,967,296 bytes; VMSN 5,567,519 bytes | Ubuntu 6.5.0-41-generic, 실제 VMware 메타데이터를 사용한 실행 확인 |
|
||||
| MCPRecon `sample.vmem` | [저자 저장소 README](https://github.com/BiTLab-BaggiliTruthLab/MCPRecon), [저자가 연결한 샘플](https://drive.google.com/file/d/16d84BfG19auxwERKwajmNnDD0gVYo2Q9/view) | 8,589,934,592 bytes | Ubuntu 6.14.0-37-generic, MCP 후보 추출 확인 |
|
||||
|
||||
13Cubed ZIP의 CRC 검사를 통과한 뒤 전체 VMEM을 추출했습니다. 이 이미지는 2024-07-03
|
||||
프로세스 생성 시각을 포함하며, MCP 통신 정답 자료는 없습니다. 아래에서 선택한 프로세스에서
|
||||
후보가 나오지 않았다는 사실만 기록하며, 이미지 전체가 MCP 음성이라고 단정하지 않습니다.
|
||||
|
||||
MCPRecon 원본 VMEM만 열면 VMware 메타데이터 누락 경고가 발생하고, 기본 `linux.pslist`
|
||||
결과가 **종료 코드 0이지만 0행**이었습니다. 이를 성공한 검사로 간주하지 않았습니다.
|
||||
덤프 안에는 6.14.0-36과 6.14.0-37 배너가 모두 있었으며, 아래 배치를 적용한 뒤 6.14.0-37
|
||||
심볼로 프로세스 목록과 VMA 목록을 읽을 수 있었습니다.
|
||||
|
||||
원본에서 확인한 BIOS-e820 텍스트의 주요 범위:
|
||||
|
||||
```text
|
||||
BIOS-e820: [mem 0x0000000000100000-0x00000000bfecffff] usable
|
||||
BIOS-e820: [mem 0x00000000bff00000-0x00000000bfffffff] usable
|
||||
BIOS-e820: [mem 0x0000000100000000-0x000000023fffffff] usable
|
||||
```
|
||||
|
||||
이 단서와 총 크기를 근거로 다음 배치를 **추정**했습니다.
|
||||
|
||||
| 원본 파일 범위 (끝 제외) | 물리 주소 범위 (끝 제외) |
|
||||
| --- | --- |
|
||||
| `[0, 3 GiB)` | `[0, 3 GiB)` |
|
||||
| `[3 GiB, 8 GiB)` | `[4 GiB, 9 GiB)` |
|
||||
|
||||
`scripts/prepare_mcprecon_sample.py`는 정확히 아래 해시의 원본만 허용하며, 모든 원본 바이트를
|
||||
순서대로 보존하고 두 개의 32바이트 LiME 헤더를 추가합니다. 물리 주소 `[3 GiB, 4 GiB)`는
|
||||
누락 범위로 남깁니다. 결과물은 **분석용 파생 컨테이너이며 LiME로 수집한 원본이 아닙니다.**
|
||||
이 추정은 원본 VMSN/VMSS에 의한 확인을 대체하지 않습니다. 다른 VMware 덤프에 일반화하지 마세요.
|
||||
|
||||
SHA-256:
|
||||
|
||||
```text
|
||||
13Cubed ZIP ca7f3f4c4638dcbf4b667e19d4406e4a3c5ca323abb42bf0e2a803ef711e4ec2
|
||||
13Cubed VMEM d718483152a85e82a96c7c260131572556f5aeaf8951f8f73af281fd1840035e
|
||||
13Cubed VMSN 7953e49f2f2ccef880ad5983ee2e491009f93c0c7837daffa5589aa7560c064f
|
||||
MCPRecon VMEM 65b5ccea17c6ce82d978d1b258590a42a2723b13f74b88a5335a7b10401c8bbf
|
||||
MCPRecon 파생 LiME 9c375e111990c55150e6fa42e674e2ef0ddb1e2af1c6c45a4912a58c1d1d6c43
|
||||
```
|
||||
|
||||
심볼은 커뮤니티 [volatility3-symbols](https://github.com/Abyss-W4tcher/volatility3-symbols)의
|
||||
정확한 배너에 해당하는 ISF를 사용했습니다. 배포판 공식 서명 검증을 했다는 의미는 아닙니다.
|
||||
|
||||
| ISF | 다운로드 | SHA-256 |
|
||||
| --- | --- | --- |
|
||||
| Ubuntu 6.5.0-41.41~22.04.2 amd64 | [ISF](https://github.com/Abyss-W4tcher/volatility3-symbols/releases/download/isf-Ubuntu-amd64-6.5.0/Ubuntu_6.5.0-41-generic_6.5.0-41.41.22.04.2_amd64.json.xz) | `6b1b998a6804bce921e32756d3ca2545516a8b9a7b944f0cbcc2f9b0f2ccdc44` |
|
||||
| Ubuntu 6.14.0-37.37~24.04.1 amd64 | [ISF](https://github.com/Abyss-W4tcher/volatility3-symbols/releases/download/isf-Ubuntu-amd64-6.14.0/Ubuntu_6.14.0-37-generic_6.14.0-37.37.24.04.1_amd64.json.xz) | `2230072c4a1565fbf04000a9d0e8c3d01ccb92e410ce0595b353f3f118f23d43` |
|
||||
|
||||
## 실행 결과
|
||||
|
||||
`linux.pslist.PsList`: 13Cubed 344행, 파생 MCPRecon 349행.
|
||||
`linux.proc.Maps`: 아래 사용자 PID에 대해 각각 831개와 360개의 VMA를 열거했습니다.
|
||||
MCPTrace의 `regions_scanned` 합계와 일치합니다. 전체 프로세스를 검사한 것은 아닙니다.
|
||||
|
||||
| 이미지 / PID | 프로세스 | VMA | 읽은 bytes | 읽지 못한 bytes | 표시 후보 |
|
||||
| --- | --- | ---: | ---: | ---: | ---: |
|
||||
| 13Cubed / 4453 | bash | 31 | 4,894,720 | 6,647,808 | 0 |
|
||||
| 13Cubed / 4954 | bash | 31 | 5,238,784 | 6,168,576 | 0 |
|
||||
| 13Cubed / 6742 | sublime_text | 769 | 90,705,920 | 250,605,568 | 0 |
|
||||
| 파생 MCPRecon / 41879 | python | 191 | 64,262,144 | 163,250,176 | 24 |
|
||||
| 파생 MCPRecon / 41880 | python | 169 | 59,805,696 | 164,917,248 | 17 |
|
||||
|
||||
13Cubed PID 2 (`kthreadd`)는 0바이트를 읽고 `skipped_reason=no-user-address-space`로
|
||||
건너뛰었습니다. 선택한 모든 PID의 `error`는 null, `limits_hit`는 빈 배열이었고,
|
||||
앵커/후보 제한 도달 및 잘못된 VMA 카운터는 0이었습니다. 읽지 못한 바이트의 원인을
|
||||
swap·비상주 페이지·누락·매핑 오류 등으로 추가 판별한 것은 아닙니다.
|
||||
|
||||
MCP 후보 41개는 모두 UTF-8 `complete`입니다.
|
||||
|
||||
- 요청 27개, 응답 13개, 알림 1개. `tools/call` 9개 모두 도구명이 `get_current_weather`입니다.
|
||||
- 원문 SHA-256이 서로 다른 바이트열은 33개입니다. 이를 고유 통신 메시지 33개로 간주하지 않습니다.
|
||||
- 연결 표시는 `candidate-only` 13개, `unpaired` 27개, `not-applicable` 1개입니다.
|
||||
`candidate-only`는 메시지 **행** 수이며 연결 쌍 수나 올바른 요청·응답 연결률이 아닙니다.
|
||||
- 별도 검증기는 MCPTrace의 스캐너·어댑터를 사용하지 않고 native Volatility `PsList`와
|
||||
프로세스 레이어로 다시 읽었습니다. PID 41879의 24개 / 8,879바이트와 PID 41880의
|
||||
17개 / 2,151바이트, 합계 **41개 / 11,030바이트**가 원문·해시·VMA 범위 대조를 통과했습니다.
|
||||
주소 변환은 여전히 동일한 Volatility 프레임워크와 위 추정 배치를 사용합니다.
|
||||
- 검증기의 실패 경로도 확인했습니다. 내보낸 후보의 한 바이트와 그 SHA-256을 함께 바꾼 복사본은
|
||||
원래 프로세스 메모리와 달라 검증기가 `process memory bytes differ`로 거부했습니다.
|
||||
|
||||
검사는 기본 옵션으로 실행했습니다: `max-object-bytes=65536`, `max-records=10000`,
|
||||
`max-bytes=0`, partial/RPC 전체 표시/정책 검사 끔, JSONL 내보내기 켬.
|
||||
한 번의 CLI 실행 경과 시간은 13Cubed 11.29초, 파생 MCPRecon 11.68초였습니다.
|
||||
다운로드·변환·해시·별도 대조 시간은 제외하며 캐시가 있는 공유 실행 환경의 관측값입니다.
|
||||
8 GiB 전체를 11.68초에 검사했다는 뜻이나 처리 속도 벤치마크가 아닙니다.
|
||||
|
||||
## 재현
|
||||
|
||||
저장소 설치 후 루트에서 Bash로 실행합니다. `.venv/bin`을 PATH에 추가합니다.
|
||||
예시 디렉터리의 원본 파일과 위 ISF를 먼저 준비해야 합니다. 심볼은 `symbols/linux/` 안에 둡니다.
|
||||
원본 12 GiB와 파생 파일 약 8 GiB, 다운로드 압축파일 등을 위한 공간이 필요합니다.
|
||||
|
||||
```bash
|
||||
export PATH="$PWD/.venv/bin:$PATH"
|
||||
mkdir -p cache symbols/linux output/13cubed output/mcp
|
||||
|
||||
# evidence/13cubed/에 memory.vmem과 같은 이름의 memory.vmsn을 함께 둡니다.
|
||||
vol -q --offline --cache-path cache -s symbols -f evidence/13cubed/memory.vmem -r json linux.pslist.PsList > output/13cubed/pslist.json
|
||||
vol -q --offline --cache-path cache -s symbols -f evidence/13cubed/memory.vmem -r json linux.proc.Maps --pid 4453 4954 6742 > output/13cubed/maps.json
|
||||
vol -q --offline --cache-path cache -s symbols -p plugins -f evidence/13cubed/memory.vmem -o output/13cubed -r json linux.mcptrace.MCPTrace --pid 2 4453 4954 6742 --dump > output/13cubed/findings.json 2> output/13cubed/stderr.txt
|
||||
|
||||
# 이 해시의 공개 샘플에만 적용되는 추정 배치입니다. 원본은 덮어쓰지 않습니다.
|
||||
python scripts/prepare_mcprecon_sample.py evidence/sample.vmem evidence/sample-derived.lime > output/mcp/preparation.json
|
||||
vol -q --offline --cache-path cache -s symbols -f evidence/sample-derived.lime -r json linux.pslist.PsList > output/mcp/pslist.json
|
||||
vol -q --offline --cache-path cache -s symbols -f evidence/sample-derived.lime -r json linux.proc.Maps --pid 41879 41880 > output/mcp/maps.json
|
||||
vol -q --offline --cache-path cache -s symbols -p plugins -f evidence/sample-derived.lime -o output/mcp -r json linux.mcptrace.MCPTrace --pid 41879 41880 --dump > output/mcp/findings.json 2> output/mcp/stderr.txt
|
||||
python scripts/verify_linux_export.py --image evidence/sample-derived.lime --symbols symbols --cache cache --require-artifacts output/mcp/mcptrace.pid-41879.jsonl output/mcp/mcptrace.pid-41880.jsonl > output/mcp/verified.json
|
||||
```
|
||||
|
||||
새 출력 폴더에서 실행하세요. 기존 JSONL이 있으면 Volatility가 이름에 접미사를 붙이므로,
|
||||
재실행 시 해당 실행의 `scan-summary.export` 파일명을 검증기에 전달해야 합니다.
|
||||
덤프, 심볼, 원문 메시지가 든 출력 파일은 저장소에 넣지 않습니다.
|
||||
|
||||
## 아직 확인하지 못한 것
|
||||
|
||||
실제 송수신 메시지 정답 집합과 수집 지연·작업량 조건을 대조하지 않았으므로 정밀도, 재현율,
|
||||
오연결률, 인자 부분 복원률을 주장할 수 없습니다. JSON은 로그·캐시·예제에서 왔을 수도 있습니다.
|
||||
Windows의 실제 MCP 메시지 추출, 기존 mmap 기반 Linux 커널, 다른 배포판·빌드, UTF-16 실제 사례,
|
||||
부분 조각 추출과 사용자 정책의 실제 사건 판정도 이번 실행 범위 밖입니다.
|
||||
이 결과는 선택한 프로세스에서의 추출 경로 확인이며, 운영 환경 전반의 검증 완료 선언이 아닙니다.
|
||||
+10
-4
@@ -1,7 +1,10 @@
|
||||
# 검증 범위
|
||||
|
||||
이 문서는 실제 Windows/Linux 호환성 검증을 수행했다는 보고서가 아닙니다.
|
||||
현재 테스트는 알려진 바이트를 메모리 레이어에 넣어 추출 결과와 비교합니다.
|
||||
이 문서는 자동 테스트의 범위와 실제 이미지 검증 절차를 설명합니다.
|
||||
공개 Linux 이미지 2개에 대한 실행 결과는 [실제 RAM 검증 기록](real-memory-validation.md)에
|
||||
별도로 기록했습니다. MCP 활동 샘플은 누락된 VMware 메타데이터를 대신해 추정한 물리 메모리
|
||||
배치를 적용했습니다. [Windows 11 실제 이미지 검증](windows-memory-validation.md)도 수행했으나,
|
||||
그 이미지에서는 MCP 후보가 나오지 않았습니다. 전체 메시지 기준 성능 평가는 아직 수행하지 않았습니다.
|
||||
|
||||
## 자동 검증
|
||||
|
||||
@@ -16,6 +19,7 @@ CI는 Ubuntu/Windows 분석 호스트와 Python 3.10/3.12 조합을 구성합니
|
||||
| 파싱 | 문자열 내부 중괄호·이스케이프·중첩, 뒤에 배치된 jsonrpc 멤버, 모든 청크 분할 위치 |
|
||||
| 손상 | 중복 키, 잘못된 인코딩, 비정상 JSON-RPC ID, 비유한 숫자 거부; 조각에서 필드 추정 금지 |
|
||||
| 메모리 경계 | 실제 BufferDataLayer 위의 누락 페이지 시뮬레이션, 인접 VAD/VMA 비결합, Linux vm_end 배제 |
|
||||
| Intel 페이지 변환 | 실제 Intel32e와 구성한 페이지 테이블로 2 TiB 누락 영역 건너뛰기, 예산 경계, 일부만 수집된 큰 페이지 보존 |
|
||||
| 후보 연결 | PID와 숫자/문자열 ID 분리, 복사본 보존, ID 충돌, 부동소수점 반올림으로 인한 오병합 방지 |
|
||||
| 정책 | Windows/POSIX 경로 경계·대소문자·구분자, URL 사용자 정보·가짜 접미사, JSON Pointer, 미지원 입력·잘못된 규칙 |
|
||||
| 어댑터 | 실제 TreeGrid, 프로세스별 제한·요약, 뒤쪽 VAD/VMA 읽기 오류 이후 기존 증거 보존 |
|
||||
@@ -48,7 +52,7 @@ Windows의 기존 `VADStart`/`vad_start` 출력은 회귀 검사하며 Linux 출
|
||||
|
||||
재현 자료에는 명령 전체, MCPTrace 커밋, Python/Volatility 버전, OS 빌드, 정책 파일 및 그 해시,
|
||||
입력 이미지 해시, 표준 오류 로그, JSONL을 포함합니다. 실제 증거·메모리 덤프는 저장소에 커밋하지 않습니다.
|
||||
실제 이미지 결과가 확보되기 전에는 특정 클라이언트 지원, 정밀도·재현율, 세션 복원 성공률을 표시하지 않습니다.
|
||||
기준 메시지와 수집 조건이 확보되기 전에는 특정 클라이언트 지원, 정밀도·재현율, 세션 복원 성공률을 표시하지 않습니다.
|
||||
|
||||
## 실제 Linux x86-64 이미지 확인 절차
|
||||
|
||||
@@ -68,5 +72,7 @@ Windows의 기존 `VADStart`/`vad_start` 출력은 회귀 검사하며 Linux 출
|
||||
크기·후보 제한과 검사 오류가 요약에 표시되는지 확인합니다.
|
||||
|
||||
기존 mmap 기반 커널과 Maple Tree 기반 커널은 각각 별도의 실제 이미지 검증이 필요합니다.
|
||||
현재는 Volatility 2.28.2의 `get_vma_iter()` API를 이용하는 구현과 합성 어댑터 테스트까지 수행했습니다.
|
||||
Volatility 2.28.2의 `get_vma_iter()` API 구현·합성 어댑터 테스트와 함께, 실제 6.5.0-41 및
|
||||
물리 배치를 추정한 6.14.0-37 이미지에서 Maple Tree 경로를 실행했습니다. 기존 mmap 기반 커널의
|
||||
실제 이미지는 아직 검증하지 않았습니다.
|
||||
컨테이너 PID 네임스페이스 매핑, ARM/32비트, swap 복원, 힙 객체 재조립은 이번 검증 범위 밖입니다.
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# Windows 11 실제 RAM 검증
|
||||
|
||||
검증일: 2026-10-27. 분석 호스트는 Ubuntu 24.04.3, Python 3.12.14이며
|
||||
`volatility3==2.28.2`를 사용했습니다. 분석 대상 덤프가 Windows이며 분석 호스트는 Linux입니다.
|
||||
|
||||
**결과:** 실제 Windows 11 x64 빌드 22000 이미지에서 사용자 프로세스 4개의 VAD 1,561개를
|
||||
검사했습니다. 읽은 바이트는 361,902,080 (약 345.14 MiB), MCP 후보는 0개입니다.
|
||||
플러그인 실행·VAD 접근·누락 집계·JSONL 출력을 검증한 결과이며,
|
||||
**Windows에서 실제 MCP 메시지를 추출했다는 결과는 아닙니다.**
|
||||
|
||||
## 원본과 심볼
|
||||
|
||||
[PassMark의 Volatility Workbench 공식 배포 페이지](https://www.osforensics.com/tools/volatility-workbench.html)에
|
||||
연결된 [Windows 11 덤프](https://www.osforensics.com/downloads/Win11Dump.zip)를 사용했습니다.
|
||||
ZIP 멤버를 끝까지 읽어 CRC를 확인했고, 추출한 원본에 메시지나 바이트를 주입하지 않았습니다.
|
||||
메모리에 포함된 프로그램은 실행하지 않았습니다.
|
||||
|
||||
| 파일 | 크기 (bytes) | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `Win11Dump.zip` | 1,318,367,225 | `150b62a071debc39651368c89ff2ba990fe825a8363be78af0e7889ce9605412` |
|
||||
| `Win11Dump.mem` | 3,745,951,744 | `0f00815b8f78f30a63d742c73152474b2a3c35183dbb1d92c8fc6a81fa3de9f1` |
|
||||
| 생성된 NT ISF JSON.XZ | 629,352 | `811a6a7dacfac7d447e1d9213d28df7273576fa821b7eb522ae312e9703fc5df` |
|
||||
|
||||
처음 `windows.info.Info` 실행 시 Volatility의 자동 심볼 다운로드·변환을 사용했고,
|
||||
이후 검사는 `--offline`으로 실행했습니다. 생성된 ISF의 식별자는
|
||||
`windows/ntkrnlmp.pdb/8E3373D6124E747F0E72EF8E02E676B3-1.json.xz`입니다.
|
||||
다른 환경에서 ISF를 다시 생성하면 압축 결과의 해시까지 같다는 보장은 없습니다.
|
||||
|
||||
`windows.info.Info` 관측값:
|
||||
|
||||
| 항목 | 값 |
|
||||
| --- | --- |
|
||||
| Is64Bit / MachineType | True / 34404 (AMD64) |
|
||||
| Major/Minor | 15.22000 |
|
||||
| NtMajorVersion / NtMinorVersion | 10 / 0 |
|
||||
| SystemTime | 2022-07-18 23:26:00 UTC |
|
||||
| Kernel Base / DTB | `0xf80029800000` / `0x1ae000` |
|
||||
| 레이어 | WindowsIntel32e → FileLayer |
|
||||
|
||||
`windows.pslist.PsList`는 103행을 반환했습니다. 그중 PID `128017663995251`,
|
||||
이름 `Milliseconds,MS`인 비정상적으로 보이는 행도 포함되어 있어 **103개를 모두 실제 정상
|
||||
프로세스로 확정하지 않았습니다.** 원인은 이번 검사에서 규명하지 않았습니다.
|
||||
아래 선택한 네 PID는 종료 시각이 없고, `windows.vadinfo.VadInfo`에서도 VAD를 열거했습니다.
|
||||
|
||||
## 발견한 읽기 문제와 수정
|
||||
|
||||
Explorer·Teams·Edge의 VAD 목록에서 각각 2 TiB 크기의 가상 주소 예약 영역을 확인했습니다.
|
||||
이 영역은 실제로 2 TiB의 RAM이 수집됐다는 뜻이 아닙니다. 기존 읽기 방식은 청크 읽기 실패 후
|
||||
빠진 페이지마다 4 KiB 재시도를 하므로 이런 큰 빈 영역을 처리하기에 부적합했습니다.
|
||||
수정 전의 전체 2 TiB 영역 검사 완료 시간은 측정하지 않았습니다.
|
||||
|
||||
`mcptrace/memory.py`는 이제 Intel 프로세스 레이어의 `PagedInvalidAddressException`에 담긴
|
||||
부재 영역 크기를 확인하고 그 경계까지 건너뜁니다. 호출자의 VAD/VMA 끝과 바이트 예산을 넘지
|
||||
않으며 건너뛴 길이는 `unreadable_bytes`에 집계합니다. 물리 레이어 읽기 실패에는 이 추정을
|
||||
적용하지 않고 4 KiB 재시도를 유지합니다. 따라서 큰 페이지의 일부만 수집된 경우에도 읽을 수
|
||||
있는 부분을 버리지 않습니다. `pad=False`와 누락 구간 비결합도 유지합니다.
|
||||
|
||||
분석 기준: 커밋 `b178f7b9de11ab829d08694b445e5ba8e00d91f3`에 이 읽기 수정을 적용했습니다.
|
||||
실제 실행한 `mcptrace/memory.py` SHA-256:
|
||||
`12ff75dc67c8a8d6194e98f086fc2edb4987b8bbb248cadb1f4f2771231754ca`.
|
||||
|
||||
## 실제 실행 결과
|
||||
|
||||
| PID | 프로세스 | VAD | 읽은 bytes | 읽지 못한 bytes | MCP 후보 |
|
||||
| --- | --- | ---: | ---: | ---: | ---: |
|
||||
| 3244 | explorer.exe | 808 | 160,899,072 | 2,203,945,033,728 | 0 |
|
||||
| 6248 | msteams.exe | 221 | 31,633,408 | 2,220,703,334,400 | 0 |
|
||||
| 6524 | msedge.exe | 337 | 115,310,592 | 2,272,876,556,288 | 0 |
|
||||
| 2180 | Calculator.exe | 195 | 54,059,008 | 4,567,629,824 | 0 |
|
||||
|
||||
각 PID에서 다음을 확인했습니다.
|
||||
|
||||
- `regions_scanned`가 native `windows.vadinfo`의 VAD 개수와 일치.
|
||||
- `bytes_read + unreadable_bytes`가 해당 VAD들의 가상 주소 범위 길이 합계와 일치.
|
||||
- `error=null`, `limits_hit=[]`, `skipped_reason=null`이며 앵커/후보 제한 도달과 잘못된 영역은 0.
|
||||
- JSONL 4개에 올바른 PID, `target_os=windows`, `region_kind=vad` 메타데이터와 요약이 존재.
|
||||
아티팩트가 0개이므로 원문·주소·해시 대조를 통과한 Windows 메시지가 있다고 표현하지 않음.
|
||||
|
||||
`unreadable_bytes`에는 사용하지 않는 예약 주소 공간도 포함됩니다. 수 TiB의 덤프 데이터가
|
||||
손실됐다는 의미가 아니며, swap·비상주 페이지·수집 누락 등의 원인을 모두 구분한 값도 아닙니다.
|
||||
이 이미지는 2022년 시각을 담은 일반 샘플이며 MCP 송수신 정답 자료가 없습니다.
|
||||
|
||||
기본 `max-object-bytes=65536`, `max-records=10000`, `max-bytes=0`으로 실행했습니다.
|
||||
정책·partial·RPC 전체 표시는 끄고 JSONL 내보내기를 켰습니다. 프로세스별 바이트 예산 제한 없이
|
||||
선택한 VAD 목록 끝까지 검사했지만, 전체 103행의 주소 공간을 검사한 것은 아닙니다.
|
||||
CLI 경과 시간 7.01초는 심볼과 파일 캐시가 있는 공유 환경의 한 번의 관측값이며,
|
||||
다운로드·해시·심볼 준비 시간은 제외합니다. 이미지 전체 처리량 벤치마크가 아닙니다.
|
||||
|
||||
## 회귀 확인
|
||||
|
||||
실제 Volatility `Intel32e`와 구성한 페이지 테이블을 이용하는 테스트 5개를 추가했습니다.
|
||||
2 TiB 간격을 페이지마다 읽지 않는지, 인접 가상 페이지의 물리 위치가 떨어져 있어도 읽는지,
|
||||
누락 페이지 양쪽의 JSON을 결합하지 않는지, 예산 경계와 일부만 수집된 큰 페이지를 확인합니다.
|
||||
전체 테스트 119개와 Ruff 검사·포맷 검사가 통과했습니다.
|
||||
|
||||
[Linux 공개 이미지 2개](real-memory-validation.md)도 같은 수정으로 다시 실행했습니다.
|
||||
13Cubed PID 4개와 MCPRecon PID 2개의 **모든 JSONL 파일이 수정 전 결과와 바이트 단위로
|
||||
동일**했습니다. MCP 후보 41개와 원문·주소·해시·연결 표시·스캔 통계가 유지됐습니다.
|
||||
|
||||
## 재현 명령
|
||||
|
||||
저장소 설치 후 루트에서 Bash로 실행합니다. `evidence/Win11Dump.mem`은 위 원본입니다.
|
||||
첫 `windows.info`는 심볼을 내려받을 수 있으므로 네트워크를 허용한 상태에서 실행하고,
|
||||
이후에는 같은 심볼 디렉터리와 캐시를 사용합니다.
|
||||
|
||||
```bash
|
||||
export PATH="$PWD/.venv/bin:$PATH"
|
||||
mkdir -p cache symbols output/win11
|
||||
vol -q --cache-path cache -s symbols -f evidence/Win11Dump.mem -r json windows.info.Info > output/win11/info.json
|
||||
vol -q --offline --cache-path cache -s symbols -f evidence/Win11Dump.mem -r json windows.pslist.PsList > output/win11/pslist.json
|
||||
vol -q --offline --cache-path cache -s symbols -f evidence/Win11Dump.mem -r json windows.vadinfo.VadInfo --pid 3244 6524 6248 2180 > output/win11/vads.json
|
||||
vol -q --offline --cache-path cache -s symbols -p plugins -f evidence/Win11Dump.mem -o output/win11 -r json windows.mcptrace.MCPTrace --pid 3244 6524 6248 2180 --dump > output/win11/findings.json 2> output/win11/stderr.txt
|
||||
```
|
||||
|
||||
후속 검증에는 실제 MCP 호출과 정답 메시지를 기록한 Windows 덤프가 필요합니다.
|
||||
이번 결과만으로 Windows의 MCP 복원 성능, 다른 Windows 빌드 호환성, 정밀도·재현율을
|
||||
주장하지 않습니다. 원본 덤프·심볼·메모리 내용은 저장소에 커밋하지 않습니다.
|
||||
+24
-4
@@ -2,6 +2,9 @@
|
||||
|
||||
from collections.abc import Iterator
|
||||
|
||||
from volatility3.framework import exceptions
|
||||
from volatility3.framework.layers.intel import Intel
|
||||
|
||||
from mcptrace.core import ScanStats
|
||||
|
||||
|
||||
@@ -19,6 +22,7 @@ def readable_chunks(
|
||||
offset = start
|
||||
while offset < end:
|
||||
amount = min(chunk_bytes, end - offset)
|
||||
next_offset = offset + amount
|
||||
try:
|
||||
data = layer.read(offset, amount, pad=False)
|
||||
if len(data) != amount:
|
||||
@@ -30,24 +34,40 @@ def readable_chunks(
|
||||
run = bytearray()
|
||||
while page < offset + amount:
|
||||
page_amount = min(4096 - page % 4096, offset + amount - page)
|
||||
advance = page_amount
|
||||
try:
|
||||
part = layer.read(page, page_amount, pad=False)
|
||||
if len(part) != page_amount:
|
||||
raise invalid_address(layer.name, page, "Short read")
|
||||
except invalid_address:
|
||||
except invalid_address as exc:
|
||||
if run:
|
||||
yield run_start, bytes(run)
|
||||
run.clear()
|
||||
stats.unreadable_bytes += page_amount
|
||||
# Intel faults identify the size of an absent page-table
|
||||
# branch. Skip that branch, possibly beyond this read chunk,
|
||||
# but never beyond the caller's VAD/VMA range or byte budget.
|
||||
# Do not apply this to physical backing failures: part of a
|
||||
# large mapped page can still be readable in a truncated dump.
|
||||
if (
|
||||
isinstance(layer, Intel)
|
||||
and isinstance(exc, exceptions.PagedInvalidAddressException)
|
||||
and exc.layer_name == layer.name
|
||||
and page <= exc.invalid_address < page + page_amount
|
||||
and 12 <= exc.invalid_bits <= 48
|
||||
):
|
||||
block_size = 1 << exc.invalid_bits
|
||||
advance = min(end - page, block_size - page % block_size)
|
||||
stats.unreadable_bytes += advance
|
||||
else:
|
||||
if not run:
|
||||
run_start = page
|
||||
run.extend(part)
|
||||
stats.bytes_read += len(part)
|
||||
page += page_amount
|
||||
page += advance
|
||||
if run:
|
||||
yield run_start, bytes(run)
|
||||
next_offset = max(next_offset, page)
|
||||
else:
|
||||
stats.bytes_read += len(data)
|
||||
yield offset, data
|
||||
offset += amount
|
||||
offset = next_offset
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Wrap one specific public MCPRecon VMEM in a derived LiME container.
|
||||
|
||||
The source lacks VMware snapshot metadata. Its BIOS-e820 text supports a
|
||||
3 GiB low RAM / 5 GiB high RAM mapping, with a 1 GiB physical hole. This is an
|
||||
inferred mapping, not acquisition metadata. The output is NOT a LiME capture.
|
||||
Only the exact sample identified below is accepted; source bytes are preserved.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
SOURCE_SHA256 = "65b5ccea17c6ce82d978d1b258590a42a2723b13f74b88a5335a7b10401c8bbf"
|
||||
DERIVED_SHA256 = "9c375e111990c55150e6fa42e674e2ef0ddb1e2af1c6c45a4912a58c1d1d6c43"
|
||||
GIB = 1024**3
|
||||
|
||||
|
||||
def prepare(source: Path, destination: Path):
|
||||
if source.stat().st_size != 8 * GIB:
|
||||
raise ValueError("Expected the specific 8 GiB MCPRecon sample.vmem")
|
||||
source_hash, output_hash = hashlib.sha256(), hashlib.sha256()
|
||||
# Exclusive create prevents replacing either the source or an existing result.
|
||||
with source.open("rb") as src, destination.open("xb") as out:
|
||||
try:
|
||||
for start, length in [(0, 3 * GIB), (4 * GIB, 5 * GIB)]:
|
||||
header = struct.pack("<IIQQQ", 0x4C694D45, 1, start, start + length - 1, 0)
|
||||
out.write(header)
|
||||
output_hash.update(header)
|
||||
remaining = length
|
||||
while remaining:
|
||||
block = src.read(min(16 * 1024**2, remaining))
|
||||
if not block:
|
||||
raise ValueError("Truncated source")
|
||||
out.write(block)
|
||||
source_hash.update(block)
|
||||
output_hash.update(block)
|
||||
remaining -= len(block)
|
||||
if src.read(1) or source_hash.hexdigest() != SOURCE_SHA256:
|
||||
raise ValueError("Source SHA-256 does not match the documented sample")
|
||||
if output_hash.hexdigest() != DERIVED_SHA256:
|
||||
raise ValueError("Derived SHA-256 mismatch")
|
||||
out.flush()
|
||||
os.fsync(out.fileno())
|
||||
except BaseException:
|
||||
# Only remove the file exclusively created by this invocation.
|
||||
out.close()
|
||||
destination.unlink()
|
||||
raise
|
||||
return {
|
||||
"source_sha256": source_hash.hexdigest(),
|
||||
"derived_sha256": output_hash.hexdigest(),
|
||||
"derived_bytes": destination.stat().st_size,
|
||||
"mapping_is_inferred": True,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("source", type=Path)
|
||||
parser.add_argument("destination", type=Path)
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(prepare(args.source, args.destination), indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Re-read Linux JSONL artifacts through native Volatility process layers.
|
||||
|
||||
This checks exported bytes, hashes, and VMA bounds. It does not establish that
|
||||
the JSON was transmitted or measure message recovery against ground truth.
|
||||
No MCPTrace scanner or adapter is used to perform the verification.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import volatility3.plugins
|
||||
import volatility3.symbols
|
||||
from volatility3 import framework
|
||||
from volatility3.framework import automagic, constants, contexts, plugins
|
||||
from volatility3.framework.automagic import stacker
|
||||
from volatility3.plugins.linux import pslist
|
||||
|
||||
|
||||
def verify_file(path, context, tasks):
|
||||
with path.open(encoding="utf-8") as stream:
|
||||
rows = [json.loads(line) for line in stream if line.strip()]
|
||||
if len(rows) < 2 or rows[0].get("type") != "metadata":
|
||||
raise ValueError(f"{path.name}: missing export metadata")
|
||||
header, *records, summary = rows
|
||||
pid = header["pid"]
|
||||
if header.get("target_os") != "linux" or header.get("region_kind") != "vma":
|
||||
raise ValueError(f"{path.name}: not a Linux VMA export")
|
||||
if header.get("address_space") != "process-virtual":
|
||||
raise ValueError(f"{path.name}: not a process virtual address export")
|
||||
if summary.get("type") != "scan-summary" or summary.get("displayed_records") != len(records):
|
||||
raise ValueError(f"{path.name}: summary/count mismatch")
|
||||
if pid not in tasks:
|
||||
raise ValueError(f"{path.name}: PID {pid} is not in native linux.pslist")
|
||||
task = tasks[pid]
|
||||
total_bytes = 0
|
||||
if records:
|
||||
if not task.mm:
|
||||
raise ValueError(f"{path.name}: records attributed to a kernel thread")
|
||||
layer_name = task.add_process_layer()
|
||||
if not layer_name:
|
||||
raise ValueError(f"{path.name}: process layer unavailable")
|
||||
layer = context.layers[layer_name]
|
||||
regions = {int(v.vm_start): int(v.vm_end) for v in task.mm.get_vma_iter()}
|
||||
for index, record in enumerate(records, 2):
|
||||
label = f"{path.name}:{index}"
|
||||
if (
|
||||
record.get("type") != "artifact"
|
||||
or record.get("pid") != pid
|
||||
or record.get("target_os") != "linux"
|
||||
or record.get("region_kind") != "vma"
|
||||
):
|
||||
raise ValueError(f"{label}: artifact metadata mismatch")
|
||||
raw = base64.b64decode(record["raw_base64"], validate=True)
|
||||
if not raw or len(raw) != record["byte_length"]:
|
||||
raise ValueError(f"{label}: byte length mismatch")
|
||||
if hashlib.sha256(raw).hexdigest() != record["raw_sha256"]:
|
||||
raise ValueError(f"{label}: SHA-256 mismatch")
|
||||
start, address = record["region_start"], record["virtual_address"]
|
||||
if (
|
||||
record.get("vma_start") != start
|
||||
or start not in regions
|
||||
or not start <= address < address + len(raw) <= regions[start]
|
||||
):
|
||||
raise ValueError(f"{label}: VMA bounds mismatch")
|
||||
if layer.read(address, len(raw), pad=False) != raw:
|
||||
raise ValueError(f"{label}: process memory bytes differ")
|
||||
total_bytes += len(raw)
|
||||
return {"pid": pid, "artifacts_verified": len(records), "bytes_verified": total_bytes}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--image", type=Path, required=True)
|
||||
parser.add_argument("--symbols", type=Path, required=True)
|
||||
parser.add_argument("--cache", type=Path, required=True)
|
||||
parser.add_argument("--require-artifacts", action="store_true")
|
||||
parser.add_argument("exports", type=Path, nargs="+")
|
||||
args = parser.parse_args()
|
||||
args.cache.mkdir(parents=True, exist_ok=True)
|
||||
constants.CACHE_PATH = str(args.cache.resolve())
|
||||
constants.OFFLINE = True
|
||||
volatility3.symbols.__path__ = [str(args.symbols.resolve()), *constants.SYMBOL_BASEPATHS]
|
||||
context = contexts.Context()
|
||||
framework.import_files(volatility3.plugins, True)
|
||||
# Import automagic modules before selecting stackers: the Linux stackers
|
||||
# live there, not in framework.layers.
|
||||
magics = automagic.choose_automagic(automagic.available(context), pslist.PsList)
|
||||
context.config["automagic.LayerStacker.single_location"] = args.image.resolve().as_uri()
|
||||
context.config["automagic.LayerStacker.stackers"] = stacker.choose_os_stackers(pslist.PsList)
|
||||
plugin = plugins.construct_plugin(context, magics, pslist.PsList, "plugins", None, None)
|
||||
tasks = {
|
||||
int(task.tgid): task
|
||||
for task in pslist.PsList.list_tasks(
|
||||
context, plugin.config["kernel"], include_threads=False
|
||||
)
|
||||
}
|
||||
results = [verify_file(path, context, tasks) for path in args.exports]
|
||||
count = sum(result["artifacts_verified"] for result in results)
|
||||
if args.require_artifacts and not count:
|
||||
raise ValueError("No artifacts to verify")
|
||||
print(json.dumps({"artifacts_verified": count, "exports": results}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Exercise sparse scanning with real x86-64 page tables and Volatility layers."""
|
||||
|
||||
import struct
|
||||
|
||||
from volatility3.framework import contexts, exceptions
|
||||
from volatility3.framework.layers.intel import Intel32e
|
||||
from volatility3.framework.layers.physical import BufferDataLayer
|
||||
|
||||
from mcptrace.core import ScanStats, scan_chunks
|
||||
from mcptrace.memory import readable_chunks
|
||||
|
||||
PAGE = 4096
|
||||
HIGH = 1 << 41
|
||||
|
||||
|
||||
def intel_layer(virtual_pages=None):
|
||||
# Virtual pages 0 and 1 are neighbors but physical pages 5 and 7 are not.
|
||||
# Page 2 is absent; page 3 maps to physical page 6. Repeat at 2 TiB to
|
||||
# exercise absent PML4/PDPT/PD branches without allocating a large buffer.
|
||||
data = bytearray(8 * PAGE)
|
||||
for address, value in [
|
||||
(0x1000, 0x2003),
|
||||
(0x1020, 0x2003),
|
||||
(0x2000, 0x3003),
|
||||
(0x3000, 0x4003),
|
||||
(0x4000, 0x5003),
|
||||
(0x4008, 0x7003),
|
||||
(0x4018, 0x6003),
|
||||
]:
|
||||
struct.pack_into("<Q", data, address, value)
|
||||
for virtual_page, physical_page in [(0, 5), (1, 7), (3, 6)]:
|
||||
page = (virtual_pages or {}).get(virtual_page, bytes([65 + virtual_page]) * PAGE)
|
||||
assert len(page) == PAGE
|
||||
data[physical_page * PAGE : (physical_page + 1) * PAGE] = page
|
||||
context = contexts.Context()
|
||||
context.layers.add_layer(BufferDataLayer(context, "physical", "physical", bytes(data)))
|
||||
context.config["virtual.memory_layer"] = "physical"
|
||||
context.config["virtual.page_map_offset"] = 0x1000
|
||||
layer = Intel32e(context, "virtual", "virtual")
|
||||
context.layers.add_layer(layer)
|
||||
return layer
|
||||
|
||||
|
||||
def test_two_tib_gap_does_not_probe_every_page(monkeypatch):
|
||||
layer = intel_layer()
|
||||
original_read = layer.read
|
||||
calls = []
|
||||
|
||||
def read(offset, length, pad=False):
|
||||
calls.append((offset, length))
|
||||
assert len(calls) <= 4096, "Missing branches must not trigger per-page reads"
|
||||
assert not pad
|
||||
return original_read(offset, length, pad=pad)
|
||||
|
||||
monkeypatch.setattr(layer, "read", read)
|
||||
stats = ScanStats()
|
||||
size = HIGH + 4 * PAGE
|
||||
chunks = list(readable_chunks(layer, 0, size, stats, exceptions.InvalidAddressException))
|
||||
assert [(address, len(data)) for address, data in chunks] == [
|
||||
(0, 2 * PAGE),
|
||||
(3 * PAGE, PAGE),
|
||||
(HIGH, 2 * PAGE),
|
||||
(HIGH + 3 * PAGE, PAGE),
|
||||
]
|
||||
assert chunks[0][1] == b"A" * PAGE + b"B" * PAGE
|
||||
assert stats.bytes_read == 6 * PAGE
|
||||
assert stats.unreadable_bytes == size - 6 * PAGE
|
||||
|
||||
|
||||
def test_sparse_reads_preserve_clipped_byte_budget():
|
||||
layer = intel_layer()
|
||||
stats = ScanStats()
|
||||
chunks = list(readable_chunks(layer, PAGE - 3, 12, stats, exceptions.InvalidAddressException))
|
||||
assert chunks == [(PAGE - 3, b"AAA" + b"B" * 9)]
|
||||
assert stats.bytes_read == 12 and stats.unreadable_bytes == 0
|
||||
|
||||
|
||||
def test_missing_branch_skip_is_clipped_to_requested_range():
|
||||
layer = intel_layer()
|
||||
stats = ScanStats()
|
||||
chunks = list(
|
||||
readable_chunks(layer, 2**30 + 7, 9999, stats, exceptions.InvalidAddressException)
|
||||
)
|
||||
assert chunks == []
|
||||
assert stats.bytes_read == 0 and stats.unreadable_bytes == 9999
|
||||
|
||||
|
||||
def test_sparse_reads_do_not_join_json_across_absent_page():
|
||||
intact = b'{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
||||
broken = b'{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
|
||||
p0, p1, p3 = bytearray(PAGE), bytearray(PAGE), bytearray(PAGE)
|
||||
p0[-12:] = intact[:12]
|
||||
p1[: len(intact) - 12] = intact[12:]
|
||||
p1[-20:] = broken[:20]
|
||||
p3[: len(broken) - 20] = broken[20:]
|
||||
layer = intel_layer({0: p0, 1: p1, 3: p3})
|
||||
stats = ScanStats()
|
||||
records = list(
|
||||
scan_chunks(readable_chunks(layer, 0, 4 * PAGE, stats, exceptions.InvalidAddressException))
|
||||
)
|
||||
assert [(r.address, r.request_id) for r in records] == [(PAGE - 12, 1)]
|
||||
assert stats.bytes_read == 3 * PAGE and stats.unreadable_bytes == PAGE
|
||||
|
||||
|
||||
def test_large_page_with_partial_backing_keeps_readable_bytes():
|
||||
# A present 2 MiB PDE maps physical address 0, but only 8 physical pages
|
||||
# were captured. Ignoring errors from mapping the entire range would drop
|
||||
# its readable prefix; ordinary physical errors must retain page fallback.
|
||||
data = bytearray(8 * PAGE)
|
||||
for address, value in [(0x1000, 0x2003), (0x2000, 0x3003), (0x3000, 0x83)]:
|
||||
struct.pack_into("<Q", data, address, value)
|
||||
context = contexts.Context()
|
||||
context.layers.add_layer(BufferDataLayer(context, "physical", "physical", bytes(data)))
|
||||
context.config["virtual.memory_layer"] = "physical"
|
||||
context.config["virtual.page_map_offset"] = 0x1000
|
||||
layer = Intel32e(context, "virtual", "virtual")
|
||||
context.layers.add_layer(layer)
|
||||
stats = ScanStats()
|
||||
chunks = list(readable_chunks(layer, 0, 2**21, stats, exceptions.InvalidAddressException))
|
||||
assert chunks == [(0, bytes(data))]
|
||||
assert stats.bytes_read == len(data)
|
||||
assert stats.unreadable_bytes == 2**21 - len(data)
|
||||
Reference in New Issue
Block a user