Встроенный код сборки для получения идентификатора процессора
Я нашел хороший фрагмент кода, который выполняет инструкции 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, но, тем не менее, код должен выполняться без ошибок.