3

ここで、CPU のシリアル番号を取得するために API 呼び出しを使用して ASM 命令を実行する素晴らしいコードを見つけました。

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 など) を含むバイトコード配列を実装することで、これを簡単に実現できると述べています。私の問題は、86x バイトコードを 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」です。なんで?どちらが正しいですか?

4

2 に答える 2

23

x64x86の両方でWin32_Processor.ProcessorIdと同じ結果が得られるようにコードを変更すると、次のようになります。

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 開発マシンをセットアップしていません)。構文エラーがある場合は、明らかな修正を行ってください。

非常に重要な点を 1 つ強調したいと思います。元のコードが読み取っていたのは、CPU のシリアル番号ではありませんでした。

  • CPUID 関数 2 を使用しました ( CPUID命令を実行する前にEAXに 2 を配置することにより)。IntelAMDの CPUID アプリケーション ノートを読むと、これがキャッシュと TLB ハードウェア構成を読み戻しており、Intel でのみサポートされていることがわかります。
  • CPUID 関数 1 を使用するようにコードを変更しました。これは、CPU のステッピング、モデル、およびファミリを読み取ります。これは、WIN32_Processor.ProcessorIDの動作と一致します。
  • 最新の x86 CPU には、「組み立てラインから出荷された」同一のユニット間で一意のシリアル番号がありません。プロセッサーのシリアル番号は、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 を逆アセンブルします。udis86のudcliを使用しました。

$ 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 

すぐに目立つことの 1 つは、単純な「mov $0x2, %eax 」で十分な場合に、「 push $0x2; pop %eax」を使用して値 2 をEAXに移動する理由です。

私の推測では、「push $0x2」、6a02の命令エンコーディングは、16 進形式で変更する方が簡単です。手動でもプログラムでも。どこかで誰かが CPUID 関数 3 を使用してプロセッサのシリアル番号を取得しようとしたところ、サポートされていないことがわかり、関数 2 を使用することに切り替えたと思います。

最後の「ret $0x10」も珍しい。RET命令のRET IMM16形式は、呼び出し元に戻り、スタックからIMM16バイトをポップします。呼び出し先が関数の戻り後にスタックから引数をポップする責任があるという事実は、これが標準の x86 呼び出し規約を使用していないことを意味します。

実際、C# コードをざっと見てみると、 CallWindowProc()を使用してアセンブリ関数を呼び出していることがわかります。CallWindowProc()のドキュメントは、アセンブリ コードが次のようなシグネチャを持つ C 関数を実装していることを示しています。

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

__stdcallは、32 ビット Windows API で使用される特別な関数呼び出し規約です。

アセンブリ コードは、関数の 3 番目の引数である0x10(%ebp)を文字配列として使用し、 CPUID命令からの出力を格納します。(x86 の標準関数プロローグの後、8(%ebp)が最初の引数です。0xc(%ebp)は 2 番目の 4 バイト引数で、0x10(%ebp)は 3 番目の引数です) ウィンドウ プロシージャ関数プロトタイプの 3 番目のパラメーター上記はwParamです。これは出力パラメーターとして使用され、アセンブリ コードで使用される唯一のパラメーターです。

アセンブリ コードの最後の興味深い点は、レジスタEDIEBXを保存せずに上書きし、 __stdcall呼び出し規則に違反していることです。このバグは、 CallWindowProc()を介して関数を呼び出した場合に明らかに潜在的ですが、アセンブリ コード ( cpuid-main.c )をテストするために 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;
}

EDIEBXを保存および復元し、 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は、32 ビット Windows で__stdcall関数名がマングルされる方法です。@16は、パラメーターが占めるバイト数です。(32 ビット Windows でそれぞれ 4 バイトを使用する 4 つのパラメーターは合計 16 になります)

これで、コードを x64 に移植する準備が整いました。

  • この便利な ABI テーブルを調べると、最初の 4 つのパラメーターがRCXRDXR8、およびR9で渡されるため、wParamR8にあることがわかります。
  • Intel のドキュメントによると、CPUID命令はEAXEBXECX、およびEDXを破壊します。EBXは、ABI に保存された GPRであるRBXの下半分です(ここでの「保存された GPR」は、関数呼び出し全体でその内容を保持する必要がある汎用レジスタを意味します)。そのため、 CPUID命令を実行して復元する前にRBXを保存するようにしました。続いて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 には関数呼び出し規約が 1 つしかないため、 __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

出力が一致することを確認します。(私は実際に Linux で MinGW-w64 を使用してクロス コンパイルし、Wine64 エミュレーションでプログラムを実行し、この時点まで C とアセンブリの作業を行いました。)

x64 アセンブリの CPUID 関数が機能するようになったので、コードを C# に統合する準備が整いました。

  • cpuid-x64.exeを逆アセンブルしてオペコードを取得し、それらを新しいバイト配列 ( code_x64 ) として貼り付けます。
  • ExecuteCode()を変更して、 IsX64Process()IntPtr.Size == 8をテストすることにより、CPUID コードの x86 バージョンと x64 バージョンのどちらを実行するかを決定します。

最後に、ProcessorId()を変更して、次の 16 進数文字列を生成します。

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

"X"だけでなく"X8"を使用すると、UInt32 がゼロ パディング付きの 8 桁の 16 進数値としてフォーマットされます。そうしないと、それらを 1 つの文字列に連結したときに、どの数字がEDXからのもので、どれがEAXからのものかわかりません。

以上です。

于 2013-05-10T17:12:36.273 に答える