pwn college 刷题记录:assembly crash course

本文最后更新于:2024年3月18日 下午

汇编,太美丽辣

总结

  • 用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

#!/usr/bin/env python3
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

#!/usr/bin/env python3
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 reg2

注意:div 是一条特殊的指令,可以将一个 128 位的被除数除以一个 64 位的除数,并且只使用一个寄存器作为操作数来存储商和余数。

这个复杂的 div 指令如何工作并操作一个 128 位的被除数(它是寄存器大小的两倍)?

对于指令:div reg,以下操作发生:
rax = rdx:rax / reg
rdx = 余数

rdx:rax 表示 rdx128 位被除数的高 64 位,rax128 位被除数的低 64 位。

在调用 div 之前,你必须小心 rdxrax 中的内容。

请计算以下内容:
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 中的逻辑指令:andornotxor

在汇编中移动位是另一个有趣的概念!

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()

#mov rax,rdi
#shl rax,24
#shr rax,32


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的逻辑指令:andornotxor

只能使用以下指令:
andorxor

实现以下逻辑:
如果x是偶数,则y等于1;否则,y等于0

其中:
x = rdi
y = rax

现在,我们将按照以下方式设置参数,以便你编写代码:
rdi = 0x3a0610d0
  • rdi与1与运算可以判断rdi的奇偶
    • 为1:奇数
    • 为0:偶数
  • 把刚刚得到的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
在这个级别中,你将使用栈,这是一个动态扩展和收缩的内存区域。你将需要读取和写入栈,这可能需要你使用 poppush 指令。你可能还需要使用栈指针寄存器 (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
在这个级别中,你将使用栈,这是一个动态扩展和收缩的内存区域。你将需要读取和写入栈,这可能需要你使用 poppush 指令。你可能还需要使用栈指针寄存器 (rsp) 来了解栈的指向位置。

在这个级别中,我们将探索栈的后进先出 (LIFO) 特性。

只能使用以下指令:
pushpop

交换 rdirsi 中的值。
例如,
如果起始时 rdi = 2rsi = 5
那么最终 rdi = 5rsi = 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
在之前的级别中,你使用 pushpop 来将数据存储到栈中和从栈中加载数据。

然而,你也可以直接使用栈指针来访问栈。

在 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`,即指令指针。你将使用 jmpcallcmp 等指令及其替代指令来实现所需的行为。

现在,我们将结合前两个级别,执行以下操作:
创建一个两次跳转的跳板(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`,即指令指针。你将使用 jmpcallcmp 等指令及其替代指令来实现所需的行为。

在这个级别中,我们将多次测试你的代码,使用动态值来运行你的代码!这意味着我们将以各种随机的方式运行你的代码,以验证逻辑是否足够健壮,能够在正常使用中生存。

现在,我们将向你介绍条件跳转——这是 x86 中最有价值的指令之一。
在高级编程语言中,存在 if-else 结构,用于执行以下操作:
如果 x 是偶数:
is_even = 1
否则:
is_even = 0

这应该很熟悉,因为它可以仅通过位逻辑来实现,而你在之前的级别中已经做过了。

在这些结构中,我们可以根据提供给程序的动态值来控制程序的控制流。

使用 jmp 来实现上述逻辑可以这样做:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
; 假设 rdi = x,rax 是输出
; rdx = rdi mod 2
mov rax, rdi
mov rsi, 2
div rsi
; 余数为 0 表示偶数
cmp rdx, 0
; 如果不为 0,则跳转到 not_even 代码
jne not_even
; 否则继续执行 even 代码
mov rbx, 1
jmp done
; 只有在 not_even 时才跳转到此处
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`,即指令指针。你将使用 jmpcallcmp 等指令及其替代指令来实现所需的行为。

在这个级别中,我们将多次使用动态值测试你的代码!这意味着我们将以各种随机方式运行你的代码,以验证逻辑是否足够强大,能够在正常使用中生存。

最后一种跳转类型是间接跳转,通常在实际中用于 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` 进行操作,它可以是 012

如果 `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]

利用上述知识,实现以下逻辑:
如果 rdi0
jmp 0x403006
否则如果 rdi1
jmp 0x4030ef
否则如果 rdi2
jmp 0x4031c8
否则如果 rdi3
jmp 0x4032ac
否则:
jmp 0x403367

请按照以下约束条件完成上述任务:
假设 rdi 不会为负数
最多使用 1cmp 指令
最多使用 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())


pwn college 刷题记录:assembly crash course
http://gls.show/p/344bba9e/
作者
郭佳明
发布于
2024年3月18日
许可协议