Windows LNK 파일 분석을 통한 아티팩트 추출
사용 시기
- Windows 바로가기 파일에서 사용자 파일 접근 기록을 재구성할 때
- 접근된 파일, 네트워크 공유, 이동식 미디어를 추적할 때
- 조사 중 사용자가 특정 문서를 열었음을 증명해야 할 때
- 파일 접근을 다른 타임라인 아티팩트와 상호 연관시키려 할 때
- 원격 시스템 또는 USB 장치에서 접근된 경로를 식별할 때
전제 조건
- 포렌식 이미지에서 LNK 파일 접근 가능 (최근 항목, 바탕 화면, 빠른 실행)
- LECmd (Eric Zimmerman), python-lnk, 또는 LnkParser 분석 도구
- LNK 파일 구조 (Shell Link Binary 형식) 이해
- Windows 시스템에서 LNK 파일 위치에 대한 지식
- 분석 도구가 설치된 포렌식 워크스테이션
워크플로우
1단계: 포렌식 이미지에서 LNK 파일 수집
# 포렌식 이미지 마운트
mount -o ro,loop,offset=$((2048*512)) /cases/case-2024-001/images/evidence.dd /mnt/evidence
mkdir -p /cases/case-2024-001/lnk/{recent,desktop,startup,custom}
# 최근 항목 LNK 파일 복사 (기본 소스)
cp /mnt/evidence/Users/*/AppData/Roaming/Microsoft/Windows/Recent/*.lnk \
/cases/case-2024-001/lnk/recent/ 2>/dev/null
# 자동 대상(Jump Lists) 복사
cp /mnt/evidence/Users/*/AppData/Roaming/Microsoft/Windows/Recent/AutomaticDestinations/*.automaticDestinations-ms \
/cases/case-2024-001/lnk/recent/ 2>/dev/null
# 사용자 지정 대상(고정된 Jump List 항목) 복사
cp /mnt/evidence/Users/*/AppData/Roaming/Microsoft/Windows/Recent/CustomDestinations/*.customDestinations-ms \
/cases/case-2024-001/lnk/custom/ 2>/dev/null
# 바탕 화면 바로가기 복사
cp /mnt/evidence/Users/*/Desktop/*.lnk /cases/case-2024-001/lnk/desktop/ 2>/dev/null
# 시작 폴더 바로가기 복사 (지속성)
cp /mnt/evidence/Users/*/AppData/Roaming/Microsoft/Windows/Start\ Menu/Programs/Startup/*.lnk \
/cases/case-2024-001/lnk/startup/ 2>/dev/null
cp "/mnt/evidence/ProgramData/Microsoft/Windows/Start Menu/Programs/Startup"/*.lnk \
/cases/case-2024-001/lnk/startup/ 2>/dev/null
# 시스템 내 모든 LNK 파일 찾기
find /mnt/evidence/ -name "*.lnk" -type f 2>/dev/null > /cases/case-2024-001/lnk/all_lnk_locations.txt
# 개수 및 해시 계산
ls /cases/case-2024-001/lnk/recent/ | wc -l
sha256sum /cases/case-2024-001/lnk/recent/*.lnk > /cases/case-2024-001/lnk/lnk_hashes.txt 2>/dev/null
2단계: LECmd로 LNK 파일 파싱
# Eric Zimmerman의 LECmd 사용 (Windows 또는 Mono)
# 디렉토리의 모든 LNK 파일 처리
LECmd.exe -d "C:\cases\lnk\recent\" --csv "C:\cases\analysis\" --csvf lnk_analysis.csv
# 단일 LNK 파일 처리 (자세한 출력)
LECmd.exe -f "C:\cases\lnk\recent\document.pdf.lnk"
# Jump List 파일 처리
JLECmd.exe -d "C:\cases\lnk\recent\" --csv "C:\cases\analysis\" --csvf jumplist_analysis.csv
# 출력 내용:
# - 소스 파일 경로
# - 대상 경로 (접근한 파일)
# - 대상 생성, 수정, 접근 타임스탬프
# - LNK 생성 및 수정 타임스탬프
# - 작업 디렉터리
# - 명령줄 인자
# - 볼륨 일련번호 및 레이블
# - 드라이브 유형 (고정, 이동식, 네트워크)
# - 머신 ID (NetBIOS 이름)
# - MAC 주소 (트래커 데이터베이스에서)
# - 대상 파일 크기
3단계: Python으로 LNK 파일 파싱
pip install LnkParse3
python3 << 'PYEOF'
import LnkParse3
import os, json, csv
from datetime import datetime
lnk_dir = '/cases/case-2024-001/lnk/recent/'
results = []
for filename in sorted(os.listdir(lnk_dir)):
if not filename.lower().endswith('.lnk'):
continue
filepath = os.path.join(lnk_dir, filename)
try:
with open(filepath, 'rb') as f:
lnk = LnkParse3.lnk_file(f)
info = lnk.get_json()
parsed = {
'lnk_file': filename,
'target_path': '',
'working_dir': '',
'arguments': '',
'target_created': '',
'target_modified': '',
'target_accessed': '',
'file_size': '',
'drive_type': '',
'volume_serial': '',
'volume_label': '',
'machine_id': '',
'mac_address': '',
}
# 헤더 타임스탬프 추출
header = info.get('header', {})
parsed['target_created'] = str(header.get('creation_time', ''))
parsed['target_modified'] = str(header.get('modified_time', ''))
parsed['target_accessed'] = str(header.get('accessed_time', ''))
parsed['file_size'] = str(header.get('file_size', ''))
# 링크 정보 추출
link_info = info.get('link_info', {})
if link_info:
local_path = link_info.get('local_base_path', '')
network_path = link_info.get('common_network_relative_link', {}).get('net_name', '')
parsed['target_path'] = local_path or network_path
vol_info = link_info.get('volume_id', {})
if vol_info:
parsed['drive_type'] = str(vol_info.get('drive_type', ''))
parsed['volume_serial'] = str(vol_info.get('drive_serial_number', ''))
parsed['volume_label'] = str(vol_info.get('volume_label', ''))
# 문자열 데이터 추출
string_data = info.get('string_data', {})
parsed['working_dir'] = str(string_data.get('working_dir', ''))
parsed['arguments'] = str(string_data.get('command_line_arguments', ''))
# 트래커 데이터 추출 (머신 ID 및 MAC)
extra = info.get('extra', {})
tracker = extra.get('DISTRIBUTED_LINK_TRACKER_BLOCK', {})
if tracker:
parsed['machine_id'] = str(tracker.get('machine_id', ''))
parsed['mac_address'] = str(tracker.get('mac_address', ''))
results.append(parsed)
# 요약 출력
print(f"\n{filename}")
print(f" 대상: {parsed['target_path']}")
print(f" 수정됨: {parsed['target_modified']}")
print(f" 드라이브: {parsed['drive_type']} (일련번호: {parsed['volume_serial']})")
if parsed['machine_id']:
print(f" 머신: {parsed['machine_id']}")
except Exception as e:
print(f" {filename} 파싱 오류: {e}")
# 결과를 CSV로 저장
with open('/cases/case-2024-001/analysis/lnk_analysis.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=results[0].keys() if results else [])
writer.writeheader()
writer.writerows(results)
print(f"\n\n총 파싱된 LNK 파일: {len(results)}")
PYEOF
4단계: 조사 가치 분석
# 이동식 미디어에서 접근된 파일 식별
python3 << 'PYEOF'
import csv
with open('/cases/case-2024-001/analysis/lnk_analysis.csv') as f:
reader = csv.DictReader(f)
print("=== 이동식 미디어에서 접근된 파일 ===\n")
removable = []
network = []
for row in reader:
if 'DRIVE_REMOVABLE' in row.get('drive_type', '').upper() or \
'removable' in row.get('drive_type', '').lower():
removable.append(row)
print(f" {row['target_modified']} | {row['target_path']} | 볼륨: {row['volume_serial']}")
if 'network' in row.get('drive_type', '').lower() or \
row.get('target_path', '').startswith('\\\\'):
network.append(row)
print(f"\n=== 네트워크 공유에서 접근된 파일 ===\n")
for row in network:
print(f" {row['target_modified']} | {row['target_path']}")
print(f"\n이동식 미디어 파일: {len(removable)}")
print(f"네트워크 공유 파일: {len(network)}")
# 고유 머신 확인 (트래커 데이터)
machines = set()
for row in [*removable, *network]:
if row.get('machine_id'):
machines.add(row['machine_id'])
if machines:
print(f"\n발견된 머신 ID: {machines}")
PYEOF
# 시작 폴더 LNK 파일 확인 (지속성)
echo "=== 시작 폴더 바로가기 (지속성) ===" > /cases/case-2024-001/analysis/startup_persistence.txt
for lnk in /cases/case-2024-001/lnk/startup/*.lnk; do
python3 -c "
import LnkParse3
with open('$lnk', 'rb') as f:
lnk = LnkParse3.lnk_file(f)
info = lnk.get_json()
target = info.get('link_info', {}).get('local_base_path', '알 수 없음')
args = info.get('string_data', {}).get('command_line_arguments', '')
print(f' \$(basename $lnk): {target} {args}')
" >> /cases/case-2024-001/analysis/startup_persistence.txt 2>/dev/null
done
주요 개념
| 개념 | 설명 |
|---|---|
| Shell Link (.lnk) | 대상 경로, 타임스탬프, 메타데이터를 포함하는 Windows 바로가기 파일 형식 |
| 대상 타임스탬프 | 바로가기가 가리키는 파일의 생성, 수정, 접근 시간 |
| 볼륨 일련번호 | 대상 파일이 있는 드라이브 볼륨의 고유 식별자 |
| 머신 ID | 분산 링크 추적 서비스가 포함한 NetBIOS 이름 |
| MAC 주소 | LNK 파일을 생성한 머신의 네트워크 어댑터 MAC |
| Jump Lists | 애플리케이션별 최근 및 고정 파일 목록 (내장 LNK 데이터 포함) |
| 자동 대상 | 최근 연 파일에 대한 시스템 관리 Jump List 항목 |
| 사용자 지정 대상 | 사용자가 고정한 Jump List 항목 (수동 제거 시까지 유지) |
도구 및 시스템
| 도구 | 목적 |
|---|---|
| LECmd | Eric Zimmerman의 LNK 파일 파서 (CSV/JSON 출력) |
| JLECmd | Eric Zimmerman의 Jump List 파서 |
| LnkParse3 | 프로그래밍 방식 LNK 파일 분석을 위한 Python 라이브러리 |
| lnk_parser | 대체 Python LNK 파싱 도구 |
| Autopsy | LNK 파일 분석 모듈을 포함한 포렌식 플랫폼 |
| KAPE | 자동화된 LNK 및 Jump List 아티팩트 수집 |
| Plaso | 슈퍼 타임라인 생성을 위한 LNK 파일 파서 포함 타임라인 도구 |
| LNK Explorer | 대화형 LNK 파일 검사를 위한 GUI 도구 |
일반 시나리오
시나리오 1: USB 드라이브를 통한 데이터 유출 최근 폴더 LNK 파일을 분석하여 이동식 드라이브의 대상을 찾고, 볼륨 일련번호를 USBSTOR 레지스트리 항목과 상호 연관시키고, USB 장치에서 접근된 파일 목록을 작성하고, 이동식 드라이브에서 열린 문서를 확인하고, 파일 복사 타임스탬프와 상호 연관시킵니다.
시나리오 2: 시작 폴더 바로가기를 통한 악성코드 지속성 시작 폴더 LNK 파일에서 악성 대상을 검사하고, 대상 경로와 인자에서 인코딩된 명령 또는 의심스러운 실행 파일을 확인하고, 대상 파일이 존재하는지 검증하고 검사하며, 생성 타임스탬프를 초기 침해 시간과 상호 연관시킵니다.
시나리오 3: 네트워크 공유 접근 조사 네트워크 경로(UNC 대상)가 있는 LNK 파일을 필터링하고, 접근된 네트워크 공유와 시간을 식별하고, 머신 ID를 알려진 기업 시스템과 상호 연관시키고, 정상 업무 외에 민감한 파일 서버에 접근했는지 확인하며, 규정 준수 조사를 위한 접근 타임라인을 구축합니다.
시나리오 4: 법적 절차를 위한 문서 접근 타임라인 모든 최근 폴더 LNK 파일을 추출하고, 사용자가 접근한 문서의 시간순 목록을 작성하고, 사건과 관련된 특정 파일을 식별하고, 파일이 열린 시간을 보여주는 대상 타임스탬프를 제시하며, 이메일 및 통신 타임라인과 상호 연관시킵니다.
출력 형식
LNK 파일 분석 요약:
사용자 프로필: suspect_user
총 LNK 파일: 234개 (최근 항목: 198, 바탕 화면: 23, 시작: 5, 기타: 8)
파일 접근 통계:
로컬 드라이브 (C:): 156개 파일
이동식 미디어: 23개 파일 (3개의 고유 볼륨 일련번호)
네트워크 공유: 15개 파일 (\\server01, \\fileserver)
기타 드라이브: 4개 파일
발견된 머신 ID: DESKTOP-ABC123, LAPTOP-XYZ789
MAC 주소: AA:BB:CC:DD:EE:FF, 11:22:33:44:55:66
이동식 미디어 접근:
볼륨 일련번호 1234-ABCD:
2024-01-15 14:32 - E:\Confidential\financial_report.xlsx
2024-01-15 14:45 - E:\Confidential\customer_database.csv
2024-01-15 15:00 - E:\Projects\source_code.zip
시작 폴더 지속성:
updater.lnk -> C:\ProgramData\svc\updater.exe (의심스러움)
OneDrive.lnk -> C:\Users\...\OneDrive.exe (정상)
타임라인: /cases/case-2024-001/analysis/lnk_analysis.csv