针对文:
http://bbs.pediy.com/showthread.php?t=110507
见到该帖后受不得诱惑,从仅有的28kx币中拿出2kx下载学习,的确很有意思,但也存在疑问,在此一并提出.
该帖中提及的代码叫Crack Me Hamishebahar,从GOOGLE中搜索应该来自伊朗的一个黑客和破解论坛,该作者已经给出了PASSWORD,但寻求求解它的方法.
下面我们对代码进行分析:
1)首先我们看到的程序是个loader,它要求您输入一个可多行的password,不如说是一个解码KEY,然后通过这个KEY解码程序目录下的一个加密DLL,并通过Assembly.Load()加载后执行.正确加载执行后会弹出一个伊朗文的提示框,否则英文提示PASSWORD错误.
2)下面我们看它的关键:如何验证KEY和解码加DLL:
/// <summary>
/// 这可是关键!
/// 1)先取头五个字节来验证 password(其实也就是解码KEY)
/// 2)文件最后五字节放着,正式文件开始五字节的解码结果,用来验证解码是否正确,不正确就不白费功夫了.
/// 3)接下来按照解码KEY长度,一段段取来解码并显示进度,最后把剩下部分解码但去掉最后的八个字节,(三个一字节计算因子,一个五字节验证用值)
/// 4)结果是个DLL,把它返回
/// </summary>
/// <param name="LoadFile"></param>
/// <param name="Password"></param>
/// <returns></returns>
public byte[] GetPasswordByte(string LoadFile, string Password)
{
List<byte> list = new List<byte>();
if (File.Exists(LoadFile))
{
byte[] buffer = new byte[Password.Length];
using (FileStream stream = new FileStream(LoadFile, FileMode.OpenOrCreate))
{
if (this.MainPrg != null)
{
this.MainPrg.Maximum = Convert.ToInt32(stream.Length);
this.MainPrg.Value = 0;
}
stream.Seek(stream.Length - 8L, SeekOrigin.Begin);
this.Rnd1 = Convert.ToByte(stream.ReadByte()); //保存那个DLL尾-8处的一个字节
stream.Seek(stream.Length - 7L, SeekOrigin.Begin);
this.Rnd2 = Convert.ToByte(stream.ReadByte()); //保存那个DLL尾-7处的一个字节
stream.Seek(stream.Length - 6L, SeekOrigin.Begin);
this.Rnd3 = Convert.ToByte(stream.ReadByte()); //保存那个DLL尾-6处的一个字节
byte[] buffer2 = new byte[5];
byte[] password = new byte[5];
stream.Seek(stream.Length - 5L, SeekOrigin.Begin);
stream.Read(buffer2, 0, 5); //取那个DLL尾-5处的五个字节
stream.Seek(0L, SeekOrigin.Begin);
stream.Read(password, 0, 5); //取DLL开头的五个字节
password = this.GetPassword(password.ToList<byte>(), Password); //取文件开头五字节解码
if (!this.Check(buffer2, password)) //和验证值比较看是否解码正确,以证明Password是对的
{
return null;
}
//下面开始真正的解码工作
stream.Seek(0L, SeekOrigin.Begin);
for (int i = 0; i < stream.Length; i++)
{
if ((i + Password.Length) >= (stream.Length - 8L))
{
int count = Convert.ToInt32(stream.Length) - i;
buffer = new byte[count];
stream.Seek((long) i, SeekOrigin.Begin);
stream.Read(buffer, 0, count);
buffer = this.GetPassword(buffer.ToList<byte>(), Password);
list.AddRange(this.SelectList(buffer.ToList<byte>(), 0, count - 8));
if (this.MainPrg != null)
{
this.MainPrg.Value = this.MainPrg.Maximum;
Application.DoEvents();
}
goto Label_024C;
}
stream.Seek((long) i, SeekOrigin.Begin);
stream.Read(buffer, 0, Password.Length);
buffer = this.GetPassword(buffer.ToList<byte>(), Password);
list.AddRange(this.SelectList(buffer.ToList<byte>(), 0, Password.Length));
if (this.MainPrg != null)
{
this.MainPrg.Value += Password.Length;
Application.DoEvents();
}
i += Password.Length - 1;
}
}
}
Label_024C:
return list.ToArray();
}
下面我们看看它调用的两个方法:
/// <summary>
/// 看看它在做什么?按Password的长度逐段调用解码方法解码,开始或最后不够Password的长度时按实际长度解码
/// </summary>
/// <param name="RET"></param>
/// <param name="Password"></param>
/// <returns></returns>
public byte[] GetPassword(List<byte> RET, string Password)
{
List<byte> list = new List<byte>();
for (int i = 0; i < RET.Count; i += Password.Length)//步长为password文本的长度,第一次时RET.Count为五,其后则取Password.Length为RET.Count,最后取文件剩下的长度
{
if ((i + Password.Length) > RET.Count)//如果当前完成位置再按步长增加超出要解码的字节数组长时
{
list.AddRange(this.GetPasswordByte(this.SelectList(RET, i, RET.Count - i), Password));//取剩下的部分解码
break;
}
list.AddRange(this.GetPasswordByte(this.SelectList(RET, i, Password.Length), Password).ToList<byte>());//按password文本的长度,一段段解码
}
return list.ToArray();
}
/// <summary>
/// 解码方法:
///
/// 对每个字节在0x00~0xff范围内进行轮式加减操作,如下:
/// (0~255轮操作)加214
/// (0~255轮操作)减199
/// 加第二个参数对应字节 (0~255轮操作)
/// 减第二人参数的长度 (0~255轮操作)
/// 加3 (0~255轮操作)
///
/// 其实是种按字节解码的方法,传入字节的多少没什么关系,只要字节数不大于Password长度,分多段解后合起来和一次解都一样
/// </summary>
/// <param name="MyByte"></param>
/// <param name="Password"></param>
/// <returns></returns>
private byte[] GetPasswordByte(List<byte> MyByte, string Password)
{
List<byte> list = MyByte;
for (int i = 0; i < list.Count; i++)
{
list[i] = this.GoPByte(this.Rnd3, list[i], true);
list[i] = this.GoPByte(this.Rnd2, list[i], false);
list[i] = this.GoPByte(Convert.ToByte(Password[i]), list[i], true);
list[i] = this.GoPByte(Password.Length, list[i], false);
list[i] = this.GoPByte(this.Rnd1, list[i], true);
}
return list.ToArray();
}
其中的三个计算因子为:
/// <summary>
/// 加法因子:3
/// 保存在那个DLL尾-8处的一个字节
/// </summary>
private int Rnd1;
/// <summary>
/// 减法因子:199
/// 保存在那个DLL尾-7处的一个字节
/// </summary>
private int Rnd2;
/// <summary>
/// 加法因子:214
/// 保存在那个DLL尾-6处的一个字节
/// </summary>
private int Rnd3;
最后我们给出DLL文件头和文件尾的五个字节,也即加密后和加密前的五个字节(程序中做验证KEY用):
文件头:[41 60 8b 06 fd]
文件尾:[4d 5a 90 00 03] (注:刚好是不加密的'MZ'头前五个字节)
好了,现在总结一下我们可利用的东西:
1)用户要提供一个可输入的KEY(也即换行,TAB外都应该是可见字符),它的长度和每一位都要参与解码.
2)我们可以放心确定的是那五个字节可以用来验证我们的算法.
3)其它字节会是什么?如果作者给我们机会的话,前64字节是标准dos头,再64字节是标准DOS sub,接下来一个'PE',事实上除了那五个字节和一个dos头中的e_lfanew(只能知道它在什么位置,值是什么都不会清楚)都可能不知道,因为都可以变化.当然,这个例子没变.我们可以获得更多的可验证字节.
4)这个例子中,key的长度并不很长,没超过我们可验证的前128+2+2个字节.
只有这么多了,真可以求出它的KEY吗?很是质疑!
首先,这个KEY是非可逆的,首先是求长度,其次是逐位求值,可验证的却只有那五个字节.(如果严谨的话)通过上面的分析,它的算法是逐位的,故知道了长度可逐位求解.
我们寄希望于下面两个假设:
1、dll前132字节都为标准值,作者没变过。
2、password长度少于132字节。
下面我们给出代码:
public partial class Form3 : Form
{
//从它的loader程序中读入,假设都是标准的,用来验证我们的KEY
byte[] validateByte = new byte[132];//包括'PE'及Machine,假设CPU也指定的一样,多了几个可验证字节
byte[] objprogramByte = new byte[132];
int[] lengthlist;//存求出的password可能的长度;
bool[] finded = new bool[132]; //保存在该位上是否验证过的字节都可以找到值,以确定最后可能的位;
public Form3()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
//
if (readValidateByte("Perplex Password Find.exe", "Crypt.dll"))
{
Initbyte();
findPass();
}
}
private void Initbyte()
{
for (int i = 0; i < 132;i++ )
{
finded[i] = true;
}
}
private int findLength()
{
int j = 0;
for (int i = 0; i < 132; i++)
{
if (finded[i])
j++;
}
return j;
}
private bool readValidateByte(string filename,string dllname)
{
bool result = false;
bool result2 = false;
if (File.Exists(filename))
{
using (FileStream stream = new FileStream(filename, FileMode.OpenOrCreate))
{
stream.Seek(0, SeekOrigin.Begin);
stream.Read(validateByte, 0, 132);
result = true;
}
}
if (File.Exists(dllname))
{
using (FileStream stream2 = new FileStream(dllname, FileMode.OpenOrCreate))
{
stream2.Seek(0, SeekOrigin.Begin);
stream2.Read(objprogramByte, 0, 132);
result2 = true;
}
}
return result && result2;
}
private void findPass()
{
TestMe me = new TestMe();
for (int x = 0; x < 132; x++)
{
for (int i = 0; i < 132; i++) //可验证的只有这么多位
{
bool findValue = false;
for (int j = 0; j < 127; j++)//ASCLL全进来吧,省不了多少,相比我往出摘的话.(真懒!什么人!鄙视一下!)
{
byte value = objprogramByte[x];
value = me.GoPByte(3, value, true);
value = me.GoPByte(i, value, false);
value = me.GoPByte(j, value, true);
value = me.GoPByte(199, value, false);
value = me.GoPByte(214, value, true);
if (value == validateByte[x])
{
//this.textBox1.Text += "第"+x.ToString()+"字节 值为:"+j.ToString()+"位数:"+i.ToString()+" ";
findValue = true;
}
}
if (!findValue)
{
finded[i] = false;
}
}
//this.textBox1.Text += + Environment.NewLine;
}
MessageBox.Show(findLength().ToString());
lengthlist = new int[findLength()];
int vv = 0;
for (int ii = 0; ii < 132; ii++)
{
if (finded[ii])
{
this.textBox1.Text += ii.ToString() + Environment.NewLine;
lengthlist[vv] = ii;
vv++;
}
}
writeSerialFile();
MessageBox.Show("It Cracked!");
}
private void writeSerialFile()
{
//copy PasswordByte.cs 原作者的文件,只把方法变成public
TestMe me = new TestMe();
for (int x = 0; x < lengthlist.Length; x++)
{
byte[] buffer = new byte[lengthlist[x]];
for (int i = 0; i < lengthlist[x]; i++)
{
for (int j = 0; j < 127; j++)//ASCLL全进来吧,省不了多少,相比我往出摘的话.(真懒!什么人!鄙视一下!)
{
byte value = objprogramByte[i];
value = me.GoPByte(3, value, true);
value = me.GoPByte(lengthlist[x], value, false);
value = me.GoPByte(j, value, true);
value = me.GoPByte(199, value, false);
value = me.GoPByte(214, value, true);
if (value == validateByte[i])
{
buffer[i] = (byte)j;
break;
}
}
}
writeFile(x, buffer);
}
}
private void writeFile(int x, byte[] buffer)
{
string filename = x.ToString() + ".dll";
using (FileStream stream = new FileStream(filename, FileMode.OpenOrCreate))
{
stream.Seek(0, SeekOrigin.Begin);
stream.Write(buffer, 0, buffer.Length);
}
}
}
说明:
1)读入其loader程序的前132字节,或其它任意exe前130(or 132)字节,作为验证标准。
2)读入加密后的dll前132字节,以便用作用于我们的方法后和验证标准比对。
3)先求可能的password长度,思路为:0~127个可用ASCLL码,1~132位字符串儿,代入解码算法比较加密前后的一个字节,找出这一个第一个字节有多少种匹配的可能。接下来看第二个字节。。。我们会发现有些位在某种长度下无论什么ASCLL码都验证不回去,把它去掉吧,最后,我们找出可验证的password长度的交集,我们发现可能的长度只有15种。
4)长度确定了(只有15种),那我们求它的password内容是什么,结果也只有15种。
5)好了,拷入原程序的密码输入框试试吧,其实除了一种之外都含有不可见字符。
最后给出第十种,也就是正确的结果:
salam Chetori?
mikhay Mano Crack Koni?
Mituni?
Midoni Man Kiam?
Man Passwordam.
Movafagh v sarboland bashi azizam.
结论:作者没有修改PE文件前132字节内容,且KEY小于这个长度,原本不可逆的KEY被用穷举加验证的方法在十几秒内运算出来。(代码见附件,代码的效率还望见谅)
另外:
本坛的帖子中网友sessiondiy很早地给出了结果,遗憾没有Solution,这里补上并给予致意,也希望如有更好的方法能以指点和交流。
src1.rar
[培训]《安卓高级研修班(网课)》月薪三万计划