如何手动编写和执行纯机器码,而不使用像EXE或ELF这样的容器?

71

我只需要一个“Hello World”演示程序,以了解机器码实际上是如何工作的。

虽然Windows的 EXE 和Linux的 ELF 接近机器码,但它们不是的。

我怎样才能编写/执行的机器码?


4
"pure" 的意思是什么?是指没有头文件吗?如果你以足够低的层次编写代码,就不需要链接任何库,因此可执行文件只是你的代码,文件结构被设置好以便操作系统可以加载它。 - Jeremiah Willcock
6
操作系统只运行指令,而不包含其他内容。 - compiler
相关但特别加载到另一个程序的虚拟内存中:https://dev59.com/-XA65IYBdhLWcg3wzyL8 - Ciro Santilli OurBigBook.com
10个回答

39

可以手动编写纯机器代码,无需汇编

Linux/ELF: https://github.com/XlogicX/m2elf。这仍在进行中,我昨天刚开始研究。

“Hello World”的源文件如下:

b8    21 0a 00 00   #moving "!\n" into eax
a3    0c 10 00 06   #moving eax into first memory location
b8    6f 72 6c 64   #moving "orld" into eax
a3    08 10 00 06   #moving eax into next memory location
b8    6f 2c 20 57   #moving "o, W" into eax
a3    04 10 00 06   #moving eax into next memory location
b8    48 65 6c 6c   #moving "Hell" into eax
a3    00 10 00 06   #moving eax into next memory location
b9    00 10 00 06   #moving pointer to start of memory location into ecx
ba    10 00 00 00   #moving string size into edx
bb    01 00 00 00   #moving "stdout" number to ebx
b8    04 00 00 00   #moving "print out" syscall number to eax
cd    80            #calling the linux kernel to execute our print to stdout
b8    01 00 00 00   #moving "sys_exit" call number to eax
cd    80            #executing it via linux sys_call

WIN/MZ/PE:

shellcode2exe.py(将ASCII十六进制格式的Shellcode转换为合法的MZ PE可执行文件)脚本位置:

https://web.archive.org/web/20140725045200/http://zeltser.com/reverse-malware/shellcode2exe.py.txt

依赖:

https://github.com/radare/toys/tree/master/InlineEgg

extract

python setup.py build




sudo python setup.py install

好的,m2elf现在支持内存分配;我刚刚测试了纯机器码的“Hello World”,它可以工作。相关证明放在上述GitHub页面的README底部。 - XlogicX
3
你是从哪里了解到这个的?你有没有一些可以分享的资源呢?我对此很感兴趣 :-) - Daniel
针对ELF头文件:elf.h,Ange Albertini的ELF信息图以及汇编->十六进制转储->分析黑客技术。 - XlogicX
对于机器码,我已经阅读了英特尔手册的第二卷(我也阅读了第一卷和第三卷,但第二卷是深入研究指令的)。我在我的一些博客文章中演示了一些奇怪的x86技巧(xlogicx.net)。如果您想进一步讨论,请给我发电子邮件。 - XlogicX
此外,如果您想使用m2elf.pl而无需设置太多内容,现在remnux v6已经内置了它(remnux.org)。 - XlogicX

25

真实机器码

运行测试所需的条件: Linux x86或x64(在我的情况下,我正在使用Ubuntu x64)

让我们开始吧

这个汇编(x86)将值666移动到eax寄存器中:

movl $666, %eax
ret

让我们将其转换为二进制表示:

操作码movl(带有32位操作数的mov)的二进制表示=1011

指令width的二进制表示=1

寄存器eax的二进制表示=000

有符号32位二进制中数字666的表示方式为=00000000 00000000 00000010 10011010

666 转换为 little endian 的形式=10011010 00000010 00000000 00000000

指令ret(返回)的二进制表示为=11000011

因此,我们最终的纯二进制指令如下所示:

1011(movl)1(width)000(eax)10011010000000100000000000000000(666) 11000011(ret)

将其全部组合在一起:

1011100010011010000000100000000000000000
11000011

要执行它,二进制代码必须放置在具有执行权限的内存页面中,我们可以使用以下C代码来实现:

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

/* Allocate size bytes of executable memory. */
unsigned char *alloc_exec_mem(size_t size)
{
    void *ptr;

    ptr = mmap(0, size, PROT_READ | PROT_WRITE | PROT_EXEC,
               MAP_PRIVATE | MAP_ANON, -1, 0);

    if (ptr == MAP_FAILED) {
            perror("mmap");
            exit(1);
    }

    return ptr;
}

/* Read up to buffer_size bytes, encoded as 1's and 0's, into buffer. */
void read_ones_and_zeros(unsigned char *buffer, size_t buffer_size)
{
    unsigned char byte = 0;
    int bit_index = 0;
    int c;

    while ((c = getchar()) != EOF) {
            if (isspace(c)) {
                    continue;
            } else if (c != '0' && c != '1') {
                    fprintf(stderr, "error: expected 1 or 0!\n");
                    exit(1);
            }

            byte = (byte << 1) | (c == '1');
            bit_index++;

            if (bit_index == 8) {
                    if (buffer_size == 0) {
                            fprintf(stderr, "error: buffer full!\n");
                            exit(1);
                    }
                    *buffer++ = byte;
                    --buffer_size;
                    byte = 0;
                    bit_index = 0;
            }
    }

    if (bit_index != 0) {
            fprintf(stderr, "error: left-over bits!\n");
            exit(1);
    }
}

int main()
{
    typedef int (*func_ptr_t)(void);

    func_ptr_t func;
    unsigned char *mem;
    int x;

    mem = alloc_exec_mem(1024);
    func = (func_ptr_t) mem;

    read_ones_and_zeros(mem, 1024);

    x = (*func)();

    printf("function returned %d\n", x);

    return 0;
}

来源: https://www.hanshq.net/files/ones-and-zeros_42.c

我们可以使用以下命令进行编译:

gcc source.c -o binaryexec

执行程序:

./binaryexec

然后我们输入第一组指令:

1011100010011010000000100000000000000000

按回车键

并通过执行返回指令:

11000011

按回车键

最后按ctrl+d结束程序并获取输出结果:

函数返回666


3
你从哪里获得MOV等操作码的?此外,为什么需要机器语言中的宽度命令? - B''H Bi'ezras -- Boruch Hashem
1
你知道是否有一种基本的跨浏览器文件格式,可以在任何平台上简单地双击执行这个机器码(假设没有外部依赖项)吗? - B''H Bi'ezras -- Boruch Hashem

22

众所周知,我们通常编写的应用程序是在操作系统上运行并由其管理。

这意味着操作系统在计算机上运行。因此,我认为你所说的是纯机器码。

所以,你需要学习操作系统如何工作。

这里有一些 NASM 汇编代码,可以在纯净环境下打印 "Hello world"。

 org
   xor ax, ax
   mov ds, ax
   mov si, msg
boot_loop:lodsb
   or al, al 
   jz go_flag   
   mov ah, 0x0E
   int 0x10
   jmp boot_loop

go_flag:
   jmp go_flag

msg   db 'hello world', 13, 10, 0

   times 510-($-$$) db 0
   db 0x55
   db 0xAA

你可以在这里找到更多资源:http://wiki.osdev.org/Main_Page

结束。

如果您已安装了nasm并有一个软盘,您可以

nasm boot.asm -f bin -o boot.bin
dd if=boot.bin of=/dev/fd0

接下来,你可以从这个软盘启动,并且你会看到消息。 (注意:你应该将计算机的第一次启动设为软盘。)

事实上,我建议你在完整的虚拟机中运行该代码,例如:bochs,virtualbox等。 因为很难找到带软盘驱动器的机器。

所以,步骤如下: 第一,你需要安装一个完整的虚拟机。 第二,通过命令创建一个虚拟软盘:bximage 第三,在虚拟软盘中写入bin文件。 最后,从虚拟软盘启动你的虚拟机。

注意:在 https://wiki.osdev.org 上,有一些关于该主题的基础信息。


有没有一个更简单的演示可以做一些相对容易的事情? - compiler
如果你使用NASM编译汇编代码,就可以使用GUI工具完成所有操作。在MS Windows中也可以这样做。你需要获取以下软件:1、Floppy image writer 2、Oracle VM VirtualBox。关键是,在VirtualBox中,首先选择引导顺序列表中选择的软盘并添加软盘控制器,然后加载由Floppy image writer创建的软盘映像文件。如何使用这两个工具,你可以阅读手册或搜索谷歌。这并不难。 - gelosie
6
“Pretty easy”? 那个人给了你16行汇编代码和运行它的命令。从零开始编写自己的操作系统再也没有比这更容易的了。我建议采用虚拟机方法而不是使用软盘。首先,现在很难找到软盘驱动器。其次,在真实计算机上引导自己的内核可能会损坏机器,但不能损坏模拟器。最后,不需要一直重启计算机就可以轻松测试。 - mgiuca
11
汇编语言并不是“纯粹”的机器码,而是机器码的一种抽象。 - Pharap
@编译器 这就是机器码最简单的形式,如果你认为这太难了,那么你可能没有足够的编程经验来尝试机器码。 - Pharap
除此方法外,这个链接听起来像是一个很好的方式来开始你的第一个比"Hello World"稍微有些挑战的程序。http://99-bottles-of-beer.net/language-assembler-(intel-8086)-45.html - Dean Meehan

17

听起来你正在寻找旧的16位DOS .COM 文件格式。一个.COM文件的字节被加载到程序段的偏移量100h处(将其限制在最大大小为64k-256字节),然后CPU就直接从偏移量100h开始执行。这种格式没有头部或者任何必需的信息,只有原始的CPU指令。


你(或其他人)能否提供一个用这种方式编写的程序的Hello World示例? - petersaints
3
好的,这是一个例子:http://99-bottles-of-beer.net/language-assembler-(intel-8086)-45.html - Greg Hewgill
3
Greg,这个例子只是更多的汇编代码,不是纯的机器码,而且啤酒很恶心。 - XlogicX

12

操作系统并不运行指令,而是由CPU来执行(除非我们谈论的是虚拟机操作系统,虚拟机操作系统确实存在,我想到的是Forth或类似的语言)。然而,操作系统需要一些元信息来知道一个文件是否包含可执行代码,以及它希望环境看起来像什么。ELF不仅仅是接近于机器码,它就是机器码,加上一些信息,让操作系统知道它应该让CPU实际执行那个东西。

如果您想要比ELF更简单但与*nix相关的格式,请查看a.out格式,这个格式要简单得多。传统的*nix C编译器(仍然)将可执行文件写入名为a.out的文件中,如果未指定输出名称。


“out”格式是从纯机器语言生成可执行文件的绝对最简单的方法吗?它是否跨平台? - B''H Bi'ezras -- Boruch Hashem
@bluejayke:这是最简单的格式,曾经被*nixes支持。然而,现在它几乎不再得到支持。它绝对不是跨平台的。如果你想采用最简单、最基本的“格式”,你可以在CPU的复位向量地址处编写一个无头、原始指令流,并将其写入BIOS/UEFI固件闪存中。但是,你必须首先实现整个BIOS,才能做出有意义的事情。 - datenwolf
@bluejayke:就最简单的格式而言,在旧版DOS中,那应该是COM格式,它也只是一个原始指令流(就像固件一样)。然而,这些只在DOS中有效。 - datenwolf
哦,有趣。如果整个BIOS确实被编写了,那么在另一个操作系统中加载它是否可能呢? - B''H Bi'ezras -- Boruch Hashem
@bluejayke:如果你要加载到机器模拟器中,那么是的。这基本上就是所有虚拟机(如VirtualBox、VMWare、Qemu等)所做的。然而,BIOS不是您可以在操作系统中作为常规进程执行的程序类型。有两个原因:第一,运行在现代操作系统下的进程被防止执行可能的所有操作;BIOS所进行的底层访问是被禁止的。第二,BIOS尝试做的许多事情,都会严重干扰操作系统已经在执行的任务。 - datenwolf

2
下面是我用16位机器码(Intel 8086)编写的“Hello World”程序。如果你想了解机器码,我建议你先学习汇编语言,因为汇编语言中的每一行代码都会转换成机器码的一行代码。据我所知,我是世界上为数不多仍在使用机器码而非汇编语言编程的人之一。
顺便说一句,在DOSBOX上运行它,将文件保存为“.com”扩展名即可!
这就是一个“Hello World”程序。请参见此处的截图。

1
解释: "B4 00 B0 10 CD 10" 设置视频模式为EGA,16种颜色,640x350。 "EB" 是一个短jmp命令,它可以向前跳127个字节,向后跳128个字节。 "4B" 是要跳转的数量。 "$0"(24 30)是打印命令的指示符,表示该字符串从哪里开始。 字符串末尾的“00”(null)告诉打印方法已经到达字符串的结尾。 "AC 3C 24 72 02 EB F9" 告诉方法将下一个字节从内存加载到AX堆栈中,如果AL(AX的一部分)等于“24”($),则转到“AC 38 D0 74 02 EB F2”...... - גיא כהן
1
...检查AL寄存器是否等于DL寄存器(我将0或1字符移入其中),如果是,则跳转到“AC 3C 00 74 06 B4 0E CD 10 EB F5 C3”,该指令基本上打印下一个字节,直到下一个字节为“00”为止,然后返回。 “B2 31 B3 0C”将DL寄存器设置为字符1,并将bl(用于颜色)设置为0C(浅红色),而“E8”则告诉它调用“CB FF”中的方法。其余部分只是用户输入,将视频模式设置回03,并退出(“B4 4C CD 21”)。 - גיא כהן
你好。我正在寻找一种方法来查看这段代码:[[[ function incrementX(obj) { return 1 + obj.x; } incrementX({x: 42}); ]]]中方括号内的机器码是什么样子的。我已经能够从中生成AST和字节码,但无法生成机器码。你知道如何做吗? - rayaqin
@גיאכהן 我应该在哪里学习机器码编程,或者更好地说:我应该如何了解如何创建机器码代码?我真的想知道事物在最内部的方式下是如何工作的! - Gilberto Albino

1
这些回答都不错,但为什么有人想要这样做可能会更好地指导答案。我认为最重要的原因是为了完全控制他们的机器,特别是对其缓存写入进行最大性能优化,并防止任何操作系统共享处理器或虚拟化代码(从而减慢速度),尤其是在这些天窥探您的代码。就我所知,汇编语言无法解决这些问题,微软/英特尔和其他公司将其视为侵权或“针对黑客”的行为。然而,这是非常错误的。如果您的汇编代码交给操作系统或专有硬件,则真正的优化(可能在GHz频率下)将无法实现。这对于科学技术来说是一个非常重要的问题,因为我们的计算机如果没有硬件优化就无法充分发挥其潜力,通常计算效率比它低几个数量级。可能有一些变通方法或一些开源硬件可以实现这一点,但我还没有找到。请分享您的想法。

1

当针对嵌入式系统时,您可以将ROM或RAM的二进制图像限定为程序中的指令和相关数据。通常可以将该二进制写入闪存/ROM并运行它。

操作系统希望了解更多信息,并且开发人员通常希望在文件中留下更多内容,以便以后进行调试或执行其他操作(使用一些可识别的符号名称进行反汇编)。此外,在嵌入式系统或操作系统上,您可能需要将.text与.data、.bss与.rodata等分离,而.elf等文件格式提供了机制来实现这一点,首选用例是通过某种加载器加载该elf文件,无论是操作系统还是编程ROM和RAM的微控制器。

.exe也具有一些头部信息。如前所述,.com文件在0x100h地址处加载并跳转到那里。

要从可执行文件创建原始二进制文件,例如由gcc创建的elf文件,您可以执行以下操作:

objcopy file.elf -O binary file.bin

如果程序被分段了(.text,.data等),并且这些段不是相邻的,那么二进制文件就会变得非常大。再次以嵌入式为例,如果rom在0x00000000处,而数据或bss在0x20000000处,即使您的程序只有4个字节的数据objcopy也会创建一个0x20000004字节的文件来填补.text和.data之间的空隙(因为这正是您要求它做的)。

你想做什么?阅读elf、intel hex或srec文件非常简单,从中可以看到二进制文件的所有位和字节。或者反汇编elf或其他格式,也可以以人类可读的形式展示给你。(objdump -D file.elf > file.list)


1

通过纯机器代码,您可以使用任何具有编写文件能力的语言。 即使是Visual Basic.net也可以在写入时在8、16、32、64位之间交换int类型。

您甚至可以设置vb根据需要循环输出机器代码, 例如setpixel,其中x、y更改并且您有argb颜色。

或者,在Windows中定期创建您的vb.net程序,并使用NGEN.exe制作程序的本地代码文件。它会一次性创建特定于ia-32的纯机器代码,将JIT调试器抛在一边。


0
在Windows系统中,至少是32位的Windows系统,您可以使用.com文件执行原始指令。
例如,如果您将此字符串保存在记事本中,并使用.com扩展名保存:
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

它将打印一个字符串并触发您的防病毒软件。


1
这不是机器码。这只是一个EICAR测试字符串,用于测试防病毒软件。出于测试目的,它将被检测为病毒。 - petersaints
7
当作为com文件运行时,所有的X86指令都是有效的。 - Patrick
6
一个常见的误解是EICAR只是一个字符串。Patrick是完全正确的,这个程序使用int 21来打印以"$"结尾的字符串"EICAR-STANDARD-ANTIVIRUS-TEST-FILE"。但由于这个完整的中断不是ASCII可打印的,引导到该字符串的代码是自修改的,以便可以使用INT 21。请阅读以下链接,以获取有趣的全面逐步分析:http://thestarman.pcministry.com/asm/eicar/eicarcom.html - XlogicX
还应该注意的是,当作为com文件运行时,并不是“所有有效的X86”。它可能会被发送到处理器进行执行,但不能保证处理器能够对代码做任何事情,更糟糕的是,它可能会运行一个未记录的命令,例如制造商代码,这可能会使设备无法使用(理论上虽然相当不可能)。 - MrMesees

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