Computing 101

Computing 101

Your First Program

1.学习了汇编指令mov,给寄存器赋值

2.系统调用syscall,系统调用的情况由对应的ax寄存器的值确定

3.系统调用参数传递,rdi转递第一个参数

4.构建一个二进制文件

  1. step_1: 用.s语法写一个文件
1
2
3
4
.intel_syntax noprefix#告诉编译器使用的intel语法,不需要在每条命令前都加上额外的前缀
mov rdi, 42
mov rax, 60
syscall
  1. step_2: 用as命令将二进制文件组装成可执行的对象文件
1
2
3
4
5
6
7
8
9
10
11
hacker@dojo:~$ ls
asm.s
hacker@dojo:~$ cat asm.s
.intel_syntax noprefix
mov rdi, 42
mov rax, 60
syscall
hacker@dojo:~$ as -o asm.o asm.s
hacker@dojo:~$ ls
asm.o asm.s
hacker@dojo:~$

as工具读取asm.s文件,将其汇编成二进制代码,并输出一个名为asm.o的对象文件,它包含了实际的汇编二进制代码,但是还不能直接运行,我们要将它链接起来

  1. step_3: 使用ld命令将一个或多个可执行的对象文件链接成最终的可执行文件
1
2
3
4
5
6
7
hacker@dojo:~$ ls
asm.o asm.s
hacker@dojo:~$ ld -o exe asm.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
hacker@dojo:~$ ls
asm.o asm.s exe
hacker@dojo:~$

这时已经创建了一个名为exe的可执行文件

1
2
3
#这里的第三步可以看到有一个没找到_start symbol的标识
#在代码中没有设定_start标识程序就会从最开始开始执行
#所以我们可以修改成如下代码,就可以避免报错的弹出
1
2
3
4
5
6
.intel_syntax noprefix
.global _start
_start:
mov rdi, 42
mov rax, 60
syscall

其实就是多了两行,第二行是添加了 _start的标签,指向代码的开头,第一行的.global _start指示链接器将 _start标签设置为全局可见,而不仅仅是对象文件级局部可见

由于 ld 是链接器,这个指令对于 _start 标签被识别是必要的

5.就是详细介绍了mov,也可以在两个寄存器之间使用

Software Introspection

1.strace 系统调用追踪器

范例:

1
2
3
4
5
hacker@dojo:~$ strace /tmp/your-program
execve("/tmp/your-program", ["/tmp/your-program"], 0x7ffd48ae28b0 /* 53 vars */) = 0
exit(42) = ?
+++ exited with 42 +++
hacker@dojo:~$

strace 指明了触发了什么系统调用,传递的参数是什么

输出语法为system_call(parameter,parameter,parameter…….)

例子中报告了两个系统调用,第二个是程序请求终止自生的exit的系统调用,你可以看到给它传递的参数42,一个是execve的系统调用,它启动一个新程序,但是并不是由这个程序调用的,strace检测到它是由于strace的独特工作机制导致的

最后可以看到exit(42),以退出码42退出!

exit的系统调用很容易在不使用strace的条件下使用,因为exit的一个目的就是像你提供一个你可以访问的退出码

这道题目strace的是alarm

2.gdb的启动

3.gdb的指令:starti启动程序

Computer Memory

1.[addr]标识地址的内容

2.巩固联系

3.寄存器中存地址并访问获取该地址的值

4.访问寄存器中的地址的一定偏移量的值

5.解指针的一些操作,具体如下

1
2
3
4
5
6
7
.intel_syntax noprefix
.global _start
_start:
mov rdi, [567800]
mov rdi, [rdi]
mov rax, 60
syscall

6.综合运用

7.寄存器三重解引用

Hello Hackers

1.write的系统调用

参数:

1
write(file_descriptor, memory_address, number_of_characters_to_write)

参数传递:

1
2
3
rdi:file_descriptor
rsi:memory_address
rdx:number_of_characters_to_write

文件描述符回顾:

1
2
3
#FD 0:标准输入是进程接收输入的通道。例如,你的 shell 使用标准输入来读取你输入的命令。
#FD 1:标准输出是进程输出正常数据的通道,比如在之前的挑战中打印给你的标志,或者像 ls 这样的工具的输出。
#FD 2:标准错误是进程输出错误信息的通道。例如,如果你输入了一个错误的命令,shell 会通过标准错误输出“该命令不存在”的信息。

2.多次系统调用

答案:

1
2
3
4
5
6
7
8
9
10
11
.intel_syntax noprefix
.global _start
_start:
mov rdi, 1
mov rsi, 1337000
mov rdx, 1
mov rax, 1
syscall
mov rdi, 42
mov rax, 60
syscall

4.read的系统调用

答案;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.intel_syntax noprefix
.global _start
_start:
mov rdi, 0
mov rsi, 1337000
mov rdx, 8
mov rax, 0
syscall
mov rax, 1
mov rdi, 1
mov rdx, 8
mov rsi, 1337000
syscall
mov rax, 60
mov rdi, 42
syscall

Assembly Crash Course

1.set-register

1
/challenge/run

开始挑战

1
2
3
4
5
6
from pwn import *
context.arch='amd64'
p=process('/challenge/run')
p.recvline()
p.send(asm('mov rdi,0x1337'))
print(p.readallS())

2.设置多个寄存器的值

3.寄存器的数学运算

4.寄存器的数学运算

mul[无符号乘法]和imul[有符号乘法]

5.整数除法向下取整

div是一个较为复杂的运算操作

1
div reg

等效于:

1
2
rax = rax:rdx / reg
rdx = 余数

其中 rdx表示128位数的高64位 , rax表示128位数的低64位

6.取模运算

7.寄存器的结构

1
2
3
4
5
6
7
8
9
10
MSB                                    LSB
+----------------------------------------+
| rax |
+--------------------+-------------------+
| eax |
+---------+---------+
| ax |
+----+----+
| ah | al |
+----+----+

小技巧:

当除数是2的n次方时,余数就是被除数的低n位

错误示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

context.arch = 'amd64'
p = process('/challenge/run')

sleep(0.5)

p.send(asm('''
mov rax, al
mov rbx, ax
'''))

print(p.recvallS())

报错虽然报一些不太相干的原因,但是可能就是pwn.college上的模块不支持这种操作

正确解答:

1
2
3
4
5
6
7
8
9
10
from pwn import *
context.arch='amd64'

p=process('/challenge/run')
p.recvline()
p.send(asm('''
mov al, dil
mov bx, si
'''.strip()))
print(p.recvallS())

shl 左移动位

1
shl al, 1
1
rax: 10001010
1
rax: 00010100#修改后

位移操作的一个好处是可以快速实现乘法(乘以 2)或除法(除以 2),还可以用于计算取模。

1
shr reg1, reg2

将reg1向右移动reg2 中指定的位数

在代码后加上.strip()避免不必要的错误

位与操作:两个值都为1才是1,否则为0

我觉得有点奇怪

按照题目的意思可以以rax为中间寄存器去操作存储and rdi rsi 的值

也就是

1
2
and rax, rsi
and rax, rdi

题目的意思貌似就是这样rax的值就是rsi和rdi的与操作的值

1
2
and rsi, rdi
mov rax, rsi

个人觉得题目可能有些问题,我觉得成立情况应该是rax为2的n次方减一才能对这些寄存器实现

这里我还参考了别人的博客:Assembly Crash Course-pwn.college(持续更新) - 零夜blog

其中level10就是这题,他给的题解是:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context.arch='amd64'

p=process('/challenge/run')
p.recvline()
p.send(asm('''
and rdi, rsi
xor rax, rax
or rax, rdi
'''))
print(p.recvallS())

xor清空了rax,所以rax的所有位都是0,这时候or rax, rdi其实就等效于mov rax,rdi

所以也可以看出这两段代码效果是相同的

1
2
xor rax, rax
or rax, rdi
1
mov rax, rdi

这样比题目给的更符合逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

context.arch = 'amd64'
p = process('/challenge/run')

p.send(asm('''
xor rax, rax
or rax, 1
and rdi, 1
xor rax, rdi
'''.strip()))

sleep(0.5)

print(p.recvallS())

内存地址的访问和设定

将寄存器中的值写入线性地址

1
mov [0x404000], rax   ; 将 rax 中的值存储到内存地址 0x404000 处

注意,不要理解成将rax中的值存储到内存地址0x404000所存储的地址的位置中

错误示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

context.arch = 'amd64'
p = process('/challenge/run')

p.send(asm('''
mov rax, [0x404000]
add [0x404000], 0x1337
'''.strip()))

sleep(0.5)

print(p.recvallS())

注意,地址里的值不能用立即数赋值,可以通过寄存器赋值

正确示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rom pwn import *

context.arch = 'amd64'
p = process('/challenge/run')

p.send(asm('''
mov rax, [0x404000]
add rbx, [0x404000]
add rbx, 0x1337
mov [0x404000], rbx
'''.strip()))

sleep(0.5)

print(p.recvallS())
1
2
3
4
双字(Quad Word)= 8 字节 = 64 位
双字(Double Word)= 4 字节 = 32 位
字(Word)= 2 字节 = 16 位
字节(Byte)= 1 字节 = 8 位

在进行类似如下这种操作时,不会清空寄存器的高位

1
mov al, [0x404000]

同上操作,但是增加了16位,32位,64位

**小端序:**存储数据的情况和我们看到的实际上是相反的

但是这个题目和大小端序没什么关系,就是给地址送值,但是需要注意,给地址送入比较大的值时,不能直接进行操作,而是要通过寄存器来协助,具体原因如下

  1. 指令集限制
  • x86 汇编指令集的设计中,直接将一个较大的常量(例如 64 位常量)写入内存地址的操作可能会受到指令长度和编码的限制。
  • 汇编指令的编码空间有限,直接将一个大常量写入内存可能需要额外的指令来实现,而不是一条简单的指令。
  1. 操作数大小限制
  • x86 架构支持多种操作数大小(如 8 位、16 位、32 位和 64 位)。直接将一个大常量写入内存时,需要明确操作数的大小。
  • 如果直接使用类似 mov [rdi], 0xdeadbeef00001337 的指令,可能会导致指令过长或无法正确解析。
  1. 寄存器间接寻址的限制
  • 在 x86 架构中,寄存器间接寻址(如 [rdi])通常用于访问内存,但直接将一个大常量赋值给内存地址可能需要先将常量加载到寄存器中,然后再通过寄存器间接寻址写入内存。

连续读取内存

这里有一个小技巧,题目要求我们除以4,实际上可以shr rax,2来做到同样的效果

介绍栈及其特性(FILO)

通过栈交换寄存器的值

通过栈顶指针esp读取写入内存

跳转学习

对于所有跳转,有三种类型:

  • 相对跳转:相对于下一条指令向前或向后跳转。
  • 绝对跳转:跳转到一个特定的地址。
  • 间接跳转:跳转到寄存器中指定的内存地址。

这一关是绝对跳转jmp reg

相对跳转

这里还介绍了一种便于重复的汇编指令

1
2
3
.rept num
assembly
.endr

重复num遍assembly的指令

jmp sign

跳转到标签sign处

绝对跳转和相对跳转交换使用

条件跳转

x86 中有许多跳转类型,学习它们的使用方法会很有帮助。几乎所有跳转指令都依赖于一个叫做 ZF(Zero Flag,零标志位)的东西。当 cmp 的结果相等时,ZF 被设置为 1,否则为 0。

1
2
cmp reg ,some_values
je sign

如果 reg 和 some_values 相等,ZF标志位则会设置为1,否则为0,而ZF标志位设置为1,则可进行后续的条件跳转,否则不进行

间接跳转:

地址表,减少cmp的使用

题解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

context.arch = 'amd64'
p = process('/challenge/run')

p.send(asm('''
cmp rdi, 4
jae default
jmp [rsi + rdi * 8]
jmp end
default:
jmp [rsi + 4 * 8]
end:
nop
'''.strip()))
sleep(0.5)

print(p.recvallS())

题解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

context.arch = 'amd64'
p = process('/challenge/run')

p.send(asm('''
mov rax, 0x00
mov rbx, 0x00
mov rcx, 0x00
mov rdx, 0x00
start:
cmp rax, rsi
je end
add rbx, [rdi + 8 * rax]
add rax, 0x01
jmp start
end:
mov rax, rbx
div rsi
'''.strip()))
sleep(0.5)

print(p.recvallS())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

context.arch = 'amd64'
p = process('/challenge/run')

p.send(asm('''
mov rax, 0x00
mov rbx, 0x00
mov rcx, 0x00
mov rdx, 0x00
start:
cmp rdi, rdx
je end
cmp [rdi + rax], rbx
je end
add rax, 0x01
jmp start
end:
nop
'''.strip()))
sleep(0.5)

print(p.recvallS())

call指令和ret指令:

1


Computing 101
http://example.com/Computing 101/
作者
briteny-pwn
发布于
2025年3月6日
许可协议