Win32::APIでPerlの中に直接機械語を書いてるときのデバッグB!

はせがわようすけです。こんにちは。今日みたいな日だとリア充揃いのPerl geeksは人手不足なようで、ほとんどPerlを使ったことのない私まで駆り出されましたよ。

まえおき

というわけで、Perlで機械語を埋め込む技を応用すると、ActivePerl で stdcall な通常のDLL関数だけでなく、MSVCRT.DLL に含まれる sprintf のような可変長引数の cdecl な呼び出し規約の関数も利用できます。

#!/c/perl/bin/perl
use strict;
use warnings;
use Win32::API;

#include 
my $EnumWindows = Win32::API->new( "user32", "EnumWindows", "NN", "N" );
my $GetProcAddress = Win32::API->new( "kernel32", "GetProcAddress", "NP", "N" );
my $LoadLibrary = Win32::API->new( "kernel32", "LoadLibraryA", "P", "N" );
my $FreeLibrary = Win32::API->new( "kernel32", "FreeLibrary", "N" );

sub my_sprintf{
    if( @_ < 1 ){
        die "argument error";
    }

    my $hDll = $LoadLibrary->Call( "msvcrt" );
    my $sprintf = pack( 'L', $GetProcAddress->Call( $hDll, "sprintf" ) );   # sprintf is cdecl
    my $buf = "\0" x 1024;
    my $x86 ="";
    my $i = @_;

    while( $i-- ){
        $x86 .= "\x68" . $_[ $i ];      # push args
    }
    $x86 .= "\x68" . pack( 'P', $buf ); # push $buf
    my $n = ( @_ + 1 ) * 4;
    $x86 .= ""
.       "\xb8" . $sprintf               # mov eax, func
.       "\xff\xd0"                      # call eax
.       "\x81\xC4"                      # add esp, @_ * 4
.       pack( 'L', $n )
.       "\x33\xc0"                      # xor eax, eax
.       "\xc2\x08\x00"                  # ret
;

    $EnumWindows->Call( unpack( 'L', pack( 'P', $x86 ) ), 0 );
    $FreeLibrary->Call( $hDll );
    $buf =~s/\0.*$//;
    return $buf;
}

my $s = my_sprintf( pack( 'P', "%s, %s" ), pack( 'P', "Hello" ), pack( 'P', "World" ) );
print $s;

実行結果

C:\>xmax.pl
Hello, World

このように、用意しておいたバイト配列をEnumWindows APIのコールバック関数として渡してやることで、任意の機械語を簡単に実行させることができます。

ただ、熟練したバイナリアンでなければ機械語を一発で動かすことは難しく、たいていの場合は途中でプログラムが強制終了させられてしまいます。

そこで、もう少し効率的にデバッグする方法を紹介します。

printf デバッグ

まず、もっとも簡単な方法として、みんな大好きな printf デバッグで機械語を確認することにしましょう。

    $x86 .= ""
.       "\xb8" . $sprintf               # mov eax, func
.       "\xff\xd0"                      # call eax
.       "\x81\xC4"                      # add esp, @_ * 4
.       pack( 'L', $n )
.       "\x33\xc0"                      # xor eax, eax
.       "\xc2\x08\x00"                  # ret
;
    print unpack( 'H2' x length( $x86 ), $x86 );
    print "\n";

実行結果

C:\>xmax.pl
68d4764d0268b4764d026894764d02685c8b4c02b831bd9676ffd081c41000000033c0c20800
Hello,World

ごらんのとおり、実行される機械語が画面に表示されますので、どこで機械語の指定を間違えたのか一目瞭然になります。

超画期的です。

デバッガでアタッチ

printfによる目視デバッグでもあまり困らないのですが、あまりやりすぎると

よしおかさんに怒られちゃうので、きちんとデバッガを使うようにしましょう。使うデバッガ環境はもちろん Visual Studio です。

Visual Studioは、「C:\Windows\System32\vsjitdebugger.exe -p プロセスID 」とすることで、任意のプロセスをデバッガにアタッチすることができますので、Perlのなかから自身のプロセスIDを指定してこれを呼び出すことで、うまくデバッガにアタッチできそうです。

指定してこれを呼び出すことで、うまくデバッガにアタッチできそうです。

ただし、Perlのsystem関数を使ってそのままvsjitdebugger.exeを呼び出したのでは、デバッガ終了までperl側の処理がブロックされてしまい、デバッガが立ち上がっても継続してデバッグすることができません。

そこで、start コマンド経由で vsjitdebugger.exe を呼び出すことにします。

system("start", "vsjitdebugger.exe", "-p", "$$" );

こうすることで、startコマンドは vsjitdebugger.exe を起動すると速やかに終了し、Perl側では system は制御を戻すので、デバッガと並行してコードの実行を進めることができます。

ただし、このままではデバッガが起動しデバッグ対象プロセス(ActivePerl)にアタッチしてデバッグの準備ができるのを待つことなく、Perl側はどんどんコードの実行を進めてしまいますので、今度はデバッガの準備ができるまでPerl側のコードの実行を停止させる必要があります。

これには、Windows APIのIsDebuggerPresent関数を使います。

while( $IsDebuggerPresent->Call() == 0 ){
    sleep( 1 );
};

これで、デバッガがきちんとアタッチしていない間は有意なコードの実行を停止させることができます。

つぎに、x86バイナリコードにブレークポイントを置くわけですが、これは単純に int 3 を実行することでデバッガにブレークを通知できます。

my $x86 = "\xCC.....";

CPUがこの0xCCという機械語を実行すると、デバッガ側にはブレークポイントとして通知され、以降のコードを自由にデバッガ上で動かすことができます。

というわけで、さきの Hello, World の機械語部分をデバッガ上で実行するよう書き換えたコードを以下に示します。

#!/c/perl/bin/perl
use strict;
use warnings;
use Win32::API;

#include 
my $EnumWindows = Win32::API->new( "user32", "EnumWindows", "NN", "N" );
my $GetProcAddress = Win32::API->new( "kernel32", "GetProcAddress", "NP", "N" );
my $LoadLibrary = Win32::API->new( "kernel32", "LoadLibraryA", "P", "N" );
my $FreeLibrary = Win32::API->new( "kernel32", "FreeLibrary", "N" );
my $IsDebuggerPresent = Win32::API->new( "kernel32", "IsDebuggerPresent", "", "N" );

sub my_sprintf{
    if( @_ < 1 ){
        die "argument error";
    }

    my $hDll = $LoadLibrary->Call( "msvcrt" );
    my $sprintf = pack( 'L', $GetProcAddress->Call( $hDll, "sprintf" ) );   # sprintf is cdecl
    my $buf = "\0" x 1024;
    my $x86 ="";
    my $i = @_;

    $x86 = "\xCC";                      # int 3

    while( $i-- ){
        $x86 .= "\x68" . $_[ $i ];      # push args
    }
    $x86 .= "\x68" . pack( 'P', $buf ); # push $buf
    my $n = ( @_ + 1 ) * 4;
    $x86 .= ""
.       "\xb8" . $sprintf               # mov eax, func
.       "\xff\xd0"                      # call eax
.       "\x81\xC4"                      # add esp, @_ * 4
.       pack( 'L', $n )
.       "\x33\xc0"                      # xor eax, eax
.       "\xc2\x08\x00"                  # ret
;

    $EnumWindows->Call( unpack( 'L', pack( 'P', $x86 ) ), 0 );
    $FreeLibrary->Call( $hDll );
    $buf =~s/\0.*$//;
    return $buf;
}

system("start", "vsjitdebugger.exe", "-p", "$$" );
while( $IsDebuggerPresent->Call() == 0 ){
    sleep( 1 );
};

my $s = my_sprintf( pack( 'P', "%s,%s" ), pack( 'P', "Hello" ), pack( 'P', "World" ) );
print $s;

まとめ

  • Perlに機械語埋め込むときでもデバッガ使ったほうが怒られないで済むよ!
  • WindowsならVisual Studio最強ですよ!
  • int 3は0xCC。バッドノウハウ万歳!

明日の最後のエントリーは・・・Yappoさんが締めくくってくれる予定?どんなネタか今から楽しみです!