Встроенный код сборки для получения идентификатора процессора

Я нашел хороший фрагмент кода, который выполняет инструкции ASM с использованием вызовов API для получения серийного номера ЦП:

using System;
using System.Text;
using System.Runtime.InteropServices;

namespace ConsoleApplication1
{
    class Program
    {
        [DllImport("user32", EntryPoint = "CallWindowProcW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]         private static extern IntPtr ExecuteNativeCode([In] byte[] bytes, IntPtr hWnd, int msg, [In, Out] byte[] wParam, IntPtr lParam);

        [return: MarshalAs(UnmanagedType.Bool)]
        [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]         public static extern bool VirtualProtect([In] byte[] bytes, IntPtr size, int newProtect, out int oldProtect);

        const int PAGE_EXECUTE_READWRITE = 0x40;

        static void Main(string[] args)
        {
            string s = CPU32_SerialNumber();
            Console.WriteLine("CPU Serial-Number: " + s);
            Console.ReadLine();
        }

        private static string CPU32_SerialNumber()
        {
            byte[] sn = new byte[12];

            if (!ExecuteCode32(ref sn))
                return "ND";

            return string.Format("{0}{1}{2}", BitConverter.ToUInt32(sn, 0).ToString("X"), BitConverter.ToUInt32(sn, 4).ToString("X"), BitConverter.ToUInt32(sn, 8).ToString("X"));
        }

        private static bool ExecuteCode32(ref byte[] result)
        {
            // CPU 32bit SerialNumber -> asm x86 from c# (c) 2003-2011 Cantelmo Software
            // 55               PUSH EBP
            // 8BEC             MOV EBP,ESP
            // 8B7D 10          MOV EDI,DWORD PTR SS:[EBP+10]
            // 6A 02            PUSH 2
            // 58               POP EAX
            // 0FA2             CPUID
            // 891F             MOV DWORD PTR DS:[EDI],EBX
            // 894F 04          MOV DWORD PTR DS:[EDI+4],ECX
            // 8957 08          MOV DWORD PTR DS:[EDI+8],EDX
            // 8BE5             MOV ESP,EBP
            // 5D               POP EBP
            // C2 1000          RETN 10

            int num;

            byte[] code_32bit = new byte[] { 0x55, 0x8b, 0xec, 0x8b, 0x7d, 0x10, 0x6a, 2, 0x58, 15, 0xa2, 0x89, 0x1f, 0x89, 0x4f, 4, 0x89, 0x57, 8, 0x8b, 0xe5, 0x5d, 0xc2, 0x10, 0 };
            IntPtr ptr = new IntPtr(code_32bit.Length);

            if (!VirtualProtect(code_32bit, ptr, PAGE_EXECUTE_READWRITE, out num))
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

            ptr = new IntPtr(result.Length);

            return (ExecuteNativeCode(code_32bit, IntPtr.Zero, 0, result, ptr) != IntPtr.Zero);
        }
    }
}

Я проверил это, и он работает нормально для меня. Но у меня все еще есть некоторые вопросы и проблемы, связанные с этим:

1) Я хотел бы реализовать этот код внутри приложения, которое может работать как в среде x86, так и в среде x64. Если я запускаю этот код в среде 64x, я получаю AccessViolationException. Автор кода сказал, что этого легко достичь, внедрив также массив байт-кода, содержащий инструкции x64 (RAX, RBX, RCX, RDX, ...). Моя проблема в том, что я абсолютно не знаю, как преобразовать 86-байтовый код в x64-байтовый код, я даже не знаю ASM. Есть ли какая-либо таблица преобразования или утилита, которая может сделать это?

2) Является ли этот фрагмент кода действительным для любого типа процессора? Я проверил его на своем ноутбуке, который использует ядро ​​Intel, и он работает... но как быть с AMD, например?

3) Я не уверен, что значение, которое я получаю, является правильным. Если я запускаю следующий код:

string cpuInfo = String.Empty;

System.Management.ManagementClass mc = new System.Management.ManagementClass("Win32_Processor");
System.Management.ManagementObjectCollection moc = mc.GetInstances();

foreach (System.Management.ManagementObject mo in moc)
{
    if (cpuInfo == String.Empty)
        cpuInfo = mo.Properties["ProcessorId"].Value.ToString();
}

В результате я получаю "BFEBFBFF000306A9". Результатом фрагмента кода является "F0B2FF0CA0000". Зачем? Который правильный?

2 ответа

Решение

Вот ваш код, модифицированный так, чтобы получить тот же результат, что и Win32_Processor.ProcessorId на x64 и x86:

using System;
using System.Text;
using System.Runtime.InteropServices;

namespace ConsoleApplication1
{
    class Program
    {
        [DllImport("user32", EntryPoint = "CallWindowProcW", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]         private static extern IntPtr CallWindowProcW([In] byte[] bytes, IntPtr hWnd, int msg, [In, Out] byte[] wParam, IntPtr lParam);

        [return: MarshalAs(UnmanagedType.Bool)]
        [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]         public static extern bool VirtualProtect([In] byte[] bytes, IntPtr size, int newProtect, out int oldProtect);

        const int PAGE_EXECUTE_READWRITE = 0x40;

        static void Main(string[] args)
        {
            string s = ProcessorId();
            Console.WriteLine("ProcessorId: " + s);
            Console.ReadLine();
        }

        private static string ProcessorId()
        {
            byte[] sn = new byte[8];

            if (!ExecuteCode(ref sn))
                return "ND";

            return string.Format("{0}{1}", BitConverter.ToUInt32(sn, 4).ToString("X8"), BitConverter.ToUInt32(sn, 0).ToString("X8"));
        }

        private static bool ExecuteCode(ref byte[] result)
        {
            int num;

            /* The opcodes below implement a C function with the signature:
             * __stdcall CpuIdWindowProc(hWnd, Msg, wParam, lParam);
             * with wParam interpreted as a pointer pointing to an 8 byte unsigned character buffer.
             * */

            byte[] code_x86 = new byte[] {
                0x55,                      /* push ebp */
                0x89, 0xe5,                /* mov  ebp, esp */
                0x57,                      /* push edi */
                0x8b, 0x7d, 0x10,          /* mov  edi, [ebp+0x10] */
                0x6a, 0x01,                /* push 0x1 */
                0x58,                      /* pop  eax */
                0x53,                      /* push ebx */
                0x0f, 0xa2,                /* cpuid    */
                0x89, 0x07,                /* mov  [edi], eax */
                0x89, 0x57, 0x04,          /* mov  [edi+0x4], edx */
                0x5b,                      /* pop  ebx */
                0x5f,                      /* pop  edi */
                0x89, 0xec,                /* mov  esp, ebp */
                0x5d,                      /* pop  ebp */
                0xc2, 0x10, 0x00,          /* ret  0x10 */
            };
            byte[] code_x64 = new byte[] {
                0x53,                                     /* push rbx */
                0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, /* mov rax, 0x1 */
                0x0f, 0xa2,                               /* cpuid */
                0x41, 0x89, 0x00,                         /* mov [r8], eax */
                0x41, 0x89, 0x50, 0x04,                   /* mov [r8+0x4], edx */
                0x5b,                                     /* pop rbx */
                0xc3,                                     /* ret */
            };

            ref byte[] code;

            if (IsX64Process())
                code = ref code_x64;
            else 
                code = ref code_x86;

            IntPtr ptr = new IntPtr(code.Length);

            if (!VirtualProtect(code, ptr, PAGE_EXECUTE_READWRITE, out num))
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

            ptr = new IntPtr(result.Length);

            return (CallWindowProcW(code, IntPtr.Zero, 0, result, ptr) != IntPtr.Zero);
        }

        private static bool IsX64Process() 
        {
            return IntPtr.Size == 8;
        }
    }
}

Я сделал тривиальные изменения в части C# без компиляции кода (в данный момент у меня нет настройки машины для Windows), так что если есть синтаксические ошибки, просто исправьте очевидное.

Я хочу подчеркнуть один очень важный момент: ваш исходный код НЕ считывал серийный номер процессора:

  • Вы использовали функцию CPUID 2 (поместив 2 в EAX перед выполнением инструкции CPUID). Если вы прочтете примечания к приложениям Intel и AMD CPUID, то увидите, что они считывают кеш и конфигурацию оборудования TLB и поддерживаются только в Intel.
  • Я изменил ваш код, чтобы использовать функцию CPUID 1, которая считывает пошаговые данные, модель и семейство CPU. Это соответствует поведению WIN32_Processor.ProcessorID
  • Современные процессоры x86 не имеют серийного номера, который был бы уникальным среди идентичных устройств, "сходящих с конвейера". Серийные номера процессора были доступны только на Pentium 3 через функцию CPUID 3.

Теперь я объясню процесс и инструменты, которые я использовал.

Вставьте массив кодов операций в скрипт Python, который затем запишет коды операций в двоичный файл (cpuid-x86.bin):

cpuid_opcodes = [ 0x55, 0x8b, 0xec, 0x8b, ... ]
open('cpuid-x86.bin', 'w').write(''.join(chr(x) for x in cpuid_opcodes))

Разберите cpuid-x86.bin. Я использовал udcli из udis86.

$ udcli -att cpuid-x86.bin
0000000000000000 55               push %ebp               
0000000000000001 8bec             mov %esp, %ebp          
0000000000000003 8b7d10           mov 0x10(%ebp), %edi    
0000000000000006 6a02             push $0x2                
0000000000000008 58               pop %eax                
0000000000000009 0fa2             cpuid                   
000000000000000b 891f             mov %ebx, (%edi)        
000000000000000d 894f04           mov %ecx, 0x4(%edi)     
0000000000000010 895708           mov %edx, 0x8(%edi)     
0000000000000013 8be5             mov %ebp, %esp          
0000000000000015 5d               pop %ebp                
0000000000000016 c21000           ret $0x10 

Одна вещь, которая сразу бросается в глаза - зачем использовать " push $ 0x2; pop% eax ", чтобы переместить значение 2 в EAX, когда подойдет простой " mov $ 0x2,% eax "?

Я предполагаю, что кодировка инструкции для " push $ 0x2 ", 6a02, легче изменить в шестнадцатеричной форме. Как вручную, так и программно. Я предполагаю, что кто-то где-то пытался использовать функцию CPUID 3 для получения серийного номера процессора и обнаружил, что он не поддерживается, а затем переключился на использование функции 2.

" Ret $ 0x10 " в конце также необычен. Форма RET IMM16 инструкции RET возвращает вызывающей стороне, а затем выталкивает байты IMM16 из стека. Тот факт, что вызываемый объект отвечает за выталкивание аргументов из стека после возврата функции, подразумевает, что он не использует стандартное соглашение о вызовах x86.

Действительно, быстрый взгляд на код C# показывает, что он использует CallWindowProc() для вызова функции сборки. Документация по CallWindowProc() показывает, что ассемблерный код реализует функцию C с такой сигнатурой:

__stdcall CpuIdWindowProc(hWnd, Msg, wParam, lParam);

__stdcall - это специальное соглашение о вызовах функций, используемое 32-битными Windows API.

Код сборки использует 0x10(%ebp), который является третьим аргументом функции, в качестве массива символов для хранения выходных данных инструкции CPUID. (После стандартного пролога функции на x86, 8 (% ebp) - первый аргумент. 0xc (% ebp) - второй 4-байтовый аргумент, а 0x10(%ebp) - третий) Третий параметр в нашем прототипе функции оконной процедуры выше wParam. Он используется как выходной параметр и является единственным параметром, используемым в коде сборки.

Последняя интересная вещь в коде ассемблера состоит в том, что он забивает регистры EDI и EBX без их сохранения, нарушая соглашение о вызовах __stdcall. Эта ошибка, очевидно, скрыта при вызове функции через CallWindowProc(), но обнаружится, если вы попытаетесь написать свою основную функцию на C для проверки кода сборки (cpuid-main.c):

#include <stdio.h>
#include <stdint.h>

void __stdcall cpuid_wind_proc(uint32_t hWnd, uint32_t msg, uint8_t *wparam, uint32_t lparam);

enum {
    RESULT_SIZE = 2 * 4, /* Two 32-bit registers: EAX, EDX */
};

static unsigned int form_word_le(uint8_t a[])
{
    return (a[3] << 24) | (a[2] << 16) | (a[1] << 8) | a[0];
}

int main()
{
    uint8_t r[RESULT_SIZE];
    memset(r, 0, sizeof(r));

    cpuid_wind_proc(0, 0, r, 0);

    printf("%08x%08x\n",  form_word_le(r + 4), form_word_le(r));
    return 0;
}

Версия сборки, исправленная для сохранения и восстановления EDI, EBX и использования функции CPUID 1, выглядит следующим образом:

    .section .text
    .global _cpuid_wind_proc@16
_cpuid_wind_proc@16:
    push %ebp
    mov %esp, %ebp
    push %edi
    mov 16(%ebp), %edi
    push $1
    pop %eax
    push %ebx
    cpuid
    mov %eax, (%edi)
    mov %edx, 0x4(%edi)
    pop %ebx
    pop %edi
    mov %ebp, %esp
    pop %ebp
    ret $16

Имя символа _cpuid_wind_proc@16 - это то, как имена функций __stdcall искажаются в 32-битной Windows. @ 16 - это число байтов, которые занимают параметры. (Четыре параметра, каждый из которых занимает четыре байта в 32-битной Windows, в сумме дают 16)

Теперь я готов портировать код на x64.

  • Изучив эту удобную таблицу ABI, я вижу, что первые четыре параметра передаются в RCX, RDX, R8 и R9, поэтому wParam находится в R8.
  • Документация Intel говорит мне, что инструкция CPUID забивает EAX, EBX, ECX и EDX. EBX - это нижняя половина RBX, которая является сохраненным GPR в ABI ("сохраненный GPR" здесь означает регистр общего назначения, который должен сохранять свое содержимое при вызове функции), поэтому я обязательно сохранил RBX перед выполнением инструкции CPUID и восстановлением RBX потом.

Вот сборка x64:

    .section .text
    .global cpuid_wind_proc
cpuid_wind_proc:
    push %rbx
    mov $1, %rax
    cpuid
    movl %eax, (%r8)
    movl %edx, 4(%r8)
    pop %rbx
    ret

Как видите, версия для x64 короче и проще для написания. В x64 существует только одно соглашение о вызовах функций, поэтому нам не нужно беспокоиться о __stdcall.

Создайте функцию сборки x64 вместе с cpuid-main.c и сравните ее вывод с этим VBScript (cpuid.vbs):

Set objProc = GetObject("winmgmts:root\cimv2:Win32_Processor='cpu0'")
WScript.echo objProc.ProcessorId

Запустите cpuid.vbs с помощью

wscript cpuid.vbs

и проверьте соответствие выходов. (На самом деле я кросс-скомпилировал с MinGW-w64 в Linux и запустил программу под эмуляцией Wine64, выполняя C и сборку до этого момента.)

Теперь, когда работает функция CPUID сборки x64, я готов интегрировать код обратно в C#.

  • Разберите cpuid-x64.exe, чтобы получить коды операций, и вставьте их в виде нового байтового массива (code_x64).
  • Измените ExecuteCode(), чтобы определить, следует ли запускать версию CPUID для x86 или x64, проверяя IntPtr.Size == 8 в IsX64Process ().

Наконец, измените ProcessorId(), чтобы получить шестнадцатеричную строку с:

string.Format("{0}{1}", BitConverter.ToUInt32(sn, 4).ToString("X8"), BitConverter.ToUInt32(sn, 0).ToString("X8"));

Использование "X8" вместо просто "X" гарантирует, что UInt32 отформатирован как шестнадцатеричное 8-значное значение с нулевым заполнением. В противном случае вы не сможете определить, какие цифры пришли из EDX, а какие из EAX, когда вы объединяете их в одну строку.

И это все.

Код, который вы опубликовали, похоже, вызывает CPUID функция № 2 (задается EAX зарегистрироваться, после PUSH 2; POP EAX). В соответствии с инструкцией Intel, ссылка не предназначена для запроса серийного номера:

Когда CPUID выполняется с EAX, установленным в 2, процессор возвращает информацию о внутренних TLB процессора, кэше и оборудовании предварительной выборки в регистрах EAX, EBX, ECX и EDX.

Также обратите внимание, что эта функция недоступна на процессорах AMD, но, тем не менее, код должен выполняться без ошибок.

Другие вопросы по тегам