Embedded assembly code to get the CPU ID

I found a nice piece of code here that executes ASM instructions using API calls to get the CPU serial number:

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); } } } 

I tested it and it works great for me. But I still have some questions and issues related to this:

1) I would like to implement this code inside an application that can work both in x86 and x64 environments. If I run this code in a 64x environment, I get an AccessViolationException. The author of the code said that this can be easily achieved by also implementing bytecode containing x64 instructions (RAX, RBX, RCX, RDX, ...). My problem is that I absolutely don't know how to convert 86 byte code to x64 byte code, I don't even know ASM. Is there any conversion table or utility that can do this?

2) Is this piece of code valid for any type of processor? I tested it on my laptop that uses the Intel core, and it works ... but what about AMD, for example?

3) I am not sure that the value I get is correct. If I run the following code:

 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(); } 

The result is "BFEBFBFF000306A9". The result of the code snippet is "F0B2FF0CA0000". What for? Which one is correct?

+3
source share
2 answers

Here, your code has been modified to get the same result as Win32_Processor.ProcessorId on both x64 and 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; } } } 

I made trivial changes for the C # part without compiling the code (I don't have Windows dev installed at the moment), so if there are syntax errors, just make the obvious fix.

I want to emphasize one very important point: what your source code read was not the processor serial number :

  • You used the CPUID 2 function (by placing 2 in EAX before executing the CPUID command ). If you read Intel and AMD CPUID in the application notes, you will see that it reads the TLB cache and hardware configuration and is only supported on Intel.
  • I modified your code to use the CPUID 1 function, which reads the step, model and CPU family. This corresponds to the behavior of Win32_Processor.ProcessorId
  • Modern x86 processors do not have a serial number that is unique among other identical units that roll the conveyor. The processor serial numbers were only available for Pentium 3 through the CPUID 3 feature.

Now I will explain the process and tools that I used.

Insert an array of opcodes into a Python script that will then write the opcodes in a binary file ( cpuid-x86.bin ):

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

Take apart cpuid-x86.bin . I used udcli from 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 

One thing that immediately stands out is the use of push $ 0x2; pop% eax "to move value 2 to EAX when simple" mov $ 0x2,% eax "?

My assumption is that the command encoding for push $ 0x2 ", 6a02 , is easier to change in hexadecimal. Both manually and programmatically. I would suggest that someone tried to use the CPUID 3 function somewhere to get the processor serial number and found that it is not supported, and then switches to using function 2.

" ret $ 0x10 " at the end is also unusual. The RET form IMM16 of the RET command returns to the caller and then removes the IMM16 stack from the line. The fact that the caller is responsible for expressing arguments from the stack after the function returns implies that this does not use the standard x86 calling convention.

In fact, a quick look at C # code shows that it uses CallWindowProc () to call the build function. The documentation for CallWindowProc () shows that the assembler code implements a C function with a signature of the type:

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

__ stdcall is a special function convention used by the 32-bit Windows APIs.

The assembly code uses 0x10 (% ebp) , which is the third argument to the function, as a character array to store the output from the CPUID command . (After the prologue of the standard function on x86, 8 (% ebp) is the first argument. 0xc (% ebp) is the second 4-byte argument and 0x10 (% ebp) is the third). The third parameter in our prototype window procedure function above: wParam . It is used as the out parameter and is the only parameter used in the assembly code.

The last interesting thing for assembler code is that it compresses the EDI and EBX registers without saving them, violating the __ stdcall calling convention . This error is apparently hidden when the function is called via CallWindowProc () , but it will be detected if you try to write your own main function in C to test the build code ( 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; } 

The build version corrected to save and restore EDI , EBX and use the CPUID function is as follows:

  .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 

The _cpuid_wind_proc @ 16 character name is how the __ stdcall names are garbled on 32-bit Windows. @ 16 is the number of bytes that take parameters. (Four parameters, each of which takes four bytes on 32-bit Windows, adds up to 16)

Now I'm ready to port the code to x64.

  • When consulting this handy ABI table, I can see that the first four parameters are passed to RCX , RDX , R8 and R9 , so wParam is in R8 .
  • Intel documentation tells me that the CPUID team clobbers EAX , EBX , ECX and EDX . EBX is the lower half of RBX , which is the saved GPR in the ABI ("saved GPR" here means the general register that its contents should be saved through a function call), so I had to save RBX before executing the CPUID instruction and restore the RBX .

Here is the x64 build:

  .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 

As you can see, the x64 version is shorter and easier to write. There is only one x64 convention invocation function, so we don’t need to worry about __ stdcall .

Create an x64 build function along with cpuid-main.c and compare its output with this VBScript ( cpuid.vbs ):

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

Run cpuid.vbs with

 wscript cpuid.vbs 

and check for matching outputs. (I actually cross-compiled with MinGW-w64 on Linux and ran the program under Wine64 emulation while running C and building up to this point.)

With the CPUID x64 feature working, I'm now ready to integrate the code back into C #.

  • Parse cpuid-x64.exe to get the operation codes and insert them as a new byte array ( code_x64 ).
  • Modify ExecuteCode () to determine whether CPUID x86 or x64 should be run by testing for IntPtr.Size == 8 in IsX64Process () .

Finally, modify ProcessorId () to create a hexadecimal string with:

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

Using "X8" instead of "X" ensures that UInt32 is formatted as a hexadecimal value with zero padding. Otherwise, you cannot determine which digits came from EDX and which ones from EAX when you combine them into one line.

What is it.

+20
source

The code you sent calls the CPUID function # 2 (set by EAX after PUSH 2; POP EAX ). According to Intel instruction set instruction, which is not intended to request a serial number:

When the CPUID runs with EAX set to 2, the processor returns information about the processor’s internal TLB, cache, and prefetch hardware in the EAX, EBX, ECX, and EDX registers.

Also note that this feature is not available for AMD processors, but the code should nevertheless execute without errors.

0
source

Source: https://habr.com/ru/post/980904/


All Articles