디지털포렌식과 친해지기
  • 디지털포렌식전문가 2급 실기와 친해지기(실기)
    • 1. 디지털 포렌식 실기 준비하기
      • 1. BIT, BYTE, 파일 그리고 Hash
      • 2. 섹터와 사본 이미지 생성
        • 2-1. FTK Imager 활용 물리이미징(Registry 쓰기방지)
        • 2-2. EnCase 활용 물리이미징(EnCase 쓰기방지)
        • (연습용) 가상디스크 만들기
      • 3. 파티션과 파일시스템 복구
        • 3-1) 파티션과 파티션 테이블
          • 3-1.1) GPT 헤더, 파티션 Entry 복구
        • 3-2) 파일시스템과 파일시스템 복구
        • 3-3) 파일시스템 복구 실전 연습
      • 4-1. 무료도구 활용 분석연습
        • 0) 이미지 획득 및 파일시스템 복구
        • 1) 저장매체와 파일시스템 분석
        • 2) 파일과 친해지기
        • 3-1) 파일 관련 분석 1
        • 3-2) 파일 관련 분석 2
        • 4-1) 윈도우 아티팩트1
        • 4-2) 윈도우 아티팩트2
        • 5) 주요 응용 프로그램 아티팩트
          • 5-1) Sqlite 열어보기
        • 6) 키워드 검색 / Base64 Decode
        • 7) bitlocker
        • 8) 가상머신(참고)
        • 무료도구 활용 분석연습 정리
      • 4-2. EnCase 활용 분석연습
        • 0) 이미지 획득 및 파일시스템 복구
        • 1) 저장매체와 파일시스템 분석
        • 2) 파일과 친해지기
        • 3-1) 파일 관련 분석 1
        • 3-2) 파일 관련 분석 2
        • 4-1) 윈도우 아티팩트1
        • 4-2) 윈도우 아티팩트2
        • 5) 주요 응용 프로그램 아티팩트
          • 5-1) Sqlite 열어보기
        • 6) 키워드 검색 / Base64 Decode
        • 7) bitlocker
        • 8) 가상머신(참고)
        • EnCase를 활용한 분석연습 정리
      • 5. 주관식 - 기본 절차 및 증거법 관련
        • (정리 중) 디지털 증거관련 주요 판례
      • 6. 답안 제출 및 보고서 작성
      • 7. 요령 및 주의사항
    • 2. 실력 다지기
      • 1. 문제 저장매체 만들고 풀어보기
        • 기초 연습문제1-분석해보기(무료도구)
        • 기초 연습문제1-분석해보기(EnCase)
      • 2. 파일시스템 복구 연습
      • 3. NTFS 로그 분석 연습
    • 3. 실전 연습 문제
      • 2018 실전 연습 문제
        • 2018 실전 연습 - 분석해보기(무료도구)
        • 2018 실전 연습 - 분석해보기(EnCase)
      • 2019 실전 연습 문제
        • 2019 실전 연습 - 분석해보기(무료도구)
        • 2019 실전 연습 - 분석해보기(EnCase)
      • 2020 실전 연습 문제
        • 2020 실전 연습 - 분석해보기(무료도구)
        • 2020 실전 연습 - 분석해보기(EnCase)
      • 2023 실전 연습 문제(종합유형)
        • 2023 실전 연습 - 분석해보기(무료도구)
        • 2023 실전 연습 - 분석해보기(EnCase)
      • 2024 실전 연습 문제
        • 2024 실전 연습 - 분석해보기(무료도구)
        • 2024 실전 연습 - 분석해보기(EnCase)
    • 4. 기출 유형
  • 디지털포렌식과 친해지기
    • 1. BIT의 저장
      • 0) 준비사항!
        • 0-1) 주요 분석 도구 간단 소개 및 설정
        • 0-2) Python 을 이용한 개발 환경 구성
        • 0-3) Python 소스로 실행파일 만들기
      • 1) BIT, BYTES와 파일 그리고 Hash
        • 1-1) Hex Viewer 만들기
        • 1-2) BIT의 저장(참고)
      • 2) 저장매체와 섹터 그리고 물리이미징
        • 2-1) 가상 디스크 설정
        • 2-2) 물리이미징(raw) 실습
        • 2-3) 물리이미징 수집 도구 만들기(기초)
      • 3) 파티션
        • 3-1) MBR 파티션 테이블 구조
        • 3-2) GPT(GUID Partition Table) 구조
        • 3-3) 파티션 분석 도구 만들기(기초)
      • 4) 파일시스템 기초 분석
        • 4-1) 파일시스템 직접 만들어 보기
        • 4-2) FAT32 분석
          • 4-2.1) FAT32 분석(BR, Directory Entry - 데이터의 접근)
          • 4-2.2) FAT32 분석 2(FAT, LFN, 삭제)
          • 4-2.3) FAT32 분석 3 (특징과 분석 도구)
        • 4-3) NTFS 기초 분석
          • 4-3.1) NTFS 기초 분석(NTFS BR과 DATA 영역)
          • 4-3.2) $MFT와 MFT Entry
          • 4-3.3) MFT Entry의 주요 속성1($SI,$FILE,$DATA)
          • 4-3.4) MFT Entry의 주요 속성2(인덱스1, resident/Nonresident)
          • 4-3.5) MFT Entry 찾기(인덱스2, $ATTRIBUTE_LIST)
          • 4-3.6) NTFS 에서 파일의 접근 정리
          • 4-3.7) NTFS 주요 메타데이터 파일
          • MFT Entry 분석용
      • 5) 파티션과 파일시스템
      • 6) 사본 이미지 생성(논리/물리이미징)
      • 7) 파일과 친해지기
Powered by GitBook
On this page
  1. 디지털포렌식과 친해지기
  2. 1. BIT의 저장
  3. 3) 파티션

3-3) 파티션 분석 도구 만들기(기초)

Previous3-2) GPT(GUID Partition Table) 구조Next4) 파일시스템 기초 분석

Last updated 5 months ago

Python 을 이용한 소스 코드 작성 및 실행 방법

우리가 알고 있는 것

  1. 0번 섹터에서 파티션 테이블에서, MBR 파티션 테이블, GPT 파티션 테이블을 확인 가능

  2. 파티션테이블은 MBR에서 확인 가능하며, GPT 파티션테이블의 경우 GPT 헤더 이 후 파티션테이블을 저장하는 공간이 따로 있다.

파티션 분석 주요 요구사항

  1. 물리 드라이브를 리스트를 표시하고 선택한 물리디스크의 파티션을 분석한다.

  2. MBR을 분석하여 GPT 파티션테이블을 포함하고 있는지 여부를 확인하고 분석 한다.

  3. 파티션테이블을 분석하고, 파티션별 시작 섹터, 마지막섹터, 총 섹터 수, 파티션별 용량을 GUI로 표시하자.

1. 파티션 분석 도구 사용 방법 및 최종 소스

우선 업로드한 실행파일을 받아서 사용할 경우 제가 공유한 프로그램이 정확한지 확인 후 사용

  • 먼저 제가 ChatGPT와 같이 작성한 파이썬 소스를 이용하여 물리이미징 획득 도구를 제작하였고, 공유하니 참고로 활용하도록 합시다.

최종 Python 소스
import ctypes
import subprocess
import logging
import tkinter as tk
from tkinter import messagebox, ttk

# Windows 상수 정의
GENERIC_READ = 0x80000000
FILE_SHARE_READ = 0x00000001
FILE_SHARE_WRITE = 0x00000002
OPEN_EXISTING = 3
IOCTL_DISK_GET_DRIVE_GEOMETRY = 0x00070000
IOCTL_DISK_GET_LENGTH_INFO = 0x0007405C

# ctypes.wintypes 불러오기
from ctypes import wintypes

# DISK_GEOMETRY 구조체
class DISK_GEOMETRY(ctypes.Structure):
    _fields_ = [
        ("Cylinders", wintypes.LARGE_INTEGER),
        ("MediaType", wintypes.DWORD),
        ("TracksPerCylinder", wintypes.DWORD),
        ("SectorsPerTrack", wintypes.DWORD),
        ("BytesPerSector", wintypes.DWORD)
    ]

# GET_LENGTH_INFORMATION 구조체
class GET_LENGTH_INFORMATION(ctypes.Structure):
    _fields_ = [("Length", wintypes.LARGE_INTEGER)]

# MBR 파티션 항목 구조체
class PARTITION_ENTRY(ctypes.Structure):
    _fields_ = [
        ("boot_flag", ctypes.c_ubyte),
        ("start_chs", ctypes.c_ubyte * 3),
        ("partition_type", ctypes.c_ubyte),
        ("end_chs", ctypes.c_ubyte * 3),
        ("start_sector", ctypes.c_uint32),
        ("num_sectors", ctypes.c_uint32)
    ]

# MBR 구조체
class MBR(ctypes.Structure):
    _fields_ = [
        ("boot_code", ctypes.c_ubyte * 446),
        ("partitions", PARTITION_ENTRY * 4),
        ("signature", ctypes.c_ushort)
    ]

    _pack_ = 1  # 패딩을 사용하지 않도록 설정

# GPT 헤더 구조체
class GPT_HEADER(ctypes.Structure):
    _fields_ = [
        ("signature", ctypes.c_char * 8),
        ("revision", ctypes.c_uint32),
        ("header_size", ctypes.c_uint32),
        ("header_crc32", ctypes.c_uint32),
        ("reserved", ctypes.c_uint32),
        ("current_lba", ctypes.c_uint64),
        ("backup_lba", ctypes.c_uint64),
        ("first_usable_lba", ctypes.c_uint64),
        ("last_usable_lba", ctypes.c_uint64),
        ("disk_guid", ctypes.c_byte * 16),
        ("partition_entry_lba", ctypes.c_uint64),
        ("num_partition_entries", ctypes.c_uint32),
        ("sizeof_partition_entry", ctypes.c_uint32),
        ("partition_entry_array_crc32", ctypes.c_uint32)
    ]

# GPT 파티션 엔트리 구조체
class GPT_PARTITION_ENTRY(ctypes.Structure):
    _fields_ = [
        ("partition_type_guid", ctypes.c_byte * 16),
        ("unique_partition_guid", ctypes.c_byte * 16),
        ("starting_lba", ctypes.c_uint64),
        ("ending_lba", ctypes.c_uint64),
        ("attributes", ctypes.c_uint64),
        ("partition_name", ctypes.c_wchar * 36)
    ]

# OVERLAPPED 구조체 정의
class OVERLAPPED(ctypes.Structure):
    _fields_ = [
        ("Internal", ctypes.c_ulong),
        ("InternalHigh", ctypes.c_ulong),
        ("Offset", ctypes.c_uint32),
        ("OffsetHigh", ctypes.c_uint32),
        ("hEvent", wintypes.HANDLE)
    ]

# Kernel32 DLL 로드
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)

# CreateFile 함수 정의
def create_file(filename, access, mode, creation, flags):
    handle = kernel32.CreateFileW(
        ctypes.c_wchar_p(filename),
        ctypes.c_ulong(access),
        ctypes.c_ulong(mode),
        None,
        ctypes.c_ulong(creation),
        ctypes.c_ulong(flags),
        None
    )
    if handle == wintypes.HANDLE(-1).value:
        error_code = ctypes.get_last_error()
        logging.error(f"Failed to open {filename}: {ctypes.WinError(error_code)}")
        raise ctypes.WinError(error_code)
    logging.info(f"Successfully opened {filename} with handle {handle}")
    return handle

# DeviceIoControl 함수 정의
def device_io_control(device, io_control_code, in_buffer, out_buffer):
    bytes_returned = wintypes.DWORD(0)
    status = kernel32.DeviceIoControl(
        wintypes.HANDLE(device),
        wintypes.DWORD(io_control_code),
        None,
        0,
        ctypes.byref(out_buffer),
        wintypes.DWORD(ctypes.sizeof(out_buffer)),
        ctypes.byref(bytes_returned),
        None
    )
    return status

# ReadFile 함수 정의
def read_file(handle, num_bytes, offset=0):
    logging.info(f"Reading {num_bytes} bytes from handle {handle} at offset {offset}")
    if handle == wintypes.HANDLE(-1).value:
        logging.error(f"Invalid handle: {handle}")
        raise ValueError("Invalid handle")
    
    data = ctypes.create_string_buffer(num_bytes)
    bytes_read = wintypes.DWORD(0)
    overlapped = OVERLAPPED()
    overlapped.Offset = offset & 0xFFFFFFFF
    overlapped.OffsetHigh = offset >> 32

    success = kernel32.ReadFile(
        wintypes.HANDLE(handle),
        data,
        num_bytes,
        ctypes.byref(bytes_read),
        None  # 동기적 호출에서는 overlapped 매개변수를 None으로 설정
    )
    logging.info(f"Success: {success}")
    if not success:
        error_code = ctypes.get_last_error()
        logging.error(f"Failed to read file: {ctypes.WinError(error_code)}")
        raise ctypes.WinError(error_code)
    logging.info(f"Successfully read {num_bytes} bytes from file")
    return data.raw

def analyze_partition(disk_number):
    drive_path = f"\\\\.\\PhysicalDrive{disk_number}"
    try:
        h_drive = create_file(drive_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, 0)
    except Exception as e:
        logging.error(f"Failed to open handle for {drive_path}: {e}")
        return []

    try:
        # MBR을 읽음
        logging.info(f"Reading MBR from {drive_path}")
        mbr_data = read_file(h_drive, 512)
        mbr = MBR.from_buffer_copy(mbr_data)
        
        # MBR signature 확인
        if mbr.signature != 0xAA55:
            raise ValueError("Invalid MBR signature")

        # GPT 확인
        logging.info(f"Reading GPT header from {drive_path}")
        gpt_header_data = read_file(h_drive, 512, 512)
        gpt_header = GPT_HEADER.from_buffer_copy(gpt_header_data)
        
        if gpt_header.signature != b'EFI PART':
            # MBR 파티션 분석
            partitions = []
            for partition in mbr.partitions:
                if partition.partition_type != 0:
                    start_sector = partition.start_sector
                    num_sectors = partition.num_sectors
                    end_sector = start_sector + num_sectors - 1
                    size = num_sectors * 512
                    size_gb = size / (1024 ** 3)
                    size_display = f"{size_gb:.2f} GB" if size_gb >= 1 else f"{size / (1024 ** 2):.2f} MB"
                    partitions.append({
                        "Start Sector": start_sector,
                        "End Sector": end_sector,
                        "Total Sectors": num_sectors,
                        "Size": size_display
                    })
            return partitions

        # GPT 파티션 분석
        partitions = []
        partition_entries_data = read_file(h_drive, gpt_header.num_partition_entries * gpt_header.sizeof_partition_entry, gpt_header.partition_entry_lba * 512)
        for i in range(gpt_header.num_partition_entries):
            offset = i * gpt_header.sizeof_partition_entry
            partition_entry = GPT_PARTITION_ENTRY.from_buffer_copy(partition_entries_data[offset:offset + gpt_header.sizeof_partition_entry])
            if partition_entry.starting_lba != 0:
                start_sector = partition_entry.starting_lba
                end_sector = partition_entry.ending_lba
                num_sectors = end_sector - start_sector + 1
                size = num_sectors * 512
                size_gb = size / (1024 ** 3)
                size_display = f"{size_gb:.2f} GB" if size_gb >= 1 else f"{size / (1024 ** 2):.2f} MB"
                partitions.append({
                    "Start Sector": start_sector,
                    "End Sector": end_sector,
                    "Total Sectors": num_sectors,
                    "Size": size_display
                })

        return partitions

    finally:
        kernel32.CloseHandle(wintypes.HANDLE(h_drive))

def is_admin():
    try:
        return ctypes.windll.shell32.IsUserAnAdmin()
    except:
        return False

# 물리 드라이브 정보를 가져오는 함수
def get_physical_drives():
    logging.info("Fetching physical drives")
    try:
        result = subprocess.check_output("wmic diskdrive get DeviceID, Model, Size", shell=True).decode('cp949').strip()
        logging.debug(f"Raw wmic output: {result}")

        lines = result.split('\n')[1:]  # 첫 번째 줄은 헤더이므로 제외
        drives_info = []
        for line in lines:
            parts = line.strip().split()
            if len(parts) < 3:
                continue

            drive = parts[0]
            model = ' '.join(parts[1:-1])

            try:
                # 마지막 섹터 번호를 기반으로 크기 계산
                disk_number = int(drive[-1])
                last_sector_number = get_last_sector_number(disk_number)
                bytes_per_sector = 512  # 일반적으로 섹터 크기는 512 바이트입니다.
                total_size = (last_sector_number + 1) * bytes_per_sector

                # Size 변환 및 표시 단위 결정
                size_in_gb = total_size / (1024 ** 3)  # bytes to gigabytes
                if size_in_gb < 1:
                    size_in_mb = size_in_gb * 1024  # Convert GB to MB
                    display_size = f"{size_in_mb:.2f} MB"
                else:
                    display_size = f"{size_in_gb:.2f} GB"

            except Exception as e:
                logging.error(f"Failed to get size for {drive}: {e}")
                display_size = "Unknown"

            drives_info.append((drive, model, display_size))
            logging.debug(f"Drive: {drive}, Model: {model}, Size: {display_size}")

        return drives_info

    except subprocess.CalledProcessError as e:
        messagebox.showerror("오류", "물리 드라이브를 조회하는 중 오류가 발생했습니다.")
        logging.error("Failed to get physical drives: %s", str(e))
        return []
    except Exception as e:
        messagebox.showerror("오류", "예상치 못한 오류가 발생했습니다.")
        logging.error("Unexpected error: %s", str(e))
        return []

# 마지막 섹터 번호를 계산하는 함수
def get_last_sector_number(disk_number):
    drive_path = f"\\\\.\\PhysicalDrive{disk_number}"
    try:
        h_drive = create_file(drive_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, 0)
    except Exception as e:
        logging.error(f"Failed to open handle for {drive_path}: {e}")
        return -1

    try:
        length_info = GET_LENGTH_INFORMATION()
        if not device_io_control(h_drive, IOCTL_DISK_GET_LENGTH_INFO, None, length_info):
            raise ctypes.WinError(ctypes.get_last_error())

        disk_geometry = DISK_GEOMETRY()
        if not device_io_control(h_drive, IOCTL_DISK_GET_DRIVE_GEOMETRY, None, disk_geometry):
            raise ctypes.WinError(ctypes.get_last_error())

        total_length = length_info.Length
        bytes_per_sector = disk_geometry.BytesPerSector
        last_sector_number = (total_length // bytes_per_sector) - 1

        return last_sector_number

    finally:
        kernel32.CloseHandle(wintypes.HANDLE(h_drive))

class DiskAnalyzerApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Disk Analyzer")
        self.geometry("600x400")
        self.create_widgets()

    def create_widgets(self):
        self.drive_listbox = tk.Listbox(self)
        self.drive_listbox.pack(fill=tk.BOTH, expand=True)

        self.analyze_button = tk.Button(self, text="Analyze Disk", command=self.analyze_selected_disk)
        self.analyze_button.pack()

        self.partition_info_text = tk.Text(self)
        self.partition_info_text.pack(fill=tk.BOTH, expand=True)

        self.load_drives()

    def load_drives(self):
        drives = get_physical_drives()
        for drive in drives:
            self.drive_listbox.insert(tk.END, f"{drive[0]} - {drive[1]} - {drive[2]}")

    def analyze_selected_disk(self):
        selection = self.drive_listbox.curselection()
        if not selection:
            messagebox.showwarning("경고", "분석할 디스크를 선택하세요.")
            return

        drive = self.drive_listbox.get(selection[0]).split()[0]
        disk_number = int(drive[-1])

        partitions = analyze_partition(disk_number)
        self.partition_info_text.delete(1.0, tk.END)
        for partition in partitions:
            self.partition_info_text.insert(tk.END, f"Start Sector: {partition['Start Sector']}\n")
            self.partition_info_text.insert(tk.END, f"End Sector: {partition['End Sector']}\n")
            self.partition_info_text.insert(tk.END, f"Total Sectors: {partition['Total Sectors']}\n")
            self.partition_info_text.insert(tk.END, f"Size: {partition['Size']}\n")
            self.partition_info_text.insert(tk.END, "\n")

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    if not is_admin():
        messagebox.showerror("오류", "이 프로그램을 실행하려면 관리자 권한이 필요합니다.")
    else:
        app = DiskAnalyzerApp()
        app.mainloop()

  • 위 소스로 실행파일 형태로 만들어 보았습니다. 아래 프로그램 다운로드 후 해당 파일의 해시값(md5, sha256)을 비교해보고 정확하지 않으면 어떤 소스인지 검증할 수 없으니 꼭 잘 알아보고 사용하도록 하자. 아래 실습 화면을 이어서 참고

다운로드 파일 정상 파일인지 확인하기!

  1. 먼저 다운로드한 프로그램의 압축해제 후 "물리이미징 도구.exe"가 있는 폴더에 빈공간에 [Shift] 키를 누른 상태에서 우클릭 후 [여기에 PowerShell 창 열기] 를 클릭

  2. 아래 명령어를 입력하여 hash값을 확인하여 봅시다. > get-filehash "Partition 분석.exe" -algorithm md5 / 띄워쓰기가 있는 경우 " ~~ " 를 사용하자. > get-filehash "Partition 분석.exe"

  3. 아래의 해시값이 맞는지 확인해봅시다. 아래 해시값이 아니면, 제대로 된 파일이 아니라는 뜻임을 잘 알 수 있습니다. md5 : D70636A3FD5164086601D44E5BD0DD25 sha256 : 3C9DD390BA905F1D10908F27FE95920BF32678CEBC338C81DFEBB61B9FAFD211

사용법

  1. 프로그램을 실행하면, 관리자 권한이 아닌 경우 아래와 같이 표시됩니다.

  2. 프로그램 우클릭 후 [관리자 권한으로 실행]해봅시다. 사용법은 매우 간단합니다. 해당 물리매체를 선택 후 Analyze Disk 를 선택하면 시작 섹터, 마지막 섹터 번호, 전체 섹터 수 그리고 전체 용량이 표시됩니다. 우리가 GPT 로 파티션 나눠둔 영역 그리고 그냥 MBR 파티션 영역도 모두 확인이 가능합니다.

  3. FTK Imager에서 디스크를 인식 후 비교해 봅시다. 우리가 만든 도구에서는 섹터관련 정보만 있으나, 우리는 GUID 등의 정보도 어떤 값을 통해 만들었는지 알 수 있기 때문에 소스를 수정하면 다른 영역의 값도 충분히 추가하여 도구를 만들 수 있습니다.

3줄 요약

  1. 저장매체에서 파티션 별로 시작 섹터와 마지막 섹터번호, 파티션의 총 섹터 수를 가진다.

  2. 대부분 파티션의 상세한 정보를 이미 많이 알려져 있기 때문에 우리가 모든 영역을 일일히 직접 만들 수고는 많이 줄었습니다. 대신 물리적으로 0번 섹터부터 접근하는 경우는 거의 없기 때문에 소스에서 그 부분을 잘 하고 있는지, 어떤 방법으로 하고 있는지 잘 알아둘 필요가 있습니다.

  3. 물리이미징 수집에서 활용했던 소스도 잘 활용해보고, 이번에 파티션에 사용했던 소스도 추후에 잘 이용해보도록 합시다.

0-2) Python 을 이용한 개발 환경 구성