diff --git a/README.md b/README.md index b2c6433..2a84758 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,15 @@ Windows·Linux 프로세스 메모리에서 MCP/JSON-RPC 메시지 후보를 추 수집된 메모리 이미지를 분석하고, 메시지 원문·주소·도구 인자·정책 검사 결과를 남깁니다. **상태: 초기 구현.** 프레임워크 패키지 버전은 `volatility3==2.28.2`로 고정했습니다. -분석 대상은 Windows x64와 Linux x86-64입니다. Windows의 첫 실증 대상은 Windows 11 x64입니다. -합성 테스트와 함께 공개 Linux RAM 이미지 2개를 시험했습니다. MCP 활동 샘플의 Python 프로세스 +분석 대상은 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 실제 이미지, 다른 커널 빌드의 호환성은 아직 검증하지 않았습니다. +전체 메시지 복원율·정밀도, Windows의 실제 MCP 메시지 추출, 다른 OS 빌드의 호환성은 아직 검증하지 않았습니다. | 분석할 이미지 | 명령 | 프로세스 메모리 영역 | | --- | --- | --- | @@ -25,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 충돌은 모호함으로 표시. diff --git a/docs/real-memory-validation.md b/docs/real-memory-validation.md index 2c0a73d..8e65c77 100644 --- a/docs/real-memory-validation.md +++ b/docs/real-memory-validation.md @@ -2,7 +2,10 @@ 검증일: 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, @@ -132,6 +135,6 @@ python scripts/verify_linux_export.py --image evidence/sample-derived.lime --sym 실제 송수신 메시지 정답 집합과 수집 지연·작업량 조건을 대조하지 않았으므로 정밀도, 재현율, 오연결률, 인자 부분 복원률을 주장할 수 없습니다. JSON은 로그·캐시·예제에서 왔을 수도 있습니다. -Windows 실제 RAM, 기존 mmap 기반 Linux 커널, 다른 배포판·빌드, UTF-16 실제 사례, +Windows의 실제 MCP 메시지 추출, 기존 mmap 기반 Linux 커널, 다른 배포판·빌드, UTF-16 실제 사례, 부분 조각 추출과 사용자 정책의 실제 사건 판정도 이번 실행 범위 밖입니다. 이 결과는 선택한 프로세스에서의 추출 경로 확인이며, 운영 환경 전반의 검증 완료 선언이 아닙니다. diff --git a/docs/validation.md b/docs/validation.md index 7706efd..9f701db 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -3,7 +3,8 @@ 이 문서는 자동 테스트의 범위와 실제 이미지 검증 절차를 설명합니다. 공개 Linux 이미지 2개에 대한 실행 결과는 [실제 RAM 검증 기록](real-memory-validation.md)에 별도로 기록했습니다. MCP 활동 샘플은 누락된 VMware 메타데이터를 대신해 추정한 물리 메모리 -배치를 적용했습니다. Windows 실제 이미지 검증과 전체 메시지 기준 성능 평가는 아직 수행하지 않았습니다. +배치를 적용했습니다. [Windows 11 실제 이미지 검증](windows-memory-validation.md)도 수행했으나, +그 이미지에서는 MCP 후보가 나오지 않았습니다. 전체 메시지 기준 성능 평가는 아직 수행하지 않았습니다. ## 자동 검증 @@ -18,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 읽기 오류 이후 기존 증거 보존 | diff --git a/docs/windows-memory-validation.md b/docs/windows-memory-validation.md new file mode 100644 index 0000000..8c45736 --- /dev/null +++ b/docs/windows-memory-validation.md @@ -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 빌드 호환성, 정밀도·재현율을 +주장하지 않습니다. 원본 덤프·심볼·메모리 내용은 저장소에 커밋하지 않습니다. diff --git a/mcptrace/memory.py b/mcptrace/memory.py index 8400023..d84fc22 100644 --- a/mcptrace/memory.py +++ b/mcptrace/memory.py @@ -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 diff --git a/tests/test_intel_memory.py b/tests/test_intel_memory.py new file mode 100644 index 0000000..5ac72c3 --- /dev/null +++ b/tests/test_intel_memory.py @@ -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("