【问题标题】:How to disassemble the main function of a stripped application?如何反汇编一个剥离的应用程序的主要功能?
【发布时间】:2011-07-25 10:48:57
【问题描述】:

假设我编译了下面的应用程序并剥离了它的符号。

#include <stdio.h>

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

构建过程:

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

如果应用程序没有被剥离,反汇编 main 函数会很容易。但是,我不知道如何反汇编剥离的应用程序的 main 函数。

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

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

我该怎么做?有没有可能?

注意:这必须仅使用 GDB 完成。忘记 objdump。假设我无权访问代码。

非常感谢您提供分步示例。

【问题讨论】:

  • 想象一下,我没有应用程序的源代码,也无法访问除 GDB 之外的其他工具。
  • 使用 IDA ;) ...抱歉,无法抗拒。我知道您只希望 GDB 使用它。
  • 谁对我的问题投了反对票,请解释原因。
  • @STATUS_ACCESS_DENIED 这个周末我标记了几十个答案,我怀疑有人不喜欢删除他们的答案。我在 3 分钟内对几乎所有问题和最近的答案都投了反对票。这就是为什么我问投票否决的原因。显然,此人决定撤消他们的投票。
  • 啊,好的。但是,当您被严重否决并重新计算您的代表时,启发式方法是否会出现?以为我在某个地方读到了一些关于它的东西。

标签: c linux gdb strip disassembly


【解决方案1】:

好的,这里是我之前回答的一个大版本。我想我现在找到了办法。

你(仍然:)有这个特定的问题:

(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。它是汇编代码开头的通用名称,从我们下面对main的解释中,从它的大小可以看出它包括main。如果您反汇编它,您将看到对 __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.

所以,除非你能在 main 开始的地方得到一些提示(比如使用带有符号的另一个代码),否则另一种方法是,如果你能得到一些关于第一个汇编指令的信息,那么你可以在特定的地方反汇编并查看它是否火柴。如果您根本无法访问代码,您仍然可以阅读ELF definition 以了解代码中应该出现多少段并尝试计算出的地址。不过,您需要有关代码中各部分的信息!

这很辛苦,我的朋友!祝你好运!

贝科

【讨论】:

  • 确实,如果我可以访问代码,那将相当容易。我所要做的就是编译一个带有符号的版本并加载该二进制文件,或者如果我真的必须调试剥离的版本,则告诉 gdb 从哪里加载符号。问题是我真的没有源代码,也没有我有兴趣调试的应用程序的非剥离版本。感谢您的努力。 +1
  • 您正在寻找的是一种计算起点的方法,给定一个 ELF。您需要深入了解 ELF 定义,并了解每个部分可以在偏移量中向下移动多少。但是您仍然需要知道部分的数量和大小。 info files 可以帮助你一点。如果我在接下来的几天里发现一些更新,我会在这里发表评论。祝你好运。
  • 另一个有用的信息是如何在不知道在哪里设置断点的情况下逐步尝试。您可以使用catch syscall write 或仅使用catch syscall,然后尝试运行。它并不总是有效,因为如果断点太早,就会缺少上下文。
  • 我刚刚用臭名昭著的命令gdb gdb、info files、disas 0x44f400,0x44f429、disas 0x44f4f0,0x44f500 对其进行了测试,它成功了。 ;)
  • @DrBeco,很好的答案!顺便问一下,你是怎么知道(gdb) disas 0x0000000000400524,0x0000000000400600声明中main的结尾在哪里?
【解决方案2】:

如何通过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 可能不会作为二进制文件中的“实体”存在;您可能无法比这种类型的程序更好。

【讨论】:

  • @karlphillip:就你所能得到的。反汇编的艺术是找出那些东西,即使你没有符号名称。所有平台上的文件结构都允许您查看从哪里开始,但接下来完全取决于您挖掘 CRT 代码并找到main()。例如,IDA 使用签名在很大程度上使用与 Mat 建议的手册类似的方法来自动执行此操作。
  • 如果二进制文件是动态链接的,您仍然可以使用 ltrace 查找 __libc_start_main(调用 main(),加上一些设置),这将使您接近。
【解决方案3】:

paradyn 项目中有一个名为 unstrip 的很棒的新免费工具(完全披露:我在这个项目上工作),它将重写您的程序二进制文件,向其中添加符号信息,并恢复所有(或几乎所有)函数为您剥离了精灵二进制文件,非常准确。它不会将 main 函数标识为“main”,但它会找到它,并且您可以应用上面已经提到的启发式方法来确定哪个函数是 main。

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

很抱歉,这不是仅 gdb 的解决方案。

【讨论】:

  • 嘿,我在 linux 内核二进制文件上尝试了 unstrip 命令。我使用了命令“unstrip -f vmlinux”。但是,它没有输出任何东西。由于 vmlinux 是一个特殊的二进制文件,是否应该为 unstrip 命令提供任何选项?这是正在考虑的二进制文件dl.dropboxusercontent.com/u/56211033/vmlinux
【解决方案4】:

IIRC,x/i &lt;location&gt; 是你的朋友。当然你得自己想清楚要拆到哪个位置。

【讨论】:

  • 我已经知道了,因为问题是先定位它,所以对我的反汇编根本没有帮助。
  • 所以你的问题是关于本地化 main。从二进制流中获取指令是次要的。我误解了这个问题。
猜你喜欢
  • 2016-10-16
  • 2010-09-08
  • 1970-01-01
  • 2022-12-12
  • 2017-04-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-01
相关资源
最近更新 更多