Inline Assembly Code to Get CPU ID

2020-07-18 10:31发布

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

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's working fine for me. But I still have some questions and problems related to it:

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

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

3) I'm not sure that the value I'm obtaining is the correct one. 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 I get is "BFEBFBFF000306A9". The result of the code snippet is "F0B2FF0CA0000". Why? Which one is correct?

2条回答
Melony?
2楼-- · 2020-07-18 10:59

The code you posted seems to invoke CPUID function #2 (given by the EAX register, after the PUSH 2; POP EAX). According to the intel instruction set reference that is not for querying the serial number:

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

Also note this function is not available on AMD processors, but the code should nevertheless execute without errors.

查看更多
我想做一个坏孩纸
3楼-- · 2020-07-18 11:08

Here's your code 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 modifications to the C# part without compiling the code (I don't have a Windows dev machine setup at the moment) so if there are syntax errors please just make the obvious fix.

I want to stress one very important point: what your original code was reading back was NOT a CPU serial number:

  • You used CPUID function 2 (by placing 2 in EAX before executing the CPUID instruction). If you read the Intel and AMD CPUID application notes you'll see that this reads back the cache and TLB hardware configuration and is only supported on Intel.
  • I modified your code to use CPUID function 1, which reads back the stepping, model, and family of a CPU. This matches the behavior of WIN32_Processor.ProcessorID
  • Modern x86 CPUs don't have a serial number that is unique among otherwise identical units "rolling off the assembly line". Processor serial numbers were only available on Pentium 3's through CPUID function 3.

I'll now explain the process and tools I used.

Paste the 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))

Disassemble 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 why use "push $0x2; pop %eax" to move the value 2 into EAX when a simple "mov $0x2, %eax" will do?

My guess is that the instruction encoding for "push $0x2", 6a02, is easier to modify in hexadecimal form. Both by hand and programmatically. I'd guess somebody somewhere tried to use CPUID function 3 to get the processor serial number and found that it wasn't supported then switched to using function 2.

The "ret $0x10" at the end is also unusual. The RET IMM16 form of the RET instruction returns to the caller then pops IMM16 bytes off the stack. The fact that the callee is responsible for popping arguments off the stack after function return implies that this is not using the standard x86 calling convention.

Indeed, a quick peek into the C# code reveals that it's using CallWindowProc() to invoke the assembly function. The documentation for CallWindowProc() shows that the assembly code is implementing a C function with a signature like:

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

__stdcall is the special function calling convention used by 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 instruction. (After a standard function prologue 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 window procedure function prototype above is wParam. It's used as an out parameter and is the only parameter used in the assembly code.

The last interesting thing about the assembly code is that it clobbers the registers EDI and EBX without saving them, violating the __stdcall calling convention. This bug is apparently latent when calling the function through CallWindowProc() but will reveal itself if you try to write your own main function in C to test the assembly 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;
}

A version of the assembly fixed to save and restore EDI, EBX and use CPUID function 1 is like this:

    .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 symbol name _cpuid_wind_proc@16 is how __stdcall function names are mangled on 32 bit Windows. The @16 is the number of bytes the parameters take up. (Four parameters each taking four bytes on 32 bit Windows adds up to 16)

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

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

Here's the x64 assembly:

    .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's only one function calling convention on x64 so we don't have to worry about __stdcall.

Build the x64 assembly 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 verify the outputs match. (I actually cross compiled with MinGW-w64 on Linux and ran the program under Wine64 emulation while doing the C and assembly work up till this point.)

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

  • Disassemble cpuid-x64.exe to get the opcodes and paste them as a new byte array (code_x64).
  • Change ExecuteCode() to determine whether to run the x86 or x64 version of the CPUID code by testing for IntPtr.Size == 8 in IsX64Process().

Finally, change ProcessorId() to produce the hexadecimal string with:

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

Using "X8" instead of just "X" ensures that the UInt32 is formatted as an 8 digit hexadecimal value with zero padding. Otherwise, you can't tell which digits came from EDX and which from EAX when you concatenate them into a single string.

And that's it.

查看更多
登录 后发表回答