如何拆解一个裁剪过的应用程序的主函数?

38

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

#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.

我该如何做呢?这样做可能吗?

注意:必须只使用 GDB 完成,不能使用 objdump。假设我没有代码访问权限。

提供一个逐步示例将会非常感激。


想象一下,我没有应用程序的源代码,也没有除GDB之外的其他工具访问权限。 - karlphillip
2
使用IDA吧 ;) ...抱歉,我忍不住了。我知道你只想用GDB。 - 0xC0000022L
谁给我的问题点了踩,请解释一下为什么。 - karlphillip
@STATUS_ACCESS_DENIED 我在周末标记了几十个答案,我怀疑有人不喜欢他们的答案被删除。我几乎所有的问题和最近的回答都在3分钟内被投反对票。这就是为什么我问投反对票的原因。显然,那个人决定取消他们的投票。 - karlphillip
啊,好的。但是当你被严重负评并重新计算声望时,启发式算法不会起作用吗?我记得在某个地方读到过。 - 0xC0000022L
这不是一次大规模的恶意踩贴攻击,只是在3分钟内出现了7或8个踩贴。 - karlphillip
4个回答

52

好的,这是我之前回答的大幅修改版。我认为我现在找到了一种方法。

你(仍然:)面临这个具体问题:

(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

现在,你可以看到你的二进制文件给出了一些信息:

Striped:

(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;
}

但是!


如果这个方法不起作用,那么你仍然有一些提示:

从现在开始,您应该尝试在所有函数的开头设置断点。它们位于 retleave 的前面。第一个入口点是.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定义以了解代码中应该出现多少个段,并尝试计算地址。不过,你还需要有关代码段的信息!

这是一项艰苦的工作,朋友!祝你好运!

Beco


1
你需要的是一种计算给定ELF文件起始点的方法。你需要深入了解ELF定义,并理解每个部分可以在偏移量上将主要部分向下移动多少。但是你仍然需要知道节的数量和大小。info files可以帮助你一些。如果我在接下来的几天中发现更新,我会在这里评论。祝你好运。 - DrBeco
另一个有用的信息是如何在不知道在哪里设置断点的情况下逐步尝试。您可以使用 catch syscall write 或只是 catch syscall,然后尝试运行。这并不总是有效,因为如果断点太早,则缺乏上下文。 - DrBeco
@karlphillip:只是为了确保,我测试了一个非C程序。我编译了一个FORTRAN测试,并成功地找到并反汇编了_main_。顺便说一句,我们是同胞。 ;) - DrBeco
1
@DrBeco,非常好的回答!顺便问一下,在语句(gdb) disas 0x0000000000400524,0x0000000000400600中,你是怎么知道main函数的结尾在哪里的? - robert
@robert 我也很困惑。 - Ebrahim Ghasemi
显示剩余7条评论

9

您可以尝试使用 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可能不会作为二进制文件中的“实体”存在;您的流程可能无法得到更好的改善。


1
@karlphillip:这大概是你能达到的最远程度了。反汇编的艺术就在于即使没有符号名称,也能找出这些东西。文件结构将在所有平台上允许您看到从哪里开始,但随后完全由您来挖掘 CRT 代码并找到 main()。例如,IDA 使用签名来自动化这一过程,使用类似于 Mat 建议的手动方法。 - 0xC0000022L
1
如果二进制文件是动态链接的,你仍然可以使用ltrace来查找__libc_start_main(调用main()函数以及一些设置),这将让你接近答案。 - Moudis

1

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

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

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

嘿,我在Linux内核二进制文件上尝试了unstrip命令。我使用了命令“unstrip -f vmlinux”。然而,它没有输出任何内容。由于vmlinux是一个特殊的二进制文件,是否应该为unstrip命令提供任何选项?这是要考虑的二进制文件 https://dl.dropboxusercontent.com/u/56211033/vmlinux - prathmesh.kallurkar

0

据我所知,x/i <location>是你的好朋友。当然,你必须自己确定要反汇编哪个位置。


我已经知道了,但是这并没有帮助我去反汇编主函数,因为问题在于首先定位它。 - karlphillip
所以你的问题是关于本地化主函数的。将二进制流中的指令提取出来是其次要的。我误解了这个问题。 - Laurent G

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接