Microsoft MVP성태의 닷넷 이야기
.NET Framework: 556. C#으로 다루는 MBR(Master Boot Record) [링크 복사], [링크+제목 복사],
조회: 28090
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
[mbr_cs.zip]    
(연관된 글이 3개 있습니다.)

C#으로 다루는 MBR(Master Boot Record)

최근에 트위터를 통해 공유된 다음의 자료를 읽게 되었습니다.

Windows MBR 분석 (MBR 분석을 통한 자동화 도구 제작 및 악성코드 분석)
; https://drive.google.com/file/d/0B3t7Uu3cQBgYZG5TS04wVTVOVkE/view

그래서, C#으로 간단하게 MBR 내용을 보여주는 프로그램을 만들어 보았습니다.

먼저 해야 할 일이 하드 디스크를 File System을 통하지 않고 직접 읽을 수 있어야 하는데요. 이는 다음의 코드를 이용하면 쉽게 구현할 수 있습니다.

CCS LABS C#: Low Level Disk Access
; https://code.msdn.microsoft.com/windowsapps/CCS-LABS-C-Low-Level-Disk-91676ca9

이 소스 코드에서 디스크 장치 목록을 구하는 것은 다음의 코드입니다.

using System.Management;

// Install-Package System.Management
internal class Program
{
    static void Main(string[] args)
    {
        foreach (Win32_DiskDrive item in GetDriveList())
        {
            Console.WriteLine(item.DeviceID);
            Console.WriteLine(item.Caption);
            Console.WriteLine(item.Description);
            Console.WriteLine(item.Index);
            Console.WriteLine(item.SerialNumber);
            Console.WriteLine(item.PNPDeviceID);
            Console.WriteLine();
        }
    }
    
    public static List<Win32_DiskDrive> GetDriveList()
    {
        List<Win32_DiskDrive> drives = new List<Win32_DiskDrive>();

        try
        {
            ManagementObjectSearcher searcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_DiskDrive");

            foreach (ManagementObject queryObj in searcher.Get())
            {
                Win32_DiskDrive item = new Win32_DiskDrive();

                item.DeviceID = queryObj["DeviceID"].ToString();
                item.Caption = queryObj["Caption"].ToString();
                item.Description = queryObj["Description"].ToString();
                item.Index = (uint)queryObj["Index"];

                item.SerialNumber = queryObj["SerialNumber"].ToString().Trim();
                item.PNPDeviceID = queryObj["PNPDeviceID"].ToString();

                drives.Add(item);
            }
        }
        catch (ManagementException)
        {
            return null;
        }

        return drives;
    }
}

public class Win32_DiskDrive
{
    public string DeviceID;
    public string Caption;
    public string Description;
    public uint Index;
    public string SerialNumber;
    public string PNPDeviceID;
}

drivelist에 담겨지는 장치명은 이런 식입니다.

\\.\PHYSICALDRIVE2
Samsung SSD 980 PRO 1TB
Disk drive
2
0025_38B8_11C6_1162.
SCSI\DISK&VEN_NVME&PROD_SAMSUNG_SSD_980\5&EBD909B&0&000000

\\.\PHYSICALDRIVE1
Samsung SSD 980 PRO 1TB
Disk drive
1
0025_38B8_11C6_117E.
SCSI\DISK&VEN_NVME&PROD_SAMSUNG_SSD_980\5&20DA63D3&0&000000

\\.\PHYSICALDRIVE0
WDC WD20EZBX-00AYRA0
Disk drive
0
WD-WXT2AC0JRNHX
SCSI\DISK&VEN_WDC&PROD_WD20EZBX-00AYRA0\4&E91BE7&0&040000

위의 출력은 디스크가 3개 있는 경우입니다. 그다음 이 장치명을 이용해 CreateFile을 호출하면 되는데요. 이 역시 다음의 소스 코드를 이용해 간단하게 작성할 수 있습니다.

internal byte[] DumpSector(string drive, double sector, int bytesPerSector)
{
    uint GENERIC_READ = 0x80000000;
    uint OPEN_EXISTING = 3;
    uint FILE_SHARE_READWRITE = 0x01 | 0x02;

    SafeFileHandle handleValue = UnsafeNativeMethods.CreateFile(drive, GENERIC_READ, FILE_SHARE_READWRITE, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);
    
    if (handleValue.IsInvalid)
    {
        Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
    }

    double sec = sector * bytesPerSector;

    int size = int.Parse(bytesPerSector.ToString());
    byte[] buf = new byte[size];
    int read = 0;
    int moveToHigh;
    UnsafeNativeMethods.SetFilePointer(handleValue, int.Parse(sec.ToString()), out moveToHigh, EMoveMethod.Begin);
    ReadFile(handleValue, buf, size, out read, IntPtr.Zero);
    handleValue.Close();
    return buf;
}

위의 메서드를 이용해 MBR이 저장된 0번 섹터의 내용을 읽어들이면 됩니다.

byte[] sector = DumpSector(driveName, 0, bytesPerSector);

이제 sector에 담겨진 내용을 "Windows MBR 분석 (MBR 분석을 통한 자동화 도구 제작 및 악성코드 분석)" 문서에서 설명한 포맷에 맞게 해석만 하면 됩니다. C#이라면, 우선 다음과 같은 구조체로 출발할 수 있습니다.

[StructLayout(LayoutKind.Sequential, Size = 512, Pack = 1)]
public struct MBR
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 446)]
    public byte[] BootCode;

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public PartitionEntry [] Partitions;

    public ushort Signature;
}

[StructLayout(LayoutKind.Explicit, Size = 16, Pack = 1)]
public unsafe struct PartitionEntry
{
    [FieldOffset(0)]
    public byte BootFlag;

    [FieldOffset(1)]
    fixed byte _startingCHSAddress[3];

    public unsafe int StartingCHSAddress
    {
        get
        {
            fixed (byte* pBytes = _startingCHSAddress)
            {
                return ToInt32(pBytes);
            }
        }
    }

    [FieldOffset(4)]
    public byte Type;

    [FieldOffset(5)]
    fixed byte _endingCHSAddress[3];

    public int EndingCHSAddress
    {
        get
        {
            fixed (byte *pBytes = _endingCHSAddress)
            {
                return ToInt32(pBytes);
            }
        }
    }

    [FieldOffset(8)]
    public uint StartingLBAAddress;

    [FieldOffset(12)]
    public uint SizeOfSector;

    unsafe int ToInt32(byte* pBytes)
    {
        byte[] intBytes = new byte[4];

        intBytes[0] = *pBytes;
        intBytes[1] = *(pBytes + 1);
        intBytes[2] = *(pBytes + 2);

        return BitConverter.ToInt32(intBytes, 0);
    }
}

이후 작업은 간단합니다. 디스크로부터 읽어들인 바이트 배열에서 MBR 구조체를 다음과 같이 복원해 주면 됩니다.

internal static bool TryParse(byte[] sector, out MBR mbr)
{
    mbr = ByteArrayToStructure<MBR>(sector);
    return mbr.Signature == 0xaa55;
}

// which marshalling method is better? 
// http://stackoverflow.com/questions/14465722/which-marshalling-method-is-better
static T ByteArrayToStructure<T>(byte[] bytes) where T : struct
{
    GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
    T stuff = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
    handle.Free();
    return stuff;
}

제 컴퓨터 기준으로 (관리자 권한으로 실행시켜) 출력해 보니 이렇군요. ^^

\\.\PHYSICALDRIVE0
Bootable =  True, Type = NTFS    , SectorSize = 0x000af000, CHS = 0x002120 ~ 0x2c12be, BeginLBA = 0x00000800
Bootable = False, Type = Extended, SectorSize = 0x0a6d383f, CHS = 0x2c33dd ~ 0xfffffe, BeginLBA = 0x000affc1
Bootable = False, Type = NTFS    , SectorSize = 0x1356e800, CHS = 0xfffffe ~ 0xfffffe, BeginLBA = 0x0a783800
(undefined)

\\.\PHYSICALDRIVE1
Bootable = False, Type = GPT(protective), SectorSize = 0xffffffff, CHS = 0x000200 ~ 0xffffff, BeginLBA = 0x00000001
(undefined)
(undefined)
(undefined)

PHYSICALDRIVE1의 출력이 흥미로운데요. 해당 디스크가 MBR 형식이 아닌 GPT 파티션으로 되어 있기 때문에 분석이 저렇게 밖에 안된 것입니다.

(첨부한 파일은 위의 코드를 모두 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/24/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2016-03-09 01시30분
MBR의 부트 코드 바이트 배열을 x86 mnemonic으로 역어셈블한 코드 내용은 다음의 글에서 확인할 수 있습니다.

머신 바이트 배열로부터 역어셈블해주는 라이브러리 - Udis86 Assembler
; http://www.sysnet.pe.kr/2/0/10916
정성태
2020-03-18 01시30분
[lolzz] 관리자님 안녕하세요 궁금한점이 있습니다.
public unsafe struct PartitionEntry
{
    [FieldOffset(0)]
    public byte BootFlag;

    [FieldOffset(1)]
    fixed byte _startingCHSAddress[3];

    public unsafe int StartingCHSAddress
    {
        get
        {
            fixed (byte* pBytes = _startingCHSAddress)
            {
                return ToInt32(pBytes);
            }
        }
    }

    [FieldOffset(4)]
    public byte Type;
---------------------
다른 cs파일을 만들어서 patitionEntry에 있는 Type(filesystem을 나타내주는) 변수를 가져오고 싶은데 어떻게 접근을 해야할까요??
[guest]
2020-03-18 01시54분
@lolzz 질문을 이해하지 못하겠습니다. struct의 필드 값을 다른 cs 파일에서 접근하는데, 뭘 더 할 필요가 있나요?
정성태
2020-03-18 02시59분
[lolzz] public override string ToString()
        {
            if (SizeOfSector == 0)
            {
                return "(undefined)";
            }

            return string.Format("Type = {0,-8}, GetPartitionTypeName(Type));
        }

        private string GetPartitionTypeName(byte type)
        {
            switch (type)
            {
                case 0x07:
                    return "NTFS";

                case 0x0c:
                    return "FAT32";

                case 0x0b:
                    return "FAT32";

                case 0x0e:
                    return "FAT";

                case 0x0F:
                    return "Extended";

                case 0xee:
                    return "GPT(protective)";

                default:
                    return string.Format("Uknown(0x{0:x})", type);
            }
        }
    }
}
다른 cs 파일에서 GetPartitionTypeName(Type) < 이 함수를 호출한다음 값을 가져오고 싶은데
방법이 있을까요? 객체를 생성해서 접근하려고 했더니 원하는 값이 안나왔습니다..
[guest]
2020-03-18 04시00분
혹시,

private string GetPartitionTypeName(byte type)

위의 코드에서 private 접근자를 public으로 바꾸면 해결되는 것을 묻는 건가요?
정성태
2020-03-19 09시18분
[lolzz] 아뇨 ㅠㅠ public으로 변환을 해도 unknowon 값을 가져오더라구요
Partition test = new PartitionEntry() // 객체생성
var filetype = test.GetPartitionEntry(test.type);
console.writeLine("type : {0}",filetype);
이런식으로 접근을 하려고 했는데 안되네요,,
[guest]
2020-03-19 09시40분
그렇게 사용하면 당연히 test.Type의 값이 0이어서 Unknown이 나옵니다. PartitionEntry는 그냥 구조체일 뿐이지 스스로 MBR의 내용을 읽어와 초기화하는 코드는 없습니다.

초기화 부분은 MasterBootRecord.cs의 MBR.TryParse 코드에서 시작됩니다.
정성태
2020-03-19 10시04분
[lolzz] 아하,,, 감사합니다 일단 알려주신대로 출력 해보겠습니다.
감사합니다!
[guest]
2023-02-13 09시42분
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
14001정성태8/19/2025116닷넷: 2356. .NET SDK 10 - 단일 소스 코드 파일을 빌드/실행하는 기능을 "dotnet" 명령어에 추가
14000정성태8/18/2025504오류 유형: 979. ERROR: failed to solve: failed to read dockerfile: open Dockerfile: no such file or directory
13999정성태8/15/20251040닷넷: 2355. C# 14 - (8) null 조건부 연산자 개선 - 대입문에도 사용 가능파일 다운로드1
13998정성태8/14/2025993닷넷: 2354. C# 14 - (7) 확장 메서드에 정적 메서드와 속성 지원을 위한 전용 구문 추가파일 다운로드1
13997정성태8/14/20251141Linux: 120. docker 컨테이너로 매핑된 볼륨에 컨테이너 측의 사용자 ID를 유지하면서 복사하는 방법
13996정성태8/13/2025689오류 유형: 978. Unable to find the requested .Net Framework Data Provider.
13995정성태8/13/2025720개발 환경 구성: 754. Visual C++ - 리눅스 빌드를 위한 Ubuntu 18 docker 컨테이너 설정
13994정성태8/12/2025707오류 유형: 977. SQL Server - User, group, or role '...' already exists in the current database. (Microsoft SQL Server, Error: 15023)
13993정성태8/11/20251052오류 유형: 976. Microsoft.ML.OnnxRuntimeGenAI 패키지 사용 시 "cublasLt64_12.dll" which is missing. (Error 126: "The specified module could not be found.") 오류
13992정성태8/11/20251264닷넷: 2353. C# - Foundry Local을 이용한 gpt-oss-20b 모델 사용파일 다운로드1
13991정성태8/9/20251197오류 유형: 975. winget - Foundry Local 패키지 업데이트가 안 되는 문제
13990정성태8/8/2025897Windows: 283. Time zone 설정이 없는 Windows Server 2025
13989정성태8/8/20251307닷넷: 2352. C# - Windows S-mode 환경인지 체크하는 방법파일 다운로드1
13988정성태8/8/20251441오류 유형: 974. 비주얼 스튜디오 업데이트 시 잠김 파일 경고 - Visual Studio Standard Collector Service 150 (VSStandardCollectorService150)
13987정성태8/7/20251095닷넷: 2351. C# 14 - (6) event와 생성자에도 partial 메서드 적용파일 다운로드1
13986정성태8/6/20251195닷넷: 2350. C# 14 - (5) 람다 매개 변수에 접근자가 있는 경우에도 타입 생략 가능파일 다운로드1
13985정성태8/6/20251601오류 유형: 973. "wsl --install" 명령어 수행 시 "The server name or address could not be resolved"
13984정성태8/6/20251311Windows: 282. 윈도우 운영체제에 추가된 ssh 서버(Win32-OpenSSH)
13983정성태8/4/20251421오류 유형: 972. Microsoft.Data.SqlClient 6.1.0 버전부터 .NET 8 이상만 지원
13982정성태8/2/20251856개발 환경 구성: 753. CentOS 7 컨테이너 내에서 openssh 서버 호스팅
13981정성태8/1/20251506오류 유형: 971. CentOS 7에서 yum 사용 시 "Could not resolve host: mirrorlist.centos.org; Unknown error"
13980정성태7/31/20251639Linux: 119. eBPF - BPF_PROG_TYPE_CGROUP_SOCK 유형에서 정상 동작하지 않는 BPF_CORE_READ (2)
13979정성태7/30/20251982Linux: 118. eBPF - BPF_PROG_TYPE_CGROUP_SOCK 유형에서 정상 동작하지 않는 BPF_CORE_READ
13978정성태7/29/20251742오류 유형: 970. 파일 복사 시 "Data error (cyclic redundancy check). (0x80070017)" 에러
13977정성태7/28/20252071닷넷: 2349. C# 14 - (4) 문자열 리터럴을 utf-8 인코딩으로 저장파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...