【问题标题】:Ubuntu 16.04 assembly code for shell用于 shell 的 Ubuntu 16.04 汇编代码
【发布时间】:2017-02-12 09:09:14
【问题描述】:
.global main
main:
    call func
    .string "/bin/sh"
func:
    push %rsp
    pop %rsi
    pop %rdi
    mov $0x00, %edx
    mov $0x3b, %eax
    syscall

我为执行 /bin/sh 编写了像上面这样的程序集 lagunage 我编译了它,但是当我尝试执行程序时,/bin/sh: 0: Can't open ???? 发生了这个错误。 它不执行 /bin/sh。我想知道为什么我不能执行/bin/sh

我使用的是 Ubuntu 16.04 和 x64 架构

【问题讨论】:

  • 另一种方法是使用调用外部程序的system 函数。查看this 以查看汇编程序示例
  • 但我听说system 函数在它的内部函数调用中也使用了execve。所以我想用execve
  • 没错,system 会调用execve,但它比你想象的要复杂一些。拨打system 更容易。看看这个post for execve
  • 您是否出于利用原因而尝试编写 shell 代码?我要求验证这种代码的基本原理。
  • 是的。我只是想研究如何制作shell代码。互联网上的大多数参考都是执行/bin/sh 所以我试了一下。

标签: linux assembly x86-64 system-calls shellcode


【解决方案1】:

由于以一种奇怪的方式使用 push/pop,您的代码不必要地难以遵循,但是在 strace -f ./a.out 下运行您的程序以跟踪系统调用显示:

... dynamic linker and libc init stuff before main() is called ...
execve("/bin/sh", ["/bin/sh", "\211\307\350\t\222\1", "\367", "\367", 0x100000000, "\350\10"], [/* 0 vars */]) = -1 EFAULT (Bad address)
exit_group(0)                           = ?
+++ exited with 0 +++

所以在我的系统上,execve 返回错误,但程序成功退出。 IDK 你如何得到/bin/sh: 0: Can't open ????。您的问题没有包含足够的信息来重现您的结果。但也许当你尝试时,堆栈碰巧包含不同的垃圾。我用gcc -g foo.S 构建了它。

main 未能返回后,执行落入main 之后的任何 CRT 函数,该函数以 RET 指令结束。它也必须为零 eax,因为在 SYSCALL 之后它将是 -EFAULT


不管怎样,你的 asm 就相当于这个无用的代码:

int main(void) {
    const char *p = "/bin/sh";
    execve(p, &p, NULL);
}

注意push %rsp; pop %rsi 等同于mov %rsp, %rsi。所以 RSI 持有指向 CALL 写入“返回地址”的堆栈内存的指针。

在此之后的另一个 POP 取消引用堆栈指针并将指向字符串的指针加载到 RDI 中。

使用 CALL 来推送字符串的地址真的很讨厌。 IDK 你为什么要这样做。像普通人一样使用 MOV。


如何正确地做到这一点

# build with gcc -no-pie -g shell.S

#include <asm/unistd.h>         // for __NR_execve                                                                                                                
// #include <sys/syscall.h>     // or include this glibc header for SYS_execve

main:          # main(argc, argv, envp)
    mov   $shell, %edi     # filename = shell
                           # argv     = main's argv, already in rsi (not on the stack like in _start)

    # leave RDX = envp
    # xor   %edx, %edx       # envp     = NULL

    mov   $__NR_execve, %eax    # execve
    syscall                     # execve(shell, argv, envp)
    ret                    # in case the syscall fails

.section .rodata
shell:
.string "/bin/sh"

这行得通,不会做任何奇怪的事情。


或者,以一种与位置无关的平面二进制文件的方式工作,并且不使用 main() 的 argv,因为您添加了该请求:

#include <asm/unistd.h>         // for __NR_execve                                                                                                                
// #include <sys/syscall.h>     // or include this glibc header for SYS_execve

.globl main
main:
    lea   shell(%rip), %rdi     # filename = shell
    xor   %edx, %edx            # envp     = NULL

    push  %rdx                  # or push $0
    push  %rdi
    mov   %rsp, %rsi            # argv     = { $shell, NULL } that we just pushed

    mov   $__NR_execve, %eax    # execve(
    syscall
    ret                    # in case the syscall fails
shell:                     # still part of the .text section, and we don't use the absolute address of the label, only for a RIP-relative LEA.
.string "/bin/sh"

您的 RSI=RSP 想法不错,但是您忘记在 argv[] 数组的末尾添加一个终止 NULL 指针。

main 之外,将envp[] 作为参数,我们没有已经构造的envp[] 可以在任何方便的地方访问,所以只需传递NULL。在 Linux 上,这相当于将一个有效指针传递给一个空的以 NULL 结尾的数组,即一个指向内存中 8 字节 0 的指针。

【讨论】:

  • 我相信他正在编写 shell 代码,因此他不想像您的示例那样使用固定部分和绝对地址。他的字符串被放置在代码中,以便他可以通过将返回地址弹出到 RDI 中来检索字符串的地址。他的方法是 JMP/CALL/POP 方法的一种变体。这个想法是从可执行堆栈中运行此代码。
  • @MichaelPetch:我想知道这一点,但 OP 从未真正说过。将字符串放在 main 之后或之前(在 .text 部分中)将生成一个带有 RIP 相关 LEA 的 PIC 代码块:lea $shell(%rip), %rdi 而不是 mov $shell, %edi。原文显然并没有试图避免 \0 字节或任何东西,所以我的回答旨在教授使用 strace 进行基本调试。
  • 实际上,当我看到问题的标题时,我首先认为这可能是一个 shell 代码问题。当我看到代码中的字符串以及他将返回地址作为参数弹出的方式时,它几乎尖叫“shell 代码”。至于 RIP 寻址,当然可以,但您的原始代码没有这样做。
  • 因此,我在 20 分钟前在问题下的评论中要求 OP 进行澄清。
  • @user4929293:显然使用 CALL 推送字符串地址并跳过它是漏洞利用编码中的常用技术。在 32 位代码中,没有 RIP 相对寻址,因此您不能那样使用 LEA。这使得 CALL 成为一个显而易见的选择。此外,LEA 中会包含一些零字节(因为相对位移始终为 4 个字节),并且许多 shellcode 字符串需要避免零字节。但是,由于您没有在代码中指定任何这些要求,因此我对 LEA 使用了更容易理解的方式。
【解决方案2】:

C 代码:

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv, char **env)
{
  argv[0] = "/bin/bash";
  return execve(argv[0], argv, env);
}

64位X86汇编代码:

// gcc shell.S -Wl,--build-id=none -static -nostdlib -fPIC -o shell
// strip -s shell

#include <asm/unistd.h>

        .global _start

        .text
_start:
        mov (%rsp), %rbx        # rbx = argc

        mov     $__NR_execve, %rax             # system call 3b is execve()
        lea     sh(%rip), %rdi     # command to execute
        lea 8(%rsp), %rsi       # argv
#        xor     %rsi, %rsi              # no args
        lea 16(%rsp, %rbx, 8), %rdx # envp !!!
#        xor     %rdx, %rdx              # no envp
        syscall                         # invoke system call

        # exit(0)
        mov     $__NR_exit, %rax               # system call 60 is exit
        xor     %rdi, %rdi              # we want return code 0
        syscall                         # invoke operating system to exit
sh:
        .string  "/bin/bash"

你可以调用 shell:

./shell

./shell -c "echo hello;echo world"

使用此代码,“shell”命令的所有参数都将被原封不动地传递给 /bin/bash。

进程名也会改变,变成“bash”(在ps -ef中)

还有什么更重要(我在其他任何地方都没有看到)

是使用此代码保留了环境。 (没有双关语)

如果您不需要 args 并且不希望保留 env,请取消注释 cmets 并注释前面的行。

【讨论】:

  • 该问题被标记为[shellcode],因此 OP 想要一个可以作为漏洞利用有效负载注入的 sn-p 代码。这就是为什么他们不希望它依赖于main 的参数或_start 堆栈布局。您已经为不同的问题写了一个很好的答案,例如How Get arguments value using inline assembly in C without Glibc? 或其他人关于编写自己的 _start(通常称为 main)。 OTOH,问题本身并没有真正这么说,所以我赞成你的答案,因为它对通过搜索到达这里的人可能有用。
  • 我们的两个答案都不能作为 shellcode,因为它们对 RIP 相关的 LEA 使用正的 rel32,它的高字节中将包含零。这可以通过在字符串上使用jmp 来解决,因此rel32 是负数,或者使用诸如使用LEA 的任意偏移量然后使用subadd 进行修复的技巧。 Avoiding 0xFF bytes in shellcode using CALL to read RIP?
  • OP 没有要求非零代码。我什至没有包括 setuid/setgid,因为它不在问题范围内。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-04-24
  • 2014-02-03
  • 2012-03-23
  • 2016-12-15
  • 2020-12-10
  • 2017-01-25
  • 1970-01-01
相关资源
最近更新 更多