Blue Fox Arm Assembly Internals and Reverse Engineering读书笔记
01-Blue Fox 读书笔记-01-Introduction
02-Blue Fox 读书笔记-02-Part 1_Chapter 1_Introduction to ReverseEngineering
目录
Introduction to Assembly
- If you’re reading this book, you’ve probably already heard about this thing called the Arm assembly language and know that understanding it is the key to analyzing binaries that run on Arm. But what is this language, and why does it exist? After all, programmers usually write code in high-level languages such as C/C++, and hardly anyone programs in assembly directly. High-level languages are, after all, far more convenient for programmers to program in.
如果你正在阅读这本书,你可能已经听说过这个叫做Arm汇编语言的东西,并知道理解它是分析在Arm上运行的二进制文件的关键。但这种语言是什么,为什么它存在?毕竟,程序员通常使用如C/C++这样的高级语言编写代码,几乎没有人直接用汇编语言编程。高级语言毕竟对程序员来说编程更为方便。
- Unfortunately, these high-level languages are too complex for processors to interpret directly. Instead, programmers compile these high-level programs down into the binary machine code that the processor can run.
不幸的是,这些高级语言对于处理器来说太复杂了,无法直接解释执行。因此,程序员将这些高级程序编译成处理器可以运行的二进制机器码。
- This machine code is not quite the same as assembly language. If you were to look at it directly in a text editor, it would look unintelligible. Processors also don’t run assembly language; they run only machine code. So, why is it so important in reverse engineering?
这种机器码与汇编语言并不完全相同。如果你直接在文本编辑器中查看它,它会看起来难以理解。处理器也不运行汇编语言;它们只运行机器码。那么,在逆向工程中,为什么它如此重要?
- To understand the purpose of assembly, let’s do a quick tour of the history of computing to see how we got to where we are and how everything connects.
要理解汇编的目的,让我们快速回顾计算机的历史,看看我们是如何达到现在这个地步的,以及所有的东西是如何联系在一起的。
Bits and Bytes
- Back in the mists of time when it all started, people decided to create computers and have them perform simple tasks. Computers don’t speak our human languages—they are just electronic devices after all—and so we needed a way to communicate with them electronically. At the lowest level, computers operate on electrical signals, and these signals are formed by switching electrical voltages between one of two levels: on and off.
回到梦开始的地方,当一切开始时,人们决定创建计算机并让它们执行简单的任务。计算机并不理解我们的人类语言——它们毕竟只是电子设备——因此,我们需要一种方式与它们进行电子通信。在最基本的层面上,计算机基于电信号运行,这些信号是通过在两个电压水平之间切换来形成的:高电平和低电平。
- The first problem is that we need a way to describe these “ons” and “offs” for communication, storage, and simply describing the state of the system. Since there are two states, it was only natural to use the binary system for encoding these values. Each binary digit (or bit) could be 0 or 1. Although each bit can store only the smallest amount of information possible, stringing multiple bits together allows representation of much larger numbers. For example, the number 30,284,334,537 could be represented in just 35 bits as the following:
首先,我们需要一种方法来描述这些“高电平”和“低电平”以进行通信、存储以及简单地描述系统的状态。由于有两种状态,因此使用二进制系统来编码这些值是最自然的选择。每个二进制位(或称为比特)可以是0或1。尽管每个比特只能存储最小量的信息,但将多个比特组合在一起可以表示更大的数字。例如,数字30,284,334,537可以用仅仅35个比特来表示,如下所示:
11100001101000101100100010111001001
- Already this system allows for encoding large numbers, but now we have a new problem: where does one number in memory (or on a magnetic tape) end and the next one begin? This is perhaps a strange question to ask modern readers, but back when computers were first being designed, this was a serious problem. The simplest solution here would be to create fixed-size groupings of bits. Computer scientists, never wanting to miss out on a good naming pun, called this group of binary digits or bits a byte.
虽然这种系统已经允许对大数字进行编码,但现在我们面临一个新的问题:在内存中(或磁带上)一个数字在哪里结束,下一个数字从哪里开始?对于现代读者来说,这可能是一个奇怪的问题,但在计算机刚开始设计时,这是一个严重的问题。最简单的解决方案是创建固定大小的比特组。计算机科学家,不想错过一个好的命名机会,称这组二进制数字或比特为字节。
- So, how many bits should be in a byte? This might seem like a blindingly obvious question to our modern ears, since we all know that a modern byte is 8 bits. But it was not always so.
那么,一个字节应该有多少比特呢?对于现代的我们来说,这可能似乎是一个显而易见的问题,因为我们都知道现代的字节是8比特。但这并不总是这样的。
- Originally, different systems made different choices for how many bits would be in their bytes. The predecessor of the 8-bit byte we know today is the 6-bit Binary Coded Decimal Interchange Code (BCDIC) format for representing alphanumeric information used in early IBM computers, such as the IBM 1620 in 1959. Before that, bytes were often 4 bits long, and before that, a byte stood for an arbitrary number of bits greater than 1. Only later, with IBM’s 8-bit Extended Binary Coded Decimal Interchange Code (EBCDIC), introduced in the 1960s in its mainframe computer product line System/360 and which had byte-addressable memory with 8-bit bytes, did the byte start to standardize around having 8 bits. This then led to the adoption of the 8-bit storage size in other widely used computer systems, including the Intel 8080 and Motorola 6800.
最初,不同的系统对其字节中应该有多少比特做出了不同的选择。我们今天所知的8比特字节的前身是6比特的二进制编码十进制交换码(BCDIC),它用于表示早期IBM计算机中的字母数字信息,例如1959年的IBM 1620。在此之前,字节通常为4比特,而在那之前,字节代表了大于1的任意数量的比特。只是后来,随着IBM在1960年代在其大型机计算机产品线System/360中引入的8比特扩展二进制编码十进制交换码(EBCDIC),并且该系统具有8比特字节的字节寻址内存,字节开始围绕拥有8比特进行标准化。这进一步导致了其他广泛使用的计算机系统,包括Intel 8080和Motorola 6800,采纳8比特的存储大小。
- The following excerpt is from a book titled Planning a Computer System, published 1962, listing three main reasons for adopting the 8-bit byte1:
以下摘录来自1962年出版的一本名为《计划计算机系统》的书,列出了采用8比特字节的三个主要原因1:
- Its full capacity of 256 characters was considered to be sufficient for the great majority of applications.
其256个字符的完整容量被认为足够满足绝大多数应用的需求。
- Within the limits of this capacity, a single character is represented by a single byte, so that the length of any particular record is not dependent on the coincidence of characters in that record.
在这个容量的限制内,单个字符由单个字节表示,因此任何特定记录的长度并不依赖于该记录中字符的组合。
- 8-bit bytes are reasonably economical of storage space.
8比特字节在存储上是相对经济实惠的。
- An 8-bit byte can hold one of 256 uniquely different values from 00000000 to 11111111. The interpretation of those values, of course, depends on the software using it. For example, we can store positive numbers in those bytes to represent a positive number from 0 to 255 inclusive. We can also use the two’s complement scheme to represent signed numbers from –128 to 127 inclusive.
一个8比特的字节可以容纳从00000000到11111111的256个独特的不同值。当然,这些值的解释取决于使用它的软件。例如,我们可以在这些字节中存储正数,代表从0到255(包含)的正数。我们还可以使用二进制补码方案来表示从-128到127(包含)的有符号数字。
Character Encoding
- Of course, computers didn’t just use bytes for encoding and processing integers. They would also often store and process human-readable letters and numbers, called characters.
当然,计算机不仅仅使用字节来编码和处理整数。它们还经常存储和处理人类可读的字母和数字,称为字符。
- Early character encodings, such as ASCII, had settled on using 7 bits per byte, but this gave only a limited set of 128 possible characters. This allowed for encoding English-language letters and digits, as well as a few symbol characters and control characters, but could not represent many of the letters used in other languages. The EBCDIC standard, using its 8-bit bytes, chose a different character set entirely, with code pages for “swapping” to different languages. But ultimately this character set was too cumbersome and inflexible.
早期的字符编码,如ASCII,决定每个字节使用7比特,但这只提供了一个限定的128个可能的字符集。这允许对英文字母和数字进行编码,以及一些符号字符和控制字符,但不能表示其他语言中使用的许多字母。EBCDIC标准使用其8比特字节选择了一个完全不同的字符集,并为切换到不同语言提供了代码页。但最终,这种字符集过于繁琐和不灵活。
- Over time, it became clear that we needed a truly universal character set, sup- porting all the world’s living languages and special symbols. This culminated in the creation of the Unicode project in 1987. A few different Unicode encodings exist, but the dominant encoding used on the Web is UTF-8. Characters within the ASCII character -set are included verbatim in UTF-8, and “extended characters” can spread out over multiple consecutive bytes.
随着时间的推移,我们明确需要一个真正的通用字符集,支持世界上所有的现存语言和特殊符号。这导致了1987年Unicode项目的创建。存在几种不同的Unicode编码,但Web上使用的主导编码是UTF-8。ASCII字符集中的字符在UTF-8中被直接包含,并且“扩展字符”可以连续扩展多个字节。
- Since characters are now encoded as bytes, we can represent characters using two hexadecimal digits. For example, the characters A, R, and M are normally encoded with the octets shown in Figure 1.1.
由于字符现在被编码为字节,我们可以使用两个十六进制数字来表示字符。例如,字符A、R和M通常使用图1.1中显示的八位组进行编码。
- Each hexadecimal digit can be encoded with a 4-bit pattern ranging from 0000 to 1111, as shown in Figure 1.2.
每个十六进制数字可以使用一个从0000到1111的4比特模式进行编码,如图1.2所示。
- Since two hexadecimal values are required to encode an ASCII character, 8 bits seemed like the ideal for storing text in most written languages around the world, or a multiple of 8 bits for characters that cannot be represented in 8 bits alone.
由于编码一个ASCII字符需要两个十六进制值,因此8比特似乎是存储世界上大多数书面语言中的文本的理想选择,或者对于不能单独用8比特表示的字符,使用8比特的倍数。
- Using this pattern, we can more easily interpret the meaning of a long string of bits. The following bit pattern encodes the word Arm:
利用这种模式,我们可以更容易地解释一长串比特的含义。以下的比特模式编码了单词“Arm”:
0100 0001 0101 0010 0100 1101
Machine Code and Assembly
- One uniquely powerful aspect of computers, as opposed to the mechanical calculators that predated them, is that they can also encode their logic as data. This code can also be stored in memory or on disk and be processed or changed on demand. For example, a software update can completely change the operating system of a computer without the need to purchase a new machine.
计算机与之前的机械计算器相比有一个独特的强大特点,那就是它们还可以将其逻辑编码为数据。这些代码也可以存储在内存或硬盘上,并根据需求进行处理或更改。例如,软件更新可以完全改变计算机的操作系统,而无需购买新机器。
- We’ve already seen how numbers and characters are encoded, but how is this logic encoded? This is where the processor architecture and its instruction set comes into play.
我们已经看到了如何编码数字和字符,但这种逻辑是如何编码的呢?这就是处理器架构及其指令集发挥作用的地方。
- If we were to create our own computer processor from scratch, we could design our own instruction encoding, mapping binary patterns to machine codes that our processor can interpret and respond to, in effect, creating our own “machine language”2 3 4. Since machine codes are meant to “instruct” the circuitry to perform an “operation,” these machine codes are also referred to as instruction codes, or, more commonly, operation codes (opcodes).
如果我们要从头开始创建自己的计算机处理器2 3 4,我们可以设计自己的指令编码,将二进制模式映射到我们的处理器可以解释和响应的机器码,从而实际上创建自己的“机器语言”。由于机器码旨在“指示”电路执行“操作”,这些机器码也被称为指令代码,或更常见的是操作代码(操作码)。
- In practice, most people use existing computer processors and therefore use the instruction encodings defined by the processor manufacturer. On Arm, instruction encodings have a fixed size and can be either 32-bit or 16-bit, depending on the instruction set in use by the program. The processor fetches and interprets each instruction and runs each in turn to perform the logic of the program. Each instruction is a binary pattern, or instruction encoding, which follows specific rules defined by the Arm architecture.
实际上,大多数人使用现有的计算机处理器,因此使用由处理器制造商定义的指令编码。在Arm上,指令编码具有固定的大小,可以是32比特或16比特,具体取决于程序使用的指令集。处理器获取并解释每个指令,并依次运行每个指令以执行程序的逻辑。每个指令都是一个二进制模式,或指令编码,它遵循Arm架构定义的特定规则。
- By way of example, let’s assume we’re building a tiny 16-bit instruction set and are defining how each instruction will look. Our first task is to designate part of the encoding as specifying exactly what type of instruction is to be run, called the opcode. For example, we might set the first 7 bits of the instruction to be an opcode and specify the opcodes for addition and subtraction, as shown in Table 1.1.
举例来说,假设我们正在构建一个微小的16比特指令集,并定义每个指令的外观。我们的第一个任务是指定编码的一部分,以确切指定要运行的指令类型,称为操作码。例如,我们可能设置指令的前7比特为操作码,并指定加法和减法的操作码,如表1.1所示。
- Writing machine code by hand is possible but unnecessarily cumbersome. In practice, we’ll want to write assembly in some human-readable “assembly language” that will be converted into its machine code equivalent. To do this, we should also define the shorthand for the instruction, called the instruction mnemonic, as shown in Table 1.2.
手工编写机器码是可行的,但过于繁琐。实际上,我们会希望用某种人类可读的“汇编语言”编写汇编,然后将其转换为相应的机器码。为此,我们还应定义指令的简写,称为指令助记符,如表1.2所示。
- Of course, it’s not sufficient to tell a processor to just do an “addition.” We also need to tell it what two things to add and what to do with the result. For example, if we write a program that performs “a = b + c,” the values of b and c need to be stored somewhere before the instruction begins, and the instruction needs to know where to write the result a to.
当然,仅仅告诉处理器执行“加法”是不够的。我们还需要告诉它要加的两个数是什么,以及如何处理结果。例如,如果我们编写一个执行“a = b + c”的程序,在指令开始之前,b和c的值需要存储在某个地方,指令还需要知道将结果a写入何处。
- In most processors, and Arm processors in particular, these temporary values are usually stored in registers, which store a small number of “working” values. Programs can pull data in from memory (or disk) into registers ready to be processed and can spill result data back to longer-term storage after processing.
在大多数处理器中,特别是Arm处理器中,这些临时值通常存储在寄存器中,寄存器存储一小部分“工作中”的值。程序可以从内存(或硬盘)中提取数据到准备好的寄存器中进行处理,并在处理后将结果数据返回到长期存储中。
- The number and naming conventions of registers are architecture-dependent. As software has become more and more complex, programs must often juggle larger numbers of values at the same time. Storing and operating on these values in registers is faster than doing so in memory directly, which means that registers reduce the number of times a program needs to access memory and result in faster execution.
寄存器的数量和命名规范取决于架构。随着软件变得越来越复杂,程序通常需要同时处理更多的值。在寄存器中存储和操作这些值比直接在内存中这样做更快,这意味着寄存器减少了程序需要访问内存的次数,从而可以更快的执行。
- Going back to our earlier example, we were designing a 16-bit instruction to per- form an operation that adds a value to a register and writes the result into another register. Since we use 7 bits for the operation (ADD/SUB) itself, the remaining 9 bits can be used for encoding the source and the destination registers and a constant value we want to add or subtract. In this example, we split the remaining bits evenly and assign the shortcuts and respective machine codes shown in Table 1.3.
回到我们之前的例子,我们设计了一个16比特的指令来执行一个操作,该操作将一个值加到一个寄存器上,并将结果写入另一个寄存器。由于我们为操作(ADD/SUB)本身使用了7比特,剩下的9比特可以用来编码源和目标寄存器以及我们想要添加或减去的常数值。在这个例子中,我们均匀地划分了剩余的比特,并分配了表1.3中显示的快捷方式和相应的机器码。
- Instead of generating these machine codes by hand, we could instead write a little program that converts the syntax ADD R1, R0, #2 (R1 = R0 + 2) into the corresponding machine-code pattern and hand that machine-code pattern to our example processor. See Table 1.4.
我们无需手动生成这些机器码,而是可以编写一个小程序,将语法==ADD R1, R0, #2==(R1 = R0 + 2)转换为相应的机器码模式,并将该机器码模式提供给我们的示例处理器。请参见表1.4。
- The bit pattern we constructed represents one of the instruction encodings for 16-bit ADD and SUB instructions that are part of the T32 instruction set. In Figure 1.3 you can see its components and how they are ordered in the instruction encoding.
我们构建的比特模式代表了T32指令集中16比特 ADD 和 SUB 指令的其中一个指令编码。在图1.3中,你可以看到它的组成部分以及它们在指令编码中的顺序。
- Of course, this is just a simplified example. Modern processors provide hundreds of possible instructions, often with more complex sub-encodings. For example, Arm defines the load register instruction (with the
LDR mnemonic
) that loads a 32-bit value from memory into a register, as illustrated in Figure 1.4.
当然,这只是一个简化的例子。现代处理器提供了数百种可能的指令,通常带有更复杂的子编码。例如,Arm定义了加载寄存器指令(使用
LDR助记符
),该指令将32比特的值从内存加载到寄存器中,如图1.4所示。
- In this instruction, the “
address
” to load is specified in register 2 (calledR2
), and the read value is written to register 3 (calledR3
).
在这个指令中,要加载的“
地址
”在寄存器2中指定(称为R2
),并将读取的值写入寄存器3(称为R3
)。说白了就是:将寄存器R2
中存储的值作为内存地址,并将该地址处的数据存储到寄存器R3
中。
- The syntax of writing brackets around
R2
indicates that the value inR2
is to be interpreted as an address in memory, rather than an ordinary value. In other words, we do not want to copy the value inR2
intoR3
, but rather fetch the con- tents of memory at the address given byR2
and load that value intoR3
. There are many reasons for a program to reference a memory location, including calling a function or loading a value from memory into a register.
将
R2
两侧加上中括号的语法表示,R2
中的值应被解释为内存中的一个地址,而不是一个普通的值。换句话说,我们不想将R2
中的值复制到R3
,而是要取出R2
给出的地址处的内存内容,并将该值加载到R3
中。程序引用内存位置的原因有很多,包括调用函数或将内存中的值加载到寄存器中。
- This is, in essence, the difference between machine code and assembly code. Assembly language is the human-readable syntax that shows how each encoded instruction should be interpreted. Machine code, by contrast, is the actual binary data ingested and processed by the actual processor, with its encoding specified precisely by the processor designer.
这本质上是机器码和汇编代码之间的区别。汇编语言是人类可读的语法,显示每个编码指令应如何被解释。相比之下,机器码是实际处理器实际摄取和处理的实际二进制数据,其编码由处理器设计者精确指定。
Assembling
- Since processors understand only machine code, and not assembly language, how do we convert between them? To do this we need a program to convert our handwritten assembly instructions into their machine-code equivalents. The programs that perform this task are called assemblers.
由于处理器只理解机器码,而不理解汇编语言,我们如何在它们之间进行转换呢?为此,我们需要一个程序将我们手写的汇编指令转换为其机器码等价物。执行此任务的程序称为汇编器(是将汇编语言翻译为机器语言的程序)。
- In practice, assemblers are capable not only of understanding and translating individual instructions into machine code but also of interpreting assembler directives5 that direct the assembler to do other things, such as switch between data and code or assemble different instruction sets. Therefore, the terms assembly language and assembler language are just two ways of looking at the same thing. The syntax and meaning of individual assembler directives and expressions depend on the specific assembler.
实际上,汇编器不仅能够理解并将单个指令翻译成机器码,还能够解释汇编指令,指导汇编器执行其他操作,例如在数据和代码之间切换或汇编不同的指令集。因此,汇编语言和“汇编器”语言这两个术语只是看待同一事物的两种方式。单个汇编指令和表达式的语法和含义取决于特定的汇编器。
note:
assembly language:就是我们常说的汇编语言
assembler language:其实也是汇编语言,但是从另一个角度说的。
- These directives and expressions are useful shortcuts that can be used in an assembly program; however, they are not strictly part of the assembly language itself, but rather are directions for how the assembler itself should operate.
这些指令和表达式是汇编程序中可以使用的有用的快捷方式;但它们并不严格地属于汇编语言本身,而是关于汇编器本身应如何操作的指示。
- There are different assemblers available on different platforms, such as the GNU assembler
as
, which is also used to assemble the Linux kernel, the ARM Toolchain assemblerarmasm
, or the Microsoft assembler with the same name (armasm
) included in Visual Studio.
在不同的平台上有不同的汇编器,例如GNU汇编器
as
,它也被用于汇编Linux内核,ARM工具链汇编器armasm
,或者包含在Visual Studio中的与其同名的Microsoft汇编器(armasm
)。
- Suppose, by way of example, we want to assemble the following two 16-bit instructions written in a file named
myasm.s
:
假设我们想要组装一个名为
myasm.s
的文件中写的以下两个16位指令,举个例子:(这里没有列出具体的指令)。
.section .text
.global _start
_start:
.thumb
movs r1, #5
ldr r3, [r2]
- In this program, the first three lines are assembler directives. These tell the assembler information about where the data should be assembled (in this case, placed in the .text section), define the label of the entry point of our code (in this case, called _start) as a global symbol, and finally specify that the instruction encoding it should use should be Thumb. The Thumb instruction set (T32) is part of the Arm architecture and allows instructions to be 16-bit wide.
在这个程序中,前三行是=="汇编器"指令==(汇编伪指令)。这些告诉汇编器关于数据应该被组装的位置的信息(在这种情况下,放在.text段中),定义我们代码的入口点的标签(在这种情况下,被称为_start)作为一个全局符号,最后指定它应使用的指令编码应该是Thumb。Thumb指令集(T32)是Arm架构的一部分,允许指令宽度为16位。
- We can use the GNU assembler,
as
, to compile this program on a Linux operating system machine running on an Arm processor.
我们可以使用GNU汇编器,即
as
,来编译这个程序,这在一个运行在Arm处理器上的Linux操作系统机器上完成。
$ as myasm.s -o myasm.o
- The assembler reads the assembly language program
myasm.s
and creates an object file calledmyasm.o
. This file contains 4 bytes of machine code corresponding to our two 2-byte instructions in hexadecimal.
汇编器读取汇编语言程序
myasm.s
并创建一个名为myasm.o
的对象文件。这个文件包含与我们两个2字节指令相对应的4字节机器码,以十六进制表示。
05 10 a0 e3 00 30 92 e5
- Another particularly useful feature of assemblers is the concept of a label, which references a specific address in memory, such as the address of a branch target, function, or global variable.
汇编器的另一个特别有用的特性是标签的概念,它引用内存中的特定地址,例如分支目标、函数或全局变量的地址。
- Let’s take the assembly program as an example.
让我们以汇编程序为例。
.section .text
.global _start
_start:
mov r1, #5
mov r2, #6
b mylabel
result:
mov r0, r4
b _exit
mylabel:
add r4, r1, r2
b result
_exit:
mov r7, #0
svc #0
- This program starts by filling two registers with values and branches, or jumps, to the label
mylabel
to execute theADD
instruction. After theADD
instruction is executed, the program branches to theresult
label, executes the move instruction, and ends with a branch to the_exit
label. The assembler will use these labels to provide hints to the linker that assigns relative memory locations to them. Figure 1.5 illustrates the program flow.
这个程序首先用值填充两个寄存器,然后跳转到标签
mylabel
执行ADD
指令。ADD
指令执行后,程序跳转到result
标签,执行移动指令,并以跳转到_exit
标签结束。汇编器将使用这些标签为链接器提供提示,为它们分配相对的内存位置。图1.5描绘了程序的流程。
- Labels are not only useful for referencing instructions to jump to but can also be used to fetch the contents of a memory location. For instance, the following assembly code snippet uses labels to fetch the contents from a memory location or jump to different instructions in the code:
标签不仅对于引用要跳转到的指令有用,还可以用来取得内存位置的内容。例如,以下汇编代码片段使用标签从内存位置获取内容或跳转到代码中的不同指令:
.section .text
.global _start
_start:
mov r1, #5 // 1. fill r1 with value 5
adr r2, myvalue // 2. fill r2 with address of mystring
ldr r3, [r2] // 3. fill r3 with value at address in r2
b mylabel // 4. jump to address of mylabel
result:
mov r0, r4 // 7. fill r0 with value in r4
b _exit // 8. Branch to address of _exit
mylabel:
add r4, r1, r3 // 5. fill r4 with result of r1 + r3
b result // 6. jump to result
myvalue:
.word 2 // word-sized value containing value 2
- The
ADR
instruction loads the address of variablemyvalue
into registerR2
and uses anLDR
instruction to load the contents of that address into registerR3
. The program then branches to the instruction referenced by the labelmylabel
, executes anADD
instruction, and branches to the instruction referenced by the labelresult
, as illustrated in Figure 1.6.
ADR
指令将变量myvalue
的地址加载到寄存器R2
中,并使用LDR
指令将该地址的内容加载到寄存器R3
中。程序接着跳转到由标签mylabel
引用的指令,执行一个ADD
指令,并跳转到由标签result
引用的指令,如图1.6所示。
- As a slightly more interesting example, the following assembly code prints
Hello World!
to the console and then exits. It uses a label to reference the stringhello
by putting the relative address of its labelmystring
into registerR1
with anADR
instruction.
作为一个稍微有趣一点的例子,下面的汇编代码将“Hello World!”打印到控制台,然后退出。它使用一个标签来通过将其标签
mystring
的相对地址与ADR
指令放入寄存器R1
来引用字符串hello
。
.section .text
.global _start
_start:
mov r0, #1 // STDOUT
adr r1, mystring // R1 = address of string
mov r2, #6 // R2 = size of string
mov r7, #4 // R7 = syscall number for 'write()'
svc #0 // invoke syscall
_exit:
mov r7, #0
svc #0
mystring:
.string "Hello\n"
- After assembling and linking this program on a processor that supports the Arm architecture and the instruction set we use, it prints out
Hello
when executed.
在支持Arm架构和我们使用的指令集的处理器上汇编和链接这个程序后,执行时它会输出
Hello
。
$ as myasm2.s -o myasm2.o
$ ld myasm2.o -o myasm2
$ ./myasm2
Hello
- Modern assemblers are often incorporated into compiler toolchains and are designed to output files that can be combined into larger executable programs. For this reason, assembly programs usually don’t just convert assembly instructions directly into machine code, but rather create an object file, including the assembly instructions, symbol information, and hints for the compiler’s
linker
program, which is ultimately responsible for creating full executable files to be run on modern operating systems.
现代汇编器通常被整合到编译器工具链中,并设计成输出可以合并成更大的可执行程序的文件。因此,汇编程序通常不仅仅直接将汇编指令转换成机器码,而是创建一个对象文件,包括汇编指令、符号信息以及供编译器的
链接器
程序使用的提示,最终这个链接器程序负责创建可以在现代操作系统上运行的完整的可执行文件。
Cross-Assemblers
- What happens if we run our Arm program on a different processor architecture? Executing our
myasm2
program on an Intel x86-64 processor will result in an error telling us that the binary file cannot be executed due to an error in the executable format.
如果我们在不同的处理器架构上运行我们的Arm程序会发生什么?在Intel x86-64处理器上执行我们的
myasm2
程序将导致一个错误,告诉我们由于可执行格式中的错误,二进制文件不能被执行。
user@ubuntu:~$ ./myasm
bash: ./myasm: cannot execute binary file: Exec format error
- We can’t run our Arm binary on an x64 machine because instructions are encoded differently on the two platforms. Even if we want to perform the same operation on different architectures, the assembly language and assigned machine codes can differ significantly. Let’s say you want to execute an instruction to move the decimal number 1 into the first register on three different processor architectures. Even though the operation itself is the same, the instruction encoding and assembly language depends on the architecture. Take the following three general architecture types as an example:
我们不能在x64机器上运行我们的Arm二进制文件,因为在这两个平台上指令的编码方式不同。即使我们想在不同的架构上执行相同的操作,汇编语言和分配的机器码也可能有很大的不同。假设你想在三种不同的处理器架构上执行一个指令,将十进制数1移动到第一个寄存器。即使操作本身是相同的,指令编码和汇编语言也取决于架构。以以下三种一般架构类型为例:
Armv8-A: 64-Bit Instruction Set (AArch64)
d2 80 00 20 mov x0, #1 // move value 1 into register r0
Armv8-A: 32-Bit Instruction Set (AArch32)
e3 a0 00 01 mov r0, #1 // move value 1 into register r0
Intel x86-64 Instruction Set
b8 01 00 00 00 mov rax, 1 // move value 1 into register rax
- Not only is the syntax different, but also the corresponding machine code bytes differ significantly between different instruction sets. This means that machine code bytes assembled for the Arm 32-bit instruction set have an entirely different meaning on an architecture with a different instruction set (such as x64 or A64).
不仅语法不同,而且对应的机器代码字节在不同的指令集之间也有很大的不同。这意味着为Arm 32位指令集组装的机器代码字节在具有不同指令集的架构上具有完全不同的含义(如x64或A64)。
- The same is true in reverse. The same sequence of bytes can have significantly different interpretations on different processors, for example:
反过来也是如此。同一序列的字节在不同的处理器上可以有截然不同的解释,例如:
Armv8-A: 64-Bit Instruction Set (AArch64)
d2 80 00 20 mov x0, #1 // move value 1 into register x0
Armv8-A: 32-Bit Instruction Set (AArch32)
d2 80 00 20 addle r0, r0, #32 // add value 32 to r0 if LE = true
-
In other words, our assembly program needs to be written in the assembly language of the architecture we want it to run on and must be assembled with an assembler that supports this instruction set.
-
Perhaps counterintuitively, however, it is possible to create Arm binaries without using an Arm machine. The assembler itself will need to know about the Arm syntax, of course, but if that assembler is itself compiled for x64, then running it on an x64 machine will let you create Arm binaries. This is called a
cross-assembler
and allows you to assemble your code for a different target architecture than the one you are currently working on.
或许有些违反直觉,但是不使用Arm机器也可以创建Arm二进制文件。当然,汇编器本身需要知道Arm的语法,但如果该汇编器本身是为x64编译的,那么在x64机器上运行它将允许你创建Arm二进制文件。这被称为
交叉汇编器(cross-assembler)
,允许你为与你当前工作的架构不同的目标架构汇编你的代码。
- For example, you can download an assembler for AArch32 on an x86-64 Ubuntu machine and assemble your code from there.
例如,您可以在x86-64 Ubuntu机器上下载一个AArch32的汇编器,并从那里汇编您的代码。
user@ubuntu:~$ arm-linux-gnueabihf-as myasm.s -o myasm.o
user@ubuntu:~$ arm-linux-gnueabihf-ld myasm.o -o myasm
- Using the Linux command “file,” we can see that we created a 32-bit ARM executable file.
使用Linux的“file”命令,我们可以看到我们创建了一个32位的ARM可执行文件。
user@ubuntu:~$ file myasm
myasm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
statically linked, not stripped
High-Level Languages
- So, why has assembly language not become the dominant programming language for writing software? One major reason is that assembly language is not portable. Imagine having to rewrite your entire application codebase for each processor architecture you want to support! That’s a lot of work. Instead, newer languages have evolved that abstract such processor-specific details away, allowing the same program to be easily compiled for multiple different architectures. These languages are often called higher-level languages, in contrast to the low-level language of assembly that is closer to the hardware and architecture of a specific computer.
那么,为什么汇编语言没有成为编写软件的主要编程语言呢?一个主要原因是汇编语言不具有可移植性。想象一下,为了支持每一种处理器架构,你必须重写整个应用程序代码库!这是一项繁重的工作。相反,新的语言已经演变出来,它们将这种处理器特定的细节进行了抽象,允许同一个程序轻松地为多种不同的架构进行编译。与接近特定计算机的硬件和架构的汇编的低级语言相比,这些语言通常被称为高级语言。
- The term high-level here is inherently relative. Originally, C and C++ were considered high-level languages, and assembly was considered the low-level language. Since newer, more abstract languages have emerged, such as Visual Basic or Python, C/C++ is often referred to as low-level. Ultimately, it depends on the perspective and who you ask.
这里的“高级”一词本质上是相对的。最初,C和C++被视为高级语言,而汇编被视为低级语言。但随着如Visual Basic或Python这样的更抽象的新语言的出现,C/C++经常被称为低级语言。最终,这取决于视角和你问的人。
- As with assembly language, processors do not understand high-level source code directly. Programmers need to convert their high-level programs into machine code using a compiler. As before, we still need to specify which architecture the binary will run on, and as before we can create Arm-binaries from non-Arm systems by making use of a cross-compiler.
与汇编语言一样,处理器并不直接理解高级源代码。程序员需要使用编译器将他们的高级程序转换为机器代码。与以前一样,我们仍然需要指定二进制文件将在哪个架构上运行,同样,我们可以通过使用交叉编译器在非Arm系统上创建Arm二进制文件。
- The output of a compiler is typically an executable file that can be run on a given operating system, and it is these binary executable files, rather than the source code of the program, that are typically distributed to customers. For this reason, often when we want to analyze a program, all we have is the compiled executable file itself.
编译器的输出通常是一个可在给定操作系统上运行的可执行文件,而这些二进制可执行文件,而不是程序的源代码,通常是分发给客户的。因此,当我们想要分析一个程序时,我们通常只有已编译的可执行文件本身。
- Unfortunately for reverse engineers, it is usually not possible to reverse the compilation process back to the original source code. Not only are compilers hideously complex programs with many layers of iteration and abstraction between the original source code and the resulting binary, but also many of these steps drop the human-readable information that makes the program easy for programmers to reason about.
对于逆向工程师来说,通常不可能将编译过程逆向回原始源代码。不仅编译器是复杂的程序,从原始源代码到最终的二进制文件之间有许多迭代和抽象的层次,而且许多这些步骤放弃了使程序易于程序员理解的人类可读的信息。
- Without the source code of the software we want to analyze, we have broadly two options depending on the level of detail our analysis requires: decompiling or disassembling the executable file.
如果我们没有想要分析的软件的源代码,我们大致上有两个选项,取决于我们的分析所需的细节程度:反编译或反汇编可执行文件。
Disassembling
- The process of disassembling a binary includes reconstructing the assembly instructions that the binary would run from their machine-code format into a human-readable assembly language. The most common use cases for disassembly include malware analysis, validation of compiler performance and output accuracy, and vulnerability analysis and exploit or proof-of-concept development against defects in closed-source software.
反汇编二进制文件的过程包括将二进制文件会运行的汇编指令从它们的机器代码格式重建成人类可读的汇编语言。反汇编的最常见用例包括恶意软件分析、验证编译器的性能和输出准确性,以及针对闭源软件中的缺陷进行漏洞分析和开发利用或概念验证。
- Of these, exploit development is perhaps the most sensitive to needing analysis of the actual assembly code. Where vulnerability discovery can often be done with techniques such as fuzzing, building exploits from detected crashes or discovering why certain areas of code are not being reached by fuzzers often requires significant assembly knowledge.
其中,利用开发可能是最需要分析实际汇编代码的。尽管漏洞发现通常可以通过模糊测试等技术完成,但从检测到的崩溃中构建利用,或发现为什么某些代码区域不被模糊器触及,通常都需要深入的汇编知识。
- Here, intimate knowledge of the exact conditions of the vulnerability by reading assembly code is critical. The exact choices of how compilers allocate variables and data structures are often critical to developing exploits, and it is here that in-depth assembly knowledge truly is required. Often a seemingly “unexploitable” vulnerability might, in fact, be exploitable with a bit of creativity and hard work invested in truly understanding the inner mechanics of how a vulnerable function works.
在这里,通过阅读汇编代码对漏洞的确切情况进行深入了解是至关重要的。编译器如何分配变量和数据结构的确切选择通常对开发利用至关重要,这是真正需要深入的汇编知识的地方。经常有一种看似“不可利用”的漏洞实际上可能在投入一些创意和努力真正理解一个易受攻击的功能的内部机制后,是可利用的。
- Disassembling an executable file can be done in multiple ways, and we will look at this in more detail in the second part of this book. But, for now, one of the simplest tools to quickly look at the disassembly output of an executable file is the Linux tool
objdump
6.
反汇编一个可执行文件可以通过多种方式完成,我们将在本书的第二部分详细讨论这个问题。但是,目前最简单的工具来快速查看可执行文件的反汇编输出是Linux工具
objdump
.
- Let’s compile and disassemble the following
write()
program:
让我们编译并反汇编以下的
write()
程序:
#include <unistd.h>
int main(void) {
write(1, "Hello!\n", 7);
}
- We can compile this code with GCC and specify the
-c
option. This option tells GCC to create the object file without invoking the linking process, so we can then runobjdump
on just our compiled code without seeing the disassembly of all the surrounding object files such as a C runtime. The disassembly output of the main function is as follows:
我们可以使用GCC编译这段代码,并指定
-c
选项。这个选项告诉GCC在不调用链接过程的情况下创建对象文件,这样我们就可以在不查看所有周围对象文件(如C运行时)的反汇编的情况下运行objdump
。main函数的反汇编输出如下:
user@arm32:~$ gcc -c hello.c
user@arm32:~$ objdump -d hello.o
Disassembly of section .text:
00000000 <main>:
0:b580 push{r7, lr}
2:af00 addr7, sp, #0
4:2207 movsr2, #7
6:4b04 ldrr3, [pc, #16]; (18 <main+0x18>)
8:447b addr3, pc
a:4619 movr1, r3
c:2001 movsr0, #1
e:f7ff fffe bl0 <write>
12:2300 movsr3, #0
14:4618 movr0, r3
16:bd80 pop{r7, pc}
18:0000000c .word0x0000000c
While Linux utilities like objdump
are useful for quickly disassembling small programs, larger programs require a more convenient solution. Various disassemblers exist to make reverse engineering more efficient, ranging from free open source tools, such as Ghidra
7, to expensive solutions like IDA Pro
8. These will be discussed in the second part of this book in more detail.
尽管像
objdump
这样的Linux工具对于快速反汇编小程序很有用,但较大的程序需要一个更加便利的解决方案。为了使逆向工程更加高效,存在各种反汇编器,从免费的开源工具如Ghidra
,到昂贵的解决方案如IDA Pro
。这些将在本书的第二部分中详细讨论。
Decompilation
- A more recent innovation in reverse engineering is the use of decompilers. Decompilers go a step further than disassemblers. Where disassemblers simply show the human-readable assembly code of the program, decompilers try to regenerate equivalent C/C++ code from a compiled binary.
在逆向工程中,一个较新的创新是使用反编译器。反编译器比反汇编器更进一步。反汇编器只显示程序的人类可读的汇编代码,而反编译器试图从已编译的二进制文件重新生成等效的C/C++代码。
- One value of decompilers is that they significantly reduce and simplify the disassembled output by generating pseudocode. This can make it easier to read when skimming over a function to see at a broad-strokes level what the program is up to.
反编译器的一个价值在于它们通过生成伪代码大大减少和简化了反汇编输出。这可以使其在浏览一个函数时更容易阅读,从而大致了解程序的操作情况。
- The flipside to this, of course, is that important details can also get lost in the process. Additionally, since compilers are inherently lossy in their conversion from source code to executable file, decompilers cannot fully reconstruct the original source code. Symbol names, local variables, comments, and much of the program structure are inherently destroyed by the compilation process. Similarly, attempts to automatically name or relabel local variables and parameters can be misleading if storage locations are reused by an aggressively optimizing compiler.
当然,这种方式的另一面是在过程中也可能丢失重要的细节。另外,由于编译器在将源代码转换为可执行文件时本质上会有损失,所以反编译器不能完全重构原始的源代码。符号名称、局部变量、注释以及程序的很多结构在编译过程中都会被破坏。同样地,如果一个积极优化的编译器重用存储位置,那么尝试自动命名或重新标记局部变量和参数可能会产生误导。
- Let’s look at an example C function, compile it with GCC, and then decompile it with both IDA Pro’s and Ghidra’s decompilers to show what this looks like in practice.
让我们看一个C函数的示例,使用GCC编译它,然后使用IDA Pro和Ghidra的反编译器来反编译它,以实际展示这在实践中是什么样子。
- Figure 1.7 shows a function called
file_record
in theihex2fw.c
9 file from the Linux source code repository.
图1.7展示了来自Linux源代码仓库中的
ihex2fw.c
文件中名为file_record
的函数。
- After compiling the C file on an Armv8-A architecture (without any specific compiler options) and loading the executable file into IDA Pro 7.6, Figure 1.8 shows the pseudocode for the previous function generated by the decompiler.
在Armv8-A架构上编译C文件(没有任何特定的编译器选项)并将可执行文件加载到IDA Pro 7.6后,图1.8显示了反编译器为前一个函数生成的伪代码。
- In Figure 1.9 you can see the same function decompiled by Ghidra 10.0.4.
在图1.9中,您可以看到由Ghidra 10.0.4反编译的同一个函数。
- In both cases we can sort of see the ghost of the original code if we squint hard enough at it, but the code is vastly less readable and far less intuitive than the original. In other words, while there are certainly many cases when decompilers can give us a quick high-level overview of a program, it is certainly no panacea and is no substitute for being able to dive in to the assembly code of a given program.
在这两种情况下,如果我们努力地仔细观察,我们可以隐约地看到原始代码的影子,但这些代码的可读性大大降低,远不如原始代码直观。换句话说,尽管确实有许多情况下反编译器可以为我们提供一个程序的快速高级概述,但它绝不是灵丹妙药,也不能替代深入研究给定程序的汇编代码的能力。
- That said, decompilers are constantly evolving and are becoming better at reconstructing source code, especially for simple functions. Using decompiler output of functions you want to reverse engineer at a higher level is a useful aid, but don’t forget to peek into the disassembly output when you are trying to get a more in-depth view of what’s going on.
话虽如此,反编译器还在不断发展,并且在重构源代码方面变得越来越好,尤其是对于简单的函数。使用你想要进行更高级反向工程的函数的反编译器输出是一个有用的辅助工具,但当你试图深入了解发生了什么时,不要忘记查看反汇编输出。
Planning a Computer System, Project Stretch, McGraw-Hill Book Company, Inc., 1962. (http://archive.computerhistory.org/resources/text/IBM/Stretch/pdfs/Buchholz_102636426.pdf) ↩︎
https://ftp.gnu.org/old-gnu/Manuals/gas-2.9.1/html_chapter/as_7.html ↩︎
https://gitlab.arm.com/linux-arm/linux-dm/-/blob/56299378726d5f2ba8d3c8cbbd13cb280ba45e4f/firmware/ihex2fw.c ↩︎