本文最后更新于:2024年3月18日 下午
汇编,太美丽辣
![](/image/pwn-college.png)
总结
- 用python的pwn库解题会非常方便。建议读读pwn库的官方文档,写的已经非常清楚了
- pwntools太好用了
- 顺便发现了readallS的输出比readall更友好
- 总的来说学习了基本的汇编程序的编写(赋值、运算、分支、循环等基本操作),以及label、rept之流的运用,栈内存布局等
1
设置寄存器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 要与任何级别进行交互,您需要通过标准输入(stdin)发送原始字节给该程序。 为了高效地解决这些问题,首先运行程序以查看挑战说明。 然后编写、汇编并将字节传输到该程序。
例如,如果您在 asm.S 文件中编写了汇编代码,可以将其汇编为目标文件: as -o asm.o asm.S
然后,您可以将 .text 段(您的代码)复制到 asm.bin 文件中: objcopy -O binary --only-section=.text asm.o asm.bin
最后,将其发送给挑战程序: cat ./asm.bin | /challenge/run
您甚至可以将其作为一个命令运行: as -o asm.o asm.S && objcopy -O binary --only-section=.text ./asm.o ./asm.bin && cat ./asm.bin | /challenge/run
在这个级别中,您将使用寄存器。您将被要求修改或读取寄存器的内容。
在这个级别中,您将使用寄存器!请设置以下内容: rdi = 0x1337
请以字节形式提供您的汇编代码(最多0x1000字节):
|
汇编-》字节码的过程
题目提示可以使用as命令得到程序的text的节段,然后用16进制编辑器查看,但是还是pwn库比较方便
1 2 3 4 5 6 7 8 9 10 11 12 13
|
from pwn import *
context.arch='amd64'
p=process('/challenge/run')
p.recvline()
p.send(asm('mov rdi,0x1337'))
print(p.readallS())
|
2
设置多个寄存器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 欢迎来到 ASMLevel2 ==================================================
要与任何关卡进行交互,您需要通过标准输入将原始字节发送给该程序。 为了有效地解决这些问题,首先运行程序以查看挑战说明。 然后编写、汇编并将字节传输到该程序。
例如,如果您将汇编代码编写在 asm.S 文件中,可以将其汇编为目标文件: as -o asm.o asm.S
然后,您可以将 .text 段(您的代码)复制到 asm.bin 文件中: objcopy -O binary --only-section=.text asm.o asm.bin
最后,将其发送到挑战程序: cat ./asm.bin | /challenge/run
您甚至可以将其作为一个命令运行: as -o asm.o asm.S && objcopy -O binary --only-section=.text ./asm.o ./asm.bin && cat ./asm.bin | /challenge/run
在这个关卡中,您将使用寄存器。您将被要求修改或读取寄存器的值。
在这个关卡中,您将使用多个寄存器。请设置以下值: rax = 0x1337 r12 = 0xCAFED00D1337BEEF rsp = 0x31337
|
类似上面,给shellcode追加点asm即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm('mov rax, 0x1337') shellcode += asm('mov r12, 0xCAFED00D1337BEEF') shellcode += asm('mov rsp, 0x31337')
print(shellcode)
pause()
p.send(shellcode)
print(p.readallS())
|
3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 为了与任何级别进行交互,你需要通过标准输入(stdin)发送原始字节给这个程序。 为了高效地解决这些问题,首先运行程序以查看挑战说明。 然后编写、汇编并将字节发送给该程序。
例如,如果你将汇编代码写在 asm.S 文件中,可以将其汇编为目标文件: as -o asm.o asm.S
然后,将 .text 段(你的代码)复制到 asm.bin 文件中: objcopy -O binary --only-section=.text asm.o asm.bin
最后,将其发送给挑战程序: cat ./asm.bin | /challenge/run
你甚至可以将其作为一个命令运行: as -o asm.o asm.S && objcopy -O binary --only-section=.text ./asm.o ./asm.bin && cat ./asm.bin | /challenge/run
在这个级别中,你将使用寄存器进行操作。你将被要求修改或读取寄存器的值。
我们将在每次运行之前动态设置一些值到内存中。每次运行时这些值会发生变化。这意味着你需要对寄存器进行某种形式的计算操作。我们会告诉你哪些寄存器被设置以及你应该将结果放在哪里。在大多数情况下,结果应该放在 rax 寄存器中。
x86 指令集中存在许多可以对寄存器和内存执行常规数学运算的指令。
简写形式中,当我们说 A += B 时,实际上是 A = A + B。
以下是一些有用的指令: add reg1, reg2 <=> reg1 += reg2 sub reg1, reg2 <=> reg1 -= reg2 imul reg1, reg2 <=> reg1 *= reg2
除法(div)更复杂,我们稍后会讨论它。 注意:所有的 'regX' 都可以替换为常数或内存位置。
请执行以下操作: 将 0x331337 加到 rdi 中
现在,我们将进行以下设置以准备你的代码: rdi = 0x937
|
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.recvline()
shellcode = asm('add rdi, 0x331337')
p.send(shellcode)
print(p.readallS())
|
4
两个寄存器相乘,加上另一个寄存器,然后结果赋值给rax
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' imul rdi,rsi mov rax,rdi add rax,rdx''')
p.send(shellcode)
print(p.readallS())
|
5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 在这个级别中,你将使用寄存器进行操作。你将被要求修改或读取寄存器的值。
我们将在每次运行之前动态设置一些值到内存中。每次运行时这些值会发生变化。这意味着你需要对寄存器进行某种形式的计算操作。我们会事先告诉你哪些寄存器已经设置,并且你应该将结果放在哪里。在大多数情况下,结果应该放在 rax 寄存器中。
在 x86 中,除法与普通数学中的除法有所不同。这里的数学称为整数运算。这意味着每个值都是一个整数。
举个例子:10 / 3 = 3 在整数运算中。
为什么呢?
因为 3.33 被舍入为一个整数。
在这个级别中,与除法相关的指令是: mov rax, reg1
注意:div 是一条特殊的指令,可以将一个 128 位的被除数除以一个 64 位的除数,并且只使用一个寄存器作为操作数来存储商和余数。
这个复杂的 div 指令如何工作并操作一个 128 位的被除数(它是寄存器大小的两倍)?
对于指令:div reg,以下操作发生: rax = rdx:rax / reg rdx = 余数
rdx:rax 表示 rdx 是 128 位被除数的高 64 位,rax 是 128 位被除数的低 64 位。
在调用 div 之前,你必须小心 rdx 和 rax 中的内容。
请计算以下内容: speed = distance / time,其中: distance = rdi time = rsi speed = rax
注意,distance 最多是一个 64 位的值,所以在除法运算时 rdx 应该为 0。
现在,我们将进行以下设置以准备你的代码: rdi = 0x1d8c rsi = 0x40
|
计算speed = distance / time
- distance 最多是一个 64 位的值,所以在除法运算时 rdx 应该为 0
- rax = rdx:rax / reg ; rdx = 余数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' xor rdx , rdx mov rax,rdi div rsi ''')
p.send(shellcode)
print(p.readallS())
|
6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 在这个级别中,你将使用寄存器进行操作。你将被要求修改或读取寄存器的值。
我们将在每次运行之前动态设置一些值到内存中。每次运行时这些值会发生变化。这意味着你需要对寄存器进行某种形式的计算操作。我们会事先告诉你哪些寄存器已经设置,并且你应该将结果放在哪里。在大多数情况下,结果应该放在 rax 寄存器中。
在汇编中,模运算是另一个有趣的概念!
x86 允许你在除法运算后获取余数。
例如:10 / 3 -> 余数 = 1
余数与模运算相同,模运算也称为 "mod" 运算符。
在大多数编程语言中,我们用符号 '%' 表示模运算。
请计算以下内容: rdi % rsi
将结果放入 rax 寄存器中。
现在,我们将进行以下设置以准备你的代码: rdi = 0xc799858 rsi = 0x3f
|
由于rax = rdx:rax / reg ; rdx = 余数
因此rax=rax-rdx即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' mov rax, rdi div rsi mov rax, rdx ''')
p.send(shellcode)
print(p.readallS())
|
7
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 在这个级别中,你将使用寄存器进行操作。你将被要求修改或读取寄存器的值。
我们将在每次运行之前动态设置一些值到内存中。每次运行时这些值会发生变化。这意味着你需要对寄存器进行某种形式的计算操作。我们会事先告诉你哪些寄存器已经设置,并且你应该将结果放在哪里。在大多数情况下,结果应该放在 rax 寄存器中。
x86 中的另一个很酷的概念是能够独立访问低位寄存器字节。
在 x86_64 中,每个寄存器的大小为 64 位,在之前的级别中,我们使用 rax、rdi 或 rsi 来访问整个寄存器。
我们还可以使用不同的寄存器名称来访问每个寄存器的低位字节。
例如,可以使用 eax 来访问 rax 的低 32 位,使用 ax 来访问低 16 位,使用 al 来访问低 8 位。
MSB LSB +----------------------------------------+ | rax | +--------------------+-------------------+ | eax | +---------+---------+ | ax | +----+----+ | ah | al | +----+----+
几乎所有寄存器都可以访问低位寄存器字节。
请使用一条 move 指令将 ax 寄存器的高 8 位设置为 0x42。
现在,我们将进行以下设置以准备你的代码: rax = 0xee4491be45a500d8
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' mov ah, 0x42 ''')
p.send(shellcode)
print(p.readallS())
|
8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 事实证明,使用 div 运算符计算模运算是很慢的!
我们可以使用一个数学技巧来优化模运算(%)。编译器经常使用这个技巧。
如果我们有 "x % y",并且 y 是 2 的幂,比如 2^n,那么结果将是 x 的低 n 位。
因此,我们可以使用低位寄存器字节访问来高效地实现模运算!
只能使用以下指令: mov
请计算以下内容: rax = rdi % 256 rbx = rsi % 65536
现在,我们将进行以下设置以准备你的代码: rdi = 0x432e rsi = 0x97d4bdd6
|
- 256是8次方,65536是16次方
- 对应rdi的低八位和rsi的低16位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' mov al, dil mov bx, si ''')
p.send(shellcode)
print(p.readallS())
|
9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 在这个级别中,你将使用位逻辑和操作。这将涉及直接与寄存器或内存位置中存储的位进行交互的操作。你还可能需要使用 x86 中的逻辑指令:and、or、not、xor。
在汇编中移动位是另一个有趣的概念!
x86 允许你在寄存器中“移动”位。
以 al 为例,它是 rax 的最低的 8 位。
al 中的值(以位表示)为: rax = 10001010
如果我们使用 shl 指令向左移动一次: shl al, 1
新的值为: al = 00010100
所有位向左移动,最高位掉落,同时在右侧添加了一个新的 0。
你可以利用这一点对你关心的位进行特殊操作。
移位还具有快速乘法(乘以 2)或除法(除以 2)的好处,并且还可以用于计算模运算。
以下是重要的指令: shl reg1, reg2 <=> 将 reg1 按 reg2 中的位数向左移动 shr reg1, reg2 <=> 将 reg1 按 reg2 中的位数向右移动 注意:'reg2' 可以由常数或内存位置替代
只能使用以下指令: mov, shr, shl
请执行以下操作: 将 rax 设置为 rdi 的第 5 个最低有效字节。
例如: rdi = | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 | 将 rax 设置为 B4 的值
现在,我们将进行以下设置以准备你的代码: rdi = 0x8902d4d2350b39b0
|
先左移位3次,再右移位7次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' shl rdi,8*3 shr rdi,8*7 mov rax,rdi ''')
p.send(shellcode)
print(p.readallS())
|
10
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| 在这个级别中,你将使用位逻辑和操作。这将涉及直接与寄存器或内存位置中存储的位进行交互的操作。你还可能需要使用 x86 中的逻辑指令:and、or、not、xor。
在汇编中进行位逻辑运算是另一个有趣的概念! x86 允许你对寄存器逐位执行逻辑操作。
为了说明这个例子,假设寄存器只存储 8 位。
rax 和 rbx 中的值为: rax = 10101010 rbx = 00110011
如果我们使用 "and rax, rbx" 指令对 rax 和 rbx 进行按位与运算,结果将通过逐位进行与运算来计算,因此被称为位逻辑。
所以从左到右: 1 AND 0 = 0 0 AND 0 = 0 1 AND 1 = 1 0 AND 1 = 0 ...
最后我们将结果组合在一起得到: rax = 00100010
以下是一些参考的真值表:
AND OR XOR A | B | X A | B | X A | B | X ---+---+--- ---+---+--- ---+---+--- 0 | 0 | 0 0 | 0 | 0 0 | 0 | 0 0 | 1 | 0 0 | 1 | 1 0 | 1 | 1 1 | 0 | 0 1 | 0 | 1 1 | 0 | 1 1 | 1 | 1 1 | 1 | 1 1 | 1 | 0
在不使用以下指令的情况下: mov, xchg
请执行以下操作: rax = rdi AND rsi
即将 rax 设置为 (rdi AND rsi) 的值。
现在,我们将进行以下设置以准备你的代码: rdi = 0x953236d544892262 rsi = 0x57539fb8540f1b4b
|
- rdi和rsi异或之后值被存在rdi中
- rax使用xor清零
- rdi和rax使用或运算,从而将rax被赋值为rdi的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' xor rax,rax and rdi,rsi or rax, rdi ''')
p.send(shellcode)
print(p.readallS())
|
11
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 在这个关卡中,你将使用寄存器进行操作。你将被要求修改或读取寄存器的值。
在每次运行之前,我们将动态地在内存中设置一些值。每次运行时,这些值都会改变。这意味着你需要使用寄存器进行一些公式化的操作。我们会事先告诉你哪些寄存器被设置了,以及你应该把结果放在哪里。在大多数情况下,结果应该存储在rax寄存器中。
在这个关卡中,你将使用位逻辑和操作。这将涉及与存储在寄存器或内存位置中的位直接交互的大量操作。你还可能需要使用x86的逻辑指令:and、or、not、xor。
只能使用以下指令: and、or、xor
实现以下逻辑: 如果x是偶数,则y等于1;否则,y等于0。
其中: x = rdi y = rax
现在,我们将按照以下方式设置参数,以便你编写代码: rdi = 0x3a0610d0
|
- rdi与1与运算可以判断rdi的奇偶
- 把刚刚得到的rdi的最后一位赋值给rax
- rax与1异或即可
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.recvline()
shellcode = asm(''' and rdi,1 xor rax,rax or rax,rdi xor rax,1 ''')
p.send(shellcode)
print(p.readallS())
|
12
地址中数据传输到寄存器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' mov rax, [0x404000] ''')
p.send(shellcode)
print(p.readallS())
|
13
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' mov rax, [0x404000] ''')
p.send(shellcode)
print(p.readallS())
|
14
无法对内存直接进行运算操作,需要寄存器进行中转
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.recvline()
shellcode = asm(''' mov rax,[0x404000] mov rdi,[0x404000] mov rdi,0x1337 add [0x404000],rdi ''')
p.send(shellcode)
print(p.readallS())
|
15
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 回想一下,在x86_64中,寄存器的宽度为64位,意味着它们可以存储64位。
同样,每个内存位置可以被视为一个64位的值。
我们将64位(8字节)的内容称为"quad word"。
下面是内存大小的名称及其对应的字节数: Quad Word = 8字节 = 64位 Double Word = 4字节 = 32位 Word = 2字节 = 16位 Byte = 1字节 = 8位
在x86_64中,当解引用一个地址时,你可以访问每个这些大小,就像使用更大或更小的寄存器访问一样: mov al, [address] <=> 将地址中的最低有效字节移动到rax寄存器 mov ax, [address] <=> 将地址中的最低有效字移动到rax寄存器 mov eax, [address] <=> 将地址中的最低有效双字移动到rax寄存器 mov rax, [address] <=> 将地址中的完整quad word移动到rax寄存器
请执行以下操作: 将rax寄存器设置为0x404000处的字节。
现在,我们将按照以下方式设置参数,以便你编写代码: [0x404000] = 0x1a4b04
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' mov al, [0x404000] ''')
p.send(shellcode)
print(p.readallS())
|
16
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 在这个关卡中,你将使用内存进行操作。这将要求你读取或写入线性存储在内存中的数据。如果你感到困惑,请去看一下"ike"中的线性寻址模块。你可能还会被要求多次解引用,以使用我们动态放置在内存中的数据。
回想一下,以下是内存大小的名称及其对应的字节数: Quad Word = 8字节 = 64位 Double Word = 4字节 = 32位 Word = 2字节 = 16位 Byte = 1字节 = 8位
在x86_64中,当解引用一个地址时,你可以访问每个这些大小,就像使用更大或更小的寄存器访问一样: mov al, [address] <=> 将地址中的最低有效字节移动到rax寄存器 mov ax, [address] <=> 将地址中的最低有效字移动到rax寄存器 mov eax, [address] <=> 将地址中的最低有效双字移动到rax寄存器 mov rax, [address] <=> 将地址中的完整quad word移动到rax寄存器
请执行以下操作: - 将rax寄存器设置为0x404000处的字节。 - 将rbx寄存器设置为0x404000处的字。 - 将rcx寄存器设置为0x404000处的双字。 - 将rdx寄存器设置为0x404000处的quad word。
现在,我们将按照以下方式设置参数,以便你编写代码: [0x404000] = 0x845bcb141d380894
|
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.recvline()
shellcode = asm(''' mov al,[0x404000] mov bx,[0x404000] mov ecx,[0x404000] mov rdx,[0x404000] ''')
p.send(shellcode)
print(p.readallS())
|
17
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 值得注意的是,正如你可能已经注意到的,值以与我们表示的顺序相反的方式存储。
举个例子,假设: [0x1330] = 0x00000000deadc0de
如果你检查它在内存中的实际表示,你会看到: [0x1330] = 0xde [0x1331] = 0xc0 [0x1332] = 0xad [0x1333] = 0xde [0x1334] = 0x00 [0x1335] = 0x00 [0x1336] = 0x00 [0x1337] = 0x00
在x86中,以这种"反向"存储的格式存储数据是有意义的,这被称为"Little Endian"。
在这个挑战中,我们会在每次运行时给你两个动态创建的地址。
第一个地址将被放置在rdi寄存器中。 第二个地址将被放置在rsi寄存器中。
根据前面提到的信息,执行以下操作:
- 将[rdi]设置为0xdeadbeef00001337。 - 将[rsi]设置为0xc0ffee0000。
提示:可能需要一些技巧将一个大常数赋值给解引用的寄存器。 尝试将一个寄存器设置为常数值,然后将该寄存器赋值给解引用的寄存器。
现在,我们将按照以下方式设置参数,以便你编写代码: [0x404208] = 0xffffffffffffffff [0x404c38] = 0xffffffffffffffff rdi = 0x404208 rsi = 0x404c38
|
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.recvline()
shellcode = asm(''' mov rax,0xdeadbeef00001337 mov [rdi],rax mov rax,0xc0ffee0000 mov [rsi],rax ''')
p.send(shellcode)
print(p.readallS())
|
18
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 回想一下,内存是按线性方式存储的。
这是什么意思?
假设我们访问地址 0x1337 处的四字节数据: [0x1337] = 0x00000000deadbeef
实际上,内存是以字节为单位按顺序存储的,采用小端模式: [0x1337] = 0xef [0x1337 + 1] = 0xbe [0x1337 + 2] = 0xad ... [0x1337 + 7] = 0x00
这对我们有什么好处?
这意味着我们可以使用偏移量来访问相邻的数据,就像上面展示的那样。
假设你想要从一个地址中获取第5个字节,你可以这样访问: mov al, [address+4]
请记住,偏移量从0开始。
执行以下操作: 从存储在 rdi 中的地址中加载两个连续的四字数据 计算前面步骤的四字数据的和。 将和存储在 rsi 中的地址中
现在,我们将进行以下设置以准备你的代码: [0x4041a8] = 0xbdc4b [0x4041b0] = 0x12b14 rdi = 0x4041a8 rsi = 0x404658
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' mov rax,[rdi] add rax,[rdi+8] mov [rsi],rax ''')
p.send(shellcode)
print(p.readallS())
|
19
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 在这个级别中,你将使用栈,这是一个动态扩展和收缩的内存区域。你将需要读取和写入栈,这可能需要你使用 pop 和 push 指令。你可能还需要使用栈指针寄存器 (rsp) 来了解栈的指向位置。
在这些级别中,我们将介绍栈。
栈是一个可以存储值以供以后使用的内存区域。
要将一个值存储在栈中,我们使用 push 指令;要检索一个值,我们使用 pop 指令。
栈是一种后进先出 (LIFO) 的内存结构,这意味着最后一个被推入的值将第一个被弹出。
想象一下从洗碗机中卸载盘子,假设有一个红色的、一个绿色的和一个蓝色的盘子。首先,我们将红色盘子放在柜子里,然后将绿色盘子放在红色盘子上面,最后放置蓝色盘子。
我们的盘子栈看起来像这样: 顶部 ----> 蓝色盘子 绿色盘子 底部 -> 红色盘子
现在,如果我们想要一个盘子来做三明治,我们将从栈中检索顶部的盘子,也就是最后一个放入柜子的蓝色盘子,也就是第一个弹出的盘子。
在x86中,pop 指令将从栈顶取出值并放入一个寄存器中。
类似地,push 指令将寄存器中的值推入栈的顶部。
使用这些指令,取出栈顶的值,减去 rdi 的值,然后将结果放回栈中。
现在,我们将进行以下设置以准备你的代码: rdi = 0x100d3 (栈) [0x7fffff1ffff8] = 0x30267be3
|
取出栈顶的值,减去 rdi 的值,然后将结果放回栈中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' pop rax sub rax,rdi push rax ''')
p.send(shellcode)
print(p.readallS())
|
20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 在这个级别中,你将使用栈,这是一个动态扩展和收缩的内存区域。你将需要读取和写入栈,这可能需要你使用 pop 和 push 指令。你可能还需要使用栈指针寄存器 (rsp) 来了解栈的指向位置。
在这个级别中,我们将探索栈的后进先出 (LIFO) 特性。
只能使用以下指令: push、pop
交换 rdi 和 rsi 中的值。 例如, 如果起始时 rdi = 2,rsi = 5 那么最终 rdi = 5,rsi = 2
现在,我们将进行以下设置以准备你的代码: rdi = 0x7ff3244 rsi = 0xa71fcb2
|
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.recvline()
shellcode = asm(''' push rdi push rsi pop rdi pop rsi ''')
p.send(shellcode)
print(p.readallS())
|
21
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 在之前的级别中,你使用 push 和 pop 来将数据存储到栈中和从栈中加载数据。
然而,你也可以直接使用栈指针来访问栈。
在 x86 中,栈指针存储在特殊寄存器 rsp 中。 rsp 总是存储栈顶的内存地址,即最后一个被推入的值的内存地址。
类似于内存级别,我们可以使用 [rsp] 来访问 rsp 中存储的内存地址上的值。
请在不使用 pop 的情况下计算栈上存储的连续四个四字节的平均值。
将平均值推入栈中。
Hint: RSP+0x?? Quad Word A RSP+0x?? Quad Word B RSP+0x?? Quad Word C RSP Quad Word D
现在,我们将进行以下设置以准备你的代码: (栈) [0x7fffff200000:0x7fffff1fffe0] = ['0xeb77e0b', '0x1e082933', '0x2ec86967', '0x392a5137'](事物列表)
|
- 使用
[rsp]
来访问 rsp 中存储的内存地址上的值
- 在不使用 pop 的情况下计算栈上存储的连续四个四字节的平均值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' xor rax,rax add rax,[rsp] add rax,[rsp+8] add rax,[rsp+16] add rax,[rsp+24] mov rdi,4 div rdi push rax ''')
p.send(shellcode)
print(p.readallS())
|
22
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 在这个级别中,你将使用控制流操作。这涉及使用指令直接或间接地控制特殊寄存器 `rip`,即指令指针。你将使用 jmp、call、cmp 等指令及其替代指令来实现所需的行为。
之前,你学习了如何以伪控制的方式操作数据,但是 x86 提供了直接操作控制流的实际指令。
有两种主要的控制流操作方式: 通过跳转(jump); 通过调用(call)。
在这个级别中,你将使用跳转指令。
跳转指令有两种类型: 无条件跳转(unconditional jumps) 有条件跳转(conditional jumps)
无条件跳转总是触发的,不基于先前指令的结果。
正如你所知,内存位置可以存储数据和指令。
你的代码将存储在 0x4000bf(每次运行都会改变)。
对于所有的跳转指令,有三种类型: 相对跳转(relative jumps):跳转到下一条指令的正向或负向偏移量。 绝对跳转(absolute jumps):跳转到特定地址。 间接跳转(indirect jumps):跳转到寄存器中指定的内存地址。
在 x86 中,绝对跳转(跳转到特定地址)的实现方式是先将目标地址放入一个寄存器中,然后执行 jmp reg。
在这个级别中,我们要求你执行一个绝对跳转。
执行以下操作: 跳转到绝对地址 0x403000
现在,我们将进行以下设置以准备你的代码: 加载你提供的代码到地址:0x4000bf
|
在 x86 中,绝对跳转(跳转到特定地址)的实现方式是先将目标地址放入一个寄存器中,然后执行 jmp reg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' mov rax,0x403000 jmp rax ''')
p.send(shellcode)
print(p.readallS())
|
23
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 在这个级别中,你将使用控制流操作。这涉及使用指令直接或间接地控制特殊寄存器 `rip`,即指令指针。你将使用 jmp、call、cmp 等指令及其替代指令来实现所需的行为。
回想一下,对于所有的跳转指令,有三种类型: 相对跳转(relative jumps) 绝对跳转(absolute jumps) 间接跳转(indirect jumps)
在这个级别中,我们要求你执行一个相对跳转。
你需要使用某种指令填充你的代码,以使这个相对跳转成为可能。
我们建议使用 `nop` 指令。它只占用一个字节的空间,而且非常可预测。
实际上,我们使用的汇编器有一个方便的 `.rept` 指令,你可以使用它来重复某个汇编指令多次: [https://ftp.gnu.org/old-gnu/Manuals/gas-2.9.1/html_chapter/as_7.html](https://ftp.gnu.org/old-gnu/Manuals/gas-2.9.1/html_chapter/as_7.html)
对于这个级别,有用的指令有: jmp (reg1 | addr | offset) ; nop
提示:对于相对跳转,查阅如何在 x86 中使用 `labels`。
利用上述知识,执行以下操作: 将你的代码的第一条指令设置为 jmp 将该 jmp 设置为相对跳转,跳转到当前位置后的 0x51 个字节处 在相对跳转将重新定向控制流的代码位置,将 rax 设置为 0x1
现在,我们将进行以下设置以准备你的代码: 加载你提供的代码到地址:0x400054
|
- 使用label进行间接跳转
- 使用rept重复nop指令 0x51次
- 使用rept可以重复某个汇编指令多次
nop
指令只占用一个字节的空间,而且非常可预测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' jmp short label_target .rept 0x50 nop .endr label_target: mov rax, 0x1 ''')
p.send(shellcode)
print(p.readallS())
|
24
1 2 3 4 5 6 7 8 9 10 11 12 13
| 在这个级别中,你将使用控制流操作。这涉及使用指令直接或间接地控制特殊寄存器 `rip`,即指令指针。你将使用 jmp、call、cmp 等指令及其替代指令来实现所需的行为。
现在,我们将结合前两个级别,执行以下操作: 创建一个两次跳转的跳板(trampoline): 将你的代码的第一条指令设置为 jmp 将该 jmp 设置为相对跳转,跳转到当前位置后的 0x51 个字节处 在地址 0x51 处写入以下代码: 将栈顶的值放入寄存器 rdi 跳转到绝对地址 0x403000
现在,我们将进行以下设置以准备你的代码: 加载你提供的代码到地址:0x40009a (stack) [0x7fffff1ffff8] = 0xfe
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' jmp short label_target .rept 0x51 nop .endr label_target: pop rdi mov rax,0x403000 jmp rax ''')
p.send(shellcode)
print(p.readallS())
|
25
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| 在这个级别中,你将使用控制流操作。这涉及使用指令直接或间接地控制特殊寄存器 `rip`,即指令指针。你将使用 jmp、call、cmp 等指令及其替代指令来实现所需的行为。
在这个级别中,我们将多次测试你的代码,使用动态值来运行你的代码!这意味着我们将以各种随机的方式运行你的代码,以验证逻辑是否足够健壮,能够在正常使用中生存。
现在,我们将向你介绍条件跳转——这是 x86 中最有价值的指令之一。 在高级编程语言中,存在 if-else 结构,用于执行以下操作: 如果 x 是偶数: is_even = 1 否则: is_even = 0
这应该很熟悉,因为它可以仅通过位逻辑来实现,而你在之前的级别中已经做过了。
在这些结构中,我们可以根据提供给程序的动态值来控制程序的控制流。
使用 jmp 来实现上述逻辑可以这样做:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mov rax, rdi mov rsi, 2 div rsi
cmp rdx, 0
jne not_even
mov rbx, 1 jmp done
not_even: mov rbx, 0 done: mov rax, rbx
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
然而,通常你不仅仅想要一个单独的 'if-else' 结构。
有时你想要两个 if 检查,然后是一个 else。
为了做到这一点,你需要确保在失败后,控制流会“落入”到下一个 `if`。
所有的代码执行后必须跳转到同一个 `done` 位置,以避免执行 else 代码。
x86 中有很多种跳转类型,了解它们的用法会很有帮助。
几乎所有的跳转都依赖于一个叫做 ZF 的标志位,即零标志位。
当 cmp 相等时,ZF 被设置为 1,否则为 0。
利用上述知识,实现以下内容: 如果 [x] 是 0x7f454c46: y = [x+4] + [x+8] + [x+12] 否则如果 [x] 是 0x00005A4D: y = [x+4] - [x+8] - [x+12] 否则: y = [x+4] * [x+8] * [x+12]
其中: x = rdi,y = rax。
假设每个解引用的值都是有符号的 dword。 这意味着每个内存位置上的值可以是负值。
一个有效的解决方案至少要使用以下指令之一: jmp(任意变体),cmp
现在,我们将在你的代码上运行多次测试,这是一个示例运行: (data) [0x404000] = {4 个随机的 dword} rdi = 0x404000
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' mov eax,[rdi] cmp eax, 0x7f454c46 je elf_format cmp eax, 0x00005A4D je dos_format jmp other_format elf_format: mov eax, [rdi + 4] add eax, [rdi + 8] add eax, [rdi + 12] jmp end
dos_format: mov eax, [rdi + 4] sub eax, [rdi + 8] sub eax, [rdi + 12] jmp end
other_format: mov eax, [rdi + 4] imul eax, [rdi + 8] imul eax, [rdi + 12] end: nop ''')
p.send(shellcode)
print(p.readallS())
|
26
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| 在这个级别中,你将使用控制流操作。这涉及使用指令直接或间接地控制特殊寄存器 `rip`,即指令指针。你将使用 jmp、call、cmp 等指令及其替代指令来实现所需的行为。
在这个级别中,我们将多次使用动态值测试你的代码!这意味着我们将以各种随机方式运行你的代码,以验证逻辑是否足够强大,能够在正常使用中生存。
最后一种跳转类型是间接跳转,通常在实际中用于 switch 语句。
switch 语句是 if 语句的一种特殊情况,它仅使用数字来确定控制流的走向。
以下是一个示例: switch(number): 0: jmp do_thing_0 1: jmp do_thing_1 2: jmp do_thing_2 default: jmp do_default_thing
这个示例中的 switch 是基于 `number` 进行操作,它可以是 0、1 或 2。
如果 `number` 不是这些数字之一,则会触发 default。
你可以将其视为简化的 else-if 结构。
在 x86 中,你已经习惯了使用数字,所以应该不会感到奇怪,你可以基于某个确切的数字来创建 if 语句。
此外,如果你知道数字的范围,switch 语句非常适用。
例如,考虑到跳转表的存在。
跳转表是一段连续的内存,其中保存着要跳转到的位置的地址。
在上面的示例中,跳转表可以是这样的: [0x1337] = do_thing_0 的地址 [0x1337+0x8] = do_thing_1 的地址 [0x1337+0x10] = do_thing_2 的地址 [0x1337+0x18] = do_default_thing 的地址
使用跳转表,我们可以大大减少使用 cmp 指令的次数。
现在我们只需要检查 `number` 是否大于 2。
如果是,总是执行: jmp [0x1337+0x18] 否则: jmp [jump_table_address + number * 8]
利用上述知识,实现以下逻辑: 如果 rdi 是 0: jmp 0x403006 否则如果 rdi 是 1: jmp 0x4030ef 否则如果 rdi 是 2: jmp 0x4031c8 否则如果 rdi 是 3: jmp 0x4032ac 否则: jmp 0x403367
请按照以下约束条件完成上述任务: 假设 rdi 不会为负数 最多使用 1 条 cmp 指令 最多使用 3 条跳转指令(任意变体) 我们会在 rdi 中提供要进行 'switch' 的数字 我们会在 rsi 中提供跳转表的基地址
这里是一个示例表格: [0x404222] = 0x403006(地址将会改变) [0x40422a] = 0x4030ef [0x404232] = 0x4031c8 [0x40423a] = 0x4032ac [0x404242] = 0x403367
|
- 跳转表的基地址在rsi中
- 偏移存在rdi中
[rsi + rdi*8]
就是要跳转的地址
- 最后一种情况:rsi偏移0x20(也就是4个8字节)的位置。注意不是绝对地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| from pwn import *
context.arch = 'amd64'
p = process('/challenge/run')
p.recvline()
shellcode = asm(''' cmp rdi, 4 jge default_case lea rax, [rsi + rdi*8] jmp [rax] default_case: mov rax,rsi add rax,0x20 jmp [rax] ''')
p.send(shellcode)
print(p.readallS())
|