目录
在攻防世界做到一个题目感觉还挺有意思,记录一下
这个放链接也只是攻防世界的页面,所以直接说题目地址是在攻防世界->Reverse模块->难度3->zorropub.
参考文章:zorropub 攻防世界_北风~的博客-CSDN博客
攻防世界 zorropub_路途之后是路途的博客-CSDN博客
题目分析:
下面进入正题:)
在IDA中打开附件,找到主函数
cpp
int sub_4009BD()
{
__int64 v0; // rax
int input_number; // [rsp+1Ch] [rbp-104h] BYREF
int input; // [rsp+20h] [rbp-100h] BYREF
int i; // [rsp+24h] [rbp-FCh]
unsigned int seed; // [rsp+28h] [rbp-F8h]
unsigned int v6; // [rsp+2Ch] [rbp-F4h]
char v7[96]; // [rsp+30h] [rbp-F0h] BYREF
char v8[16]; // [rsp+90h] [rbp-90h] BYREF
char v9[32]; // [rsp+A0h] [rbp-80h] BYREF
char s[32]; // [rsp+C0h] [rbp-60h] BYREF
char s1[40]; // [rsp+E0h] [rbp-40h] BYREF
unsigned __int64 v12; // [rsp+108h] [rbp-18h]
v12 = __readfsqword(0x28u);
seed = 0;
puts("Welcome to Pub Zorro!!");
printf("Straight to the point. How many drinks you want?");
__isoc99_scanf("%d", &input_number);
if ( input_number <= 0 )
{
printf("You are too drunk!! Get Out!!");
exit(-1);
}
printf("OK. I need details of all the drinks. Give me %d drink ids:", input_number);// 输入数字
for ( i = 0; i < input_number; ++i )
{
__isoc99_scanf("%d", &input);
if ( input <= 16 || input > 0xFFFF )
{
puts("Invalid Drink Id.");
printf("Get Out!!");
exit(-1);
}
seed ^= input; // 根据输入的数字大小输入对应的drink_id,并且与seed异或
}
i = seed;
v6 = 0;
while ( i )
{
++v6;
i &= i - 1; // 判断一个数的二进制形式中含有多少个1
}
if ( v6 != 10 )
{
puts("Looks like its a dangerous combination of drinks right there.");
puts("Get Out, you will get yourself killed");
exit(-1);
}
srand(seed);
MD5_Init(v7);
for ( i = 0; i <= 29; ++i )
{
v6 = rand() % 1000; // 取随机数
sprintf(s, "%d", v6);
v0 = strlen(s);
MD5_Update(v7, s, v0); // MD5加密
v9[i] = v6 ^ LOBYTE(dword_6020C0[i]);
}
v9[i] = 0;
MD5_Final(v8, v7);
for ( i = 0; i <= 15; ++i )
sprintf(&s1[2 * i], "%02x", v8[i]);
if ( strcmp(s1, "5eba99aff105c9ff6a1a913e343fec67") )// 此处传的是加密结果的前16位
{
puts("Try different mix, This mix is too sloppy");
exit(-1);
}
return printf("\nYou choose right mix and here is your reward: The flag is nullcon{%s}\n", v9);
}
函数先是要求输入一个数字a,再输入a个dring_id号,要求大小在16~0xffff之间,并且将这些id号与种子值seed异或,下面的一个while循环时判断seed值的,i&=i-1用来判断一个数的二进制形式中1的个数,也就是要求seed值的二进制形式里要有10个1 .
接下来就是取30个随机数v6进行MD5加密,传MD5加密结果的前16位来进行判断是否相等,最后的flag就是v6异或数组dword_6020C0的值,这里%02x是以是十六进制形式表示,不足两位的补0
本来是想爆破v6的,还是没搞懂MD5加密,MD5加密,无论输入有多长,输出总是128位也就是32个16进制数,这里传的只是加密结果的前16位。不过也还是可以爆破的,不是从MD5爆破的就是了,看其他大佬的wp才知道可以使用subprocess模块,学到了
subprocess模块:
参考文章:python subprocess模块 - lincappu - 博客园 (cnblogs.com)
subprocess (这个b站的视频也不错)
subprocess模块是Python中自带的一个模块,可以在父进程中创建一个子进程,允许在python中执行外部命令并与其进行交互。嗯我的理解是在一个程序中实现与另一个程序进行输入输出的交流,就类似于我们写一个自动脚本模拟我们与另一个程序进行交流的情况。(个人理解)
subprocess模块包含几个核心函数,这些函数方便启动和控制子进程
subprocess.Popen()函数:
这个函数是subprocess模块最核心,最底层的函数,用于创建一个子进程,可以在该对象上调用方法来与子进程进行交互
class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0,restore_signals=True, start_new_session=False, pass_fds=(), *, encoding=None, errors=None)
- args:shell命令,可以是字符串或者序列类型(如:list,元组)
- bufsize:缓冲区大小。当创建标准流的管道对象时使用,默认-1。
0:不使用缓冲区
1:表示行缓冲,仅当universal_newlines=True时可用,也就是文本模式
正数:表示缓冲区大小
负数:表示使用系统默认的缓冲区大小。- stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄
- preexec_fn:只在 Unix 平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用
- shell:如果该参数为 True,将通过操作系统的 shell 执行指定的命令。
- cwd:用于设置子进程的当前目录。
- env:用于指定子进程的环境变量。如果 env = None,子进程的环境变量将从父进程中继承。
创建一个子进程,然后执行一个简单的命令:
subprocess.run()函数:
.run函数是python3.5之后出现的一个函数,其底层接口还是Popen,不过会更方便,用于运行一个外部命令,等待它完成并返回结果。
subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None)
- args:表示要执行的命令。必须是一个字符串,字符串参数列表。
- stdin、stdout 和 stderr:子进程的标准输入、输出和错误。其值可以是 subprocess.PIPE、subprocess.DEVNULL、一个已经存在的文件描述符、已经打开的文件对象或者 None。subprocess.PIPE 表示为子进程创建新的管道。subprocess.DEVNULL 表示使用 os.devnull。默认使用的是 None,表示什么都不做。另外,stderr 可以合并到 stdout 里一起输出。
- timeout:设置命令超时时间。如果命令执行时间超时,子进程将被杀死,并弹出 TimeoutExpired 异常。
- check:如果该参数设置为 True,并且进程退出状态码不是 0,则弹 出 CalledProcessError 异常。
- encoding: 如果指定了该参数,则 stdin、stdout 和 stderr 可以接收字符串数据,并以该编码方式编码。否则只接收 bytes 类型的数据。
- shell:如果该参数为 True,将通过操作系统的 shell 执行指定的命令。
一般的话使用stdin,stdout比较多
补充:
Popen.communicate()
: 用于与子进程进行输入输出交互,向子进程发送数据并获取输出结果。
Popen.stdin
,Popen.stdout
,Popen.stderr
: 分别代表子进程的标准输入、标准输出和标准错误输出。
Popen.wait()
: 等待子进程结束,并返回其返回码。使用时要导入subpeocess ,比如import subprocess这样
python
import subprocess #
import shlex # 帮助分隔命令
process = subprocess.run(shlex.split("python 解密脚本.py")) # subprocess.run接口
print(process)
# returncode=0表示调用成功,不为0表示调用失败
# subprocess.Popen不会阻断整个程序的运行
process = subprocess.Popen(['python', '解密脚本.py'], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
# shell表示终端,表示前面的Tree命令要在终端出运行,
# stdout=subprocess.PIPE,其中subprocess.PIPE表示创建一个管道,令stdout=subprocess.PIPE表示命令正常执行的结果丢到stdout这个管道里
# stderr表示命令执行出错的结果丢到这里,注意subprocess.PIPE每次都会创建一个新的管道,会得到一个对象obj接住这个对象
# stdin=subprocess.PIPE表示输入
res = process.stdout.read() #读取管道信息
print(res.decode('utf-8')) # 打印出正确执行的结果
process.stdin.write('yes\n'.encode()) # 向Py脚本输入数据使用此。encode表示转为byte类型,模拟与终端的交互
process.stdin.flush() # 写入
print(process.stdout.read())
下面那这题作为例子具体介绍一下吧
题目脚本:
我们现在需要知道的就是随机数的值,因为没法利用MD5爆破所以只能利用程序自身了,我们知道只要得到符合要求的seed值,就可以进入下面随机数生成的部分,而固定的seed+固定的随机数算法=生成固定的随机数,所以我们只需要看哪一个符合要求的随机数种子得到的随机数是符合最后的加密结果就可以(感觉说的有点绕....就是找到所有符合要求的seed(编写脚本爆破找)->一个一个输入到程序中测试(这一块就可以使用subprocess来搞,手动输入当然是很麻烦)->看哪个seed是可以得到最后结果的))
注意:因为drink_id是在16~0xffff范围,而初始值位0的seed值和他们异或之后的最终结果也是在16~0xffff范围,这样的话,就方便爆破了。
python
#!/usr/bin/python
# -*- coding: utf-8 -*-
from subprocess import *
# 这里是找出符合条件的数 //这部分是先找到符合要求的数,即最后的seed,因为我们可以知道input在16到0xffff范围内,那么seed=0循环异或
# 原函数对种子的加密 //的结果seed也是在这个范围内,那么当循环的次数最少的时候是不是就是找最后seed最简单的时候?,而且也是直接通过进程
a = []
for i in range(16, 65535):
v9 = 0
s = i
while i:
v9 += 1
i &= i - 1
if v9 == 10:
a.append(s) //找出所有符合条件的值,可能不止一个,添加到a列表中
# 循环输入符合条件的数,爆破flag
for i in a: //在符合第一个判断条件a列表中来和程序进行交互,看哪一个数的的输出结果包含flag,就打印这个数
proc = Popen(['./87356aae634e4e0a9a081f30fc81fe16'], stdin=PIPE, stdout=PIPE)
#PIPE表示新建输入输出管道,便于下面对管道的读取,若为none会直接在运行窗口输出,无法对其进行操作
out = proc.communicate(('1\n%s\n' % i).encode('utf-8'))[0]
#1\n%s\n表示喝一瓶饮料,id号为i,将i转为字符串加入其中,与'1\n%d\n' % i一样
# 这里communicate与子进程进行输入输出交互,返回的是一个元组,但是元组只有一个元素,所以要加上偏移0,1为标准错误
if "nullcon".encode('utf-8') in out:
print(out)
print(i)
这个脚本需要在linux中运行,但是在因为版本问题,在ubuntu里运行的时候会报错no such file or dictionary,后来问其他大佬,可能是libcrypto版本要求为1.0.0,所以我是运行不了....
最后的输出结果是
nullcon{nu11c0n_s4yz_x0r1n6_1s_4m4z1ng}
当然也可以直接爆破seed的值,然后手动运行程序输入input_number为1,再手动输入seed值拿到结果