web-dev-qa-db-ja.com

ストリップされたアプリケーションの主な機能を分解する方法は?

以下のアプリケーションをコンパイルして、そのシンボルを取り除いたとしましょう。

#include <stdio.h>

int main()
{
    printf("Hello\n");
}

ビルド手順:

gcc -o hello hello.c
strip --strip-unneeded hello

アプリケーションが取り除かれていない場合、メイン機能を分解するのは簡単です。ただし、ストリップされたアプリケーションのmain関数を分解する方法はわかりません。

(gdb) disas main
No symbol table is loaded.  Use the "file" command.

(gdb) info line main
Function "main" not defined.

どうすればできますか?可能ですか?

Notes:これはGDBでのみ実行する必要があります。 objdumpを忘れます。コードにアクセスできないとします。

ステップバイステップの例をいただければ幸いです。

33
karlphillip

さて、ここに私の以前の答えの大きな版があります。私は今道を見つけたと思います。

あなた(まだ:)にはこの特定の問題があります:

(gdb) disas main
No symbol table is loaded.  Use the "file" command.

ここで、コードをコンパイルすると(最後にreturn 0を追加しました)、gcc -Sが得られます。

    pushq   %rbp
    movq    %rsp, %rbp
    movl    $.LC0, %edi
    call    puts
    movl    $0, %eax
    leave
    ret

これで、バイナリがいくつかの情報を提供していることがわかります。

ストライプ:

(gdb) info files
Symbols from "/home/beco/Documents/fontes/cpp/teste/stackoverflow/distrip".
Local exec file:
    `/home/beco/Documents/fontes/cpp/teste/stackoverflow/distrip', file type elf64-x86-64.
    Entry point: 0x400440
    0x0000000000400238 - 0x0000000000400254 is .interp
    ...
    0x00000000004003a8 - 0x00000000004003c0 is .rela.dyn
    0x00000000004003c0 - 0x00000000004003f0 is .rela.plt
    0x00000000004003f0 - 0x0000000000400408 is .init
    0x0000000000400408 - 0x0000000000400438 is .plt
    0x0000000000400440 - 0x0000000000400618 is .text
    ...
    0x0000000000601010 - 0x0000000000601020 is .data
    0x0000000000601020 - 0x0000000000601030 is .bss

ここで最も重要なエントリは.textです。これは、アセンブリのコード開始の一般的な名前です。メインの怒鳴る声の説明から、そのサイズから、メインが含まれていることがわかります。逆アセンブルすると、__ libc_start_mainが呼び出されます。最も重要なのは、実際のコードである適切なエントリポイントを逆アセンブルすることです(DATAをCODEに変更するように誤解させることはありません)。

disas 0x0000000000400440,0x0000000000400618
Dump of assembler code from 0x400440 to 0x400618:
   0x0000000000400440:  xor    %ebp,%ebp
   0x0000000000400442:  mov    %rdx,%r9
   0x0000000000400445:  pop    %rsi
   0x0000000000400446:  mov    %rsp,%rdx
   0x0000000000400449:  and    $0xfffffffffffffff0,%rsp
   0x000000000040044d:  Push   %rax
   0x000000000040044e:  Push   %rsp
   0x000000000040044f:  mov    $0x400540,%r8
   0x0000000000400456:  mov    $0x400550,%rcx
   0x000000000040045d:  mov    $0x400524,%rdi
   0x0000000000400464:  callq  0x400428 <__libc_start_main@plt>
   0x0000000000400469:  hlt
   ...

   0x000000000040046c:  sub    $0x8,%rsp
   ...
   0x0000000000400482:  retq   
   0x0000000000400483:  nop
   ...
   0x0000000000400490:  Push   %rbp
   ..
   0x00000000004004f2:  leaveq 
   0x00000000004004f3:  retq   
   0x00000000004004f4:  data32 data32 nopw %cs:0x0(%rax,%rax,1)
   ...
   0x000000000040051d:  leaveq 
   0x000000000040051e:  jmpq   *%rax
   ...
   0x0000000000400520:  leaveq 
   0x0000000000400521:  retq   
   0x0000000000400522:  nop
   0x0000000000400523:  nop
   0x0000000000400524:  Push   %rbp
   0x0000000000400525:  mov    %rsp,%rbp
   0x0000000000400528:  mov    $0x40062c,%edi
   0x000000000040052d:  callq  0x400418 <puts@plt>
   0x0000000000400532:  mov    $0x0,%eax
   0x0000000000400537:  leaveq 
   0x0000000000400538:  retq   

__ libc_start_main の呼び出しは、最初の引数としてmain()へのポインタを取得します。したがって、呼び出しの直前のスタックの最後の引数はmain()アドレスです。

   0x000000000040045d:  mov    $0x400524,%rdi
   0x0000000000400464:  callq  0x400428 <__libc_start_main@plt>

ここでは0x400524です(既に知っています)。ここでブレークポイントを設定して、これを試してください:

(gdb) break *0x400524
Breakpoint 1 at 0x400524
(gdb) run
Starting program: /home/beco/Documents/fontes/cpp/teste/stackoverflow/disassembly/d2 

Breakpoint 1, 0x0000000000400524 in main ()
(gdb) n
Single stepping until exit from function main, 
which has no line number information.
hello 1
__libc_start_main (main=<value optimized out>, argc=<value optimized out>, ubp_av=<value optimized out>, 
    init=<value optimized out>, fini=<value optimized out>, rtld_fini=<value optimized out>, 
    stack_end=0x7fffffffdc38) at libc-start.c:258
258 libc-start.c: No such file or directory.
    in libc-start.c
(gdb) n

Program exited normally.
(gdb) 

これで、次のコマンドを使用して分解できます。

(gdb) disas 0x0000000000400524,0x0000000000400600
Dump of assembler code from 0x400524 to 0x400600:
   0x0000000000400524:  Push   %rbp
   0x0000000000400525:  mov    %rsp,%rbp
   0x0000000000400528:  sub    $0x10,%rsp
   0x000000000040052c:  movl   $0x1,-0x4(%rbp)
   0x0000000000400533:  mov    $0x40064c,%eax
   0x0000000000400538:  mov    -0x4(%rbp),%edx
   0x000000000040053b:  mov    %edx,%esi
   0x000000000040053d:  mov    %rax,%rdi
   0x0000000000400540:  mov    $0x0,%eax
   0x0000000000400545:  callq  0x400418 <printf@plt>
   0x000000000040054a:  mov    $0x0,%eax
   0x000000000040054f:  leaveq 
   0x0000000000400550:  retq   
   0x0000000000400551:  nop
   0x0000000000400552:  nop
   0x0000000000400553:  nop
   0x0000000000400554:  nop
   0x0000000000400555:  nop
   ...

これは主に解決策です。

ところで、これは動作するかどうかを確認するための別のコードです。上記のアセンブリが少し違うのはそのためです。上記のコードは、このcファイルからのものです。

#include <stdio.h>

int main(void)
{
    int i=1;
    printf("hello %d\n", i);
    return 0;
}

だが!


これが機能しない場合でも、いくつかのヒントがあります。

今後は、すべての関数の最初にブレークポイントを設定する必要があります。 retまたはleaveの直前にあります。最初のエントリポイントは.text自体です。これは組立開始ですが、メインではありません。

問題は、常にブレークポイントがプログラムを実行できるとは限らないことです。まさに.textの中でこのように:

(gdb) break *0x0000000000400440
Breakpoint 2 at 0x400440
(gdb) run
Starting program: /home/beco/Documents/fontes/cpp/teste/stackoverflow/disassembly/d2 

Breakpoint 2, 0x0000000000400440 in _start ()
(gdb) n
Single stepping until exit from function _start, 
which has no line number information.
0x0000000000400428 in __libc_start_main@plt ()
(gdb) n
Single stepping until exit from function __libc_start_main@plt, 
which has no line number information.
0x0000000000400408 in ?? ()
(gdb) n
Cannot find bounds of current function

したがって、次の場所にブレークポイントを設定して、方法が見つかるまで試行を続ける必要があります。

0x400440
0x40046c
0x400490
0x4004f4
0x40051e
0x400524

他の答えから、この情報を保持する必要があります:

ストライプ化されていないバージョンのファイルでは、次のようになります。

(gdb) disas main
Dump of assembler code for function main:
   0x0000000000400524 <+0>: Push   %rbp
   0x0000000000400525 <+1>: mov    %rsp,%rbp
   0x0000000000400528 <+4>: mov    $0x40062c,%edi
   0x000000000040052d <+9>: callq  0x400418 <puts@plt>
   0x0000000000400532 <+14>:    mov    $0x0,%eax
   0x0000000000400537 <+19>:    leaveq 
   0x0000000000400538 <+20>:    retq   
End of assembler dump.

これで、mainが0x0000000000400524,0x0000000000400539にあることがわかりました。同じオフセットを使用してストライプバイナリを確認すると、同じ結果が得られます。

(gdb) disas 0x0000000000400524,0x0000000000400539
Dump of assembler code from 0x400524 to 0x400539:
   0x0000000000400524:  Push   %rbp
   0x0000000000400525:  mov    %rsp,%rbp
   0x0000000000400528:  mov    $0x40062c,%edi
   0x000000000040052d:  callq  0x400418 <puts@plt>
   0x0000000000400532:  mov    $0x0,%eax
   0x0000000000400537:  leaveq 
   0x0000000000400538:  retq   
End of assembler dump.

したがって、メインが開始するヒント(シンボルを含む別のコードを使用するなど)が得られない限り、別の方法は、最初のアセンブリ命令に関する情報を入手でき、特定の場所で分解して、一致するかどうかを確認できることです。コードにまったくアクセスできない場合でも、 ELF定義 を読んで、コードに表示されるセクションの数を理解し、計算されたアドレスを試すことができます。それでも、コード内のセクションに関する情報が必要です!

それは大変な仕事です、私の友人!幸運を!

ベコ

42
Dr Beco

info files(アドレス付きの)セクションリストを取得し、そこから移動しますか?

例:

gdb) info files

Symbols from "/home/bob/tmp/t".
Local exec file:
`/home/bob/tmp/t', file type elf64-x86-64.
Entry point: 0x400490
0x0000000000400270 - 0x000000000040028c is .interp
0x000000000040028c - 0x00000000004002ac is .note.ABI-tag
    ....

0x0000000000400448 - 0x0000000000400460 is .init
    ....

逆アセンブル.init

(gdb) disas 0x0000000000400448,0x0000000000400460
Dump of assembler code from 0x400448 to 0x400460:
   0x0000000000400448:  sub    $0x8,%rsp
   0x000000000040044c:  callq  0x4004bc
   0x0000000000400451:  callq  0x400550
   0x0000000000400456:  callq  0x400650
   0x000000000040045b:  add    $0x8,%rsp
   0x000000000040045f:  retq   

次に、残りを分解します。

私があなたであり、実行可能ファイルのビルドに使用したのと同じGCCバージョンを持っている場合、ダミーのストリップされていない実行可能ファイルで呼び出された一連の関数を調べます。呼び出しのシーケンスは、ほとんどの通常の場合でおそらく同じであるため、mainまでの起動シーケンスを比較するのに役立ちます。最適化はおそらく邪魔になるでしょう。

バイナリが削除されて最適化されている場合、mainはバイナリの「エンティティ」として存在しない可能性があります。おそらく、このタイプの手順よりもはるかに良くなることはできません。

8
Mat

Undtripと呼ばれるparadynプロジェクト(完全な開示:私はこのプロジェクトに取り組んでいます)と呼ばれる素晴らしい無料のツールがあり、プログラムバイナリを書き換えてシンボル情報を追加し、ストリップされたElfバイナリのすべて(またはほぼすべて)の関数を回復しますあなたのために、非常に正確に。それはメイン関数を「メイン」として識別しませんが、それを見つけます。そして、あなたがすでに述べたヒューリスティックを適用して、どの関数がメインであるかを理解することができます。

http://www.paradyn.org/html/tools/unstrip.html

これはgdbのみのソリューションではありません。

1
Kevin