学破解第110天,《C++之base64编码解码》学习
前言:
一直对黑客充满了好奇,觉得黑客神秘,强大,无所不能,来论坛两年多了,天天看各位大佬发帖,自己只能做一个伸手党。也看了官方的入门视频教程,奈何自己基础太差,看不懂。自我反思之下,决定从今天(2019年6月17日)开始定下心来,从简单的基础教程开始学习,希望能从照抄照搬,到能独立分析,能独立破解。
不知不觉学习了好几个月,发现自己离了教程什么都不会,不懂算法,不懂编程。随着破解学习的深入,楼主这个半吊子迷失了自我,日渐沉迷水贴装X,不能自拔。
==========申明:从第71天楼主开始水贴装X,帖子不再具有连续性,仅供参考,后续帖子为楼主YY专用贴!!!==========
立帖为证!--------记录学习的点点滴滴
部分bug已更正,其余bug未知,本文视频讲解:
https://www.52pojie.cn/thread-1212676-1-1.html
0x1base64算法加解密原理
1.首先利用万能的百度搜索相关知识,得到如下知识:
1)Base64使用A--Z,a--z,0--9,+,/ 这64个字符.
2)编码原理:将3个字节转换成4个字节( (3 X 8) = 24 = (4 X 6) )先读入3个字节,每读一个字节,左移8位,再右移四次,每次6位,这样就有4个字节了.
3)解码原理:将4个字节转换成3个字节.先读入4个6位(用或运算),每次左移6位,再右移3次,每次8位.这样就还原了.
2.通过百度知道的知识知道的密码表是由上述64字符组成的,接下来看看编码原理将3个字节转换成4个字节,这句话有点糊涂。
3.解码原理是将4个字节转换成3个字节,反过来就是解码,后面的左移,右移运算又是什么了,还是有些糊涂,接下来用代码模拟一遍。
0x2base64算法实现
1.密码表:
char base64[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";//定义密码表
2.编码实现:
char str[5] = "zxcv";//定义明文
char cpystr[10];//定义密文
/*
第一个参数:明文字符串
第二个参数:明文字符串长度
第三个参数:密文字符串分配的内存的地址
第四个参数:密文字符串所占内存空间大小
返回值:
0:表示加密完成
1:程序逻辑错误
2:密文字符串所占内存空间大小保存不下明文
其他值:未知错误
*/
int encodeBase64(const char *str, int strLen, const char *cpystr, int cpystrLen);
int encodeBase64(const char *str, int strLen, char *cpystr, int cpystrLen)
{
//读取3个字节zxc,转换为二进制01111010 01111000 01100011
//转换为4个6位字节,011110 100111 100001 100011
//不足8位在前补0,变成00011110 00100111 00100001 00100011
//再将4个字节转换为十进制,30 39 33 35
//通过下标找到码表中对应的,e n h j
//zxc加密后的密文就为,enhj
//接下来第二轮加密,只剩1个字母v,转换成二进制01110110
//转换成2个6位字节,011101 100000(10不足6位末尾补0)
//不足8位在前补0,变成 00011101 00100000
//转换成十进制,29 32
//通过下标找到码表中对应的d g,还缺两个才构成四字节,这时补等于号即可
//v加密后的密文为dg==
//最后整个明文被base64加密完成,得到密文enhjdg==
char base64[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";//定义base64码表
int i = 0;//明文下标
int j = 0;//密文下标
while (i < strLen)//判断是否加密完毕
{
if (j < cpystrLen - 1)//判断下标是否超过密文可存储范围
{
if (i + 3 <= strLen)//读取到3个字节
{
cpystr[j] = base64[str[i] >> 2];//取第1个字节的前6位,将其加密为base64[]码表中对应的字符
cpystr[j + 1] = base64[(str[i] & 3) << 4 | (str[i + 1] >> 4)];//取第一个字节的后2位再加上第二个字节的前4位,将其加密为base64[]码表中对应的字符
cpystr[j + 2] = base64[(str[i + 1] & 15) << 2 | (str[i + 2] >> 6)];//取第二个字节的后4位再加上第三个字节的前2位,将其加密为base64[]码表中对应的字符
cpystr[j + 3] = base64[str[i + 2] & 63];//取第三个字节的后6位,将其加密为base64[]码表中对应的字符
}
else if (i + 2 <= strLen)//读取到2个字节
{
cpystr[j] = base64[str[i] >> 2];//取第1个字节的前6位,将其加密为base64[]码表中对应的字符
cpystr[j + 1] = base64[(str[i] & 3) << 4 | (str[i + 1] >> 4)];//取第一个字节的后2位再加上第二个字节的前4位,将其加密为base64[]码表中对应的字符
cpystr[j + 2] = base64[(str[i + 1] & 15) << 2];//取第二个字节的后4位再加上第三个字节的前2位,将其加密为base64[]码表中对应的字符
//末尾补一个等于号
cpystr[j + 3] = '=';
}
else if (i + 1 <= strLen)//只读取到1个字节
{
cpystr[j] = base64[str[i] >> 2];//取第1个字节的前6位,将其加密为base64[]码表中对应的字符
cpystr[j + 1] = base64[(str[i] & 3) << 4];//取第一个字节的后2位再加上第二个字节的前4位,将其加密为base64[]码表中对应的字符
//末尾补两个等于号
cpystr[j + 2] = '=';
cpystr[j + 3] = '=';
}
else
{
cout << "为什么循环还在执行,程序逻辑错误!!!" << endl;
return 1;
}
i += 3;//明文每次循环向后移动3位
j += 4;//密文每次循环向后移动4位
}
else
{
cout << "存储密文的内存不足,请重新分配!!!" << endl;
return 2;
}
}
cpystr[j] = '\0';//补一个字符串结束符
return 0;
}
3.解码实现:
/*
base64解密函数
第一个参数:密文字符串
第二个参数:密文字符串长度
第三个参数:明文字符串分配的内存的地址
第四个参数:明文字符串所占内存空间大小
返回值:
0:表示解密完成
1:程序逻辑错误
2:明文字符串所占内存空间大小保存不下明文
其他值:未知错误
*/
int decodeBase64(const char *cpystr, int cpystrLen, char *str, int strLen);
int decodeBase64(const char *cpystr, int cpystrLen, char *str, int strLen)
{
//解密就是将加密的过程倒过来即可!
char base64[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";//定义base64码表
int i = 0;//密文下标
int j = 0;//明文下标
while (i < cpystrLen)//判断是否解密完毕
{
int index = 0;//码表下标
int temp1 = -1;//保存第一个密文下标
int temp2 = -1;//保存第二个密文下标
int temp3 = -1;//保存第三个密文下标
int temp4 = -1;//保存第四个密文下标
if (j < strLen - 1)//判断下标是否超过明文可存储范围
{
//通过码表反查密文对应的下标,然后分别获得十进制数字
for (; index < 64; index++) {
if (cpystr[i] == base64[index])
{
temp1 = index;
}
if (cpystr[i + 1] == base64[index])
{
temp2 = index;
}
if (cpystr[i + 2] == base64[index])
{
temp3 = index;
}
if (cpystr[i + 3] == base64[index])//一轮密文4个字节已取出,退出循环
{
temp4 = index;
}
//如果已经查到四个下标就退出循环!
if (temp1 != -1 && temp2 != -1 && temp3 != -1 && temp4 != -1) break;
}
if (temp3 != -1 && temp4 != -1)//完整的读取到了4个字节,直接解密
{
str[j] = (temp1 << 2) | (temp2 >> 4);//取第一个密文6个字符再加上第二个密文前2个字符
str[j + 1] = ((temp2 & 15) << 4) | (temp3 >> 2);//取第二个密文后4个字符再加上第三个密文前4个字符
str[j + 2] = ((temp3 & 3) << 6) | temp4;//取第三个密文后2个字符再加上第四个密文6个字符
}
else if (temp3 == -1)//只取到了2个字节
{
str[j] = (temp1 << 2) | (temp2 >> 4);//取第一个密文6个字符再加上第二个密文前2个字符
j -= 2;//下标前移2位,不然补‘\0’的位置不对
}
else if (temp4 == -1)//只取到了3个字节
{
str[j] = (temp1 << 2) | (temp2 >> 4);//取第一个密文6个字符再加上第二个密文前2个字符
str[j + 1] = ((temp2 & 15) << 4 | (temp3 >> 2));//取第二个密文后4个字符第三个密文前4个字符
j--;//下标前移1位,不然补‘\0’的位置不对
}
else
{
cout << "下标取值不对,程序逻辑错误!!!" << endl;
return 1;
}
i += 4;//密文每次循环向后移动4位
j += 3;//明文每次循环向后移动3位
}
else
{
cout << "存储明文的内存不足,请重新分配!!!" << endl;
return 2;
}
}
str[j] = '\0';//补一个字符串结束符
return 0;
}
0x3base64算法的变形及识别
1.base64可以对中文加密,当然了我这段代码并没有对中文处理,在处理前判断是中文或者英文字符就可以了,例如在ASCII码,32位平台中,一个中文有两个字节表示,一般是负数,为了base64能够对其处理,可以将其加上128,然后再行处理,这里就不在重复编码了。
2.然后我们看,这个加密方式是有一个码表的,那么我们就可以自由的更改码表,让在线的解密平无法解密,或者解密出的明文不对。
3.那么如何识别base64,==号可以换成别的,码表也可以换,但是加密逻辑是固定的(换了就不叫base64加密了)。
例如:3个字符加密后是4个,4个字符加密后是8个,且末尾两个字符相同,无论密文位数怎么改变,都是4的倍数,并且相同字符加密后的密文绝对一样,例如输入6个1,输出每三个字符必然一样!
0x4通过OD分析base64加密
1.首先还是常规思路,准备一个base64加密的cm程序!
编译器vs2015,xp可运行。成功会得到压缩包的解压密码,失败就直接退出了。
2.打开OD载入程序,这里我输入00401000,转过去
00401000 . B9 F8CD4200 mov ecx,demo01.0042CDF8
00401005 . E8 21270000 call demo01.0040372B
0040100A . 68 61D74100 push demo01.0041D761
0040100F . E8 4C500000 call demo01.00406060
00401014 . 59 pop ecx ; kernel32.7C817077
00401015 . C3 retn
3.那么我们该怎么找程序入口点呢?
方法一:搜字符串,缺点如果字符串太多不太容易分辨
方法二:直接让程序跑起来,下API断点回溯过去,找到段首
方法三:直接F8让程序跑起来,然后丢到IDA F5分析,如果还是系统api,那么记下跑飞的地方,F7进去然后继续F8,重复以上步骤
4.跑一遍,随便输入个123456,然后发现经过一个与0xA的比较后,提示输入的字符要大于10,那我就输入01234567890,开始分析
00401F2F . 0F1005 649542>movups xmm0,dqword ptr ds:[0x429564] ; ZDNkM0xqVXljRzlxYVdVdVkyND0=
00401F81 . E8 09900000 call demo01.0040AF8F ;这里就要输入字符串了
00401F8B . E8 F00E0000 call demo01.00402E80
00401F90 . 50 push eax ; 这些都是干扰指令
00401F91 . E8 DA110000 call demo01.00403170
00401F96 . 8D5424 74 lea edx,dword ptr ss:[esp+0x74] ; 取出01234567890给edx
00401F9A . 83C4 0C add esp,0xC ; 销毁局部变量,平衡堆栈
00401F9D . 8D4A 01 lea ecx,dword ptr ds:[edx+0x1] ; 指针右移1位,将1234567890保存给ecx
00401FA0 > 8A02 mov al,byte ptr ds:[edx] ;接下来这几行就是计算字符串长度给edx
00401FA2 . 42 inc edx
00401FA3 . 84C0 test al,al
00401FA5 .^ 75 F9 jnz short demo01.00401FA0
00401FA9 . 83FA 0A cmp edx,0xA ;如果小于等于10就跳向失败
00401FAC . 0F86 AA000000 jbe demo01.0040205C
00401FBB . 8D4C24 70 lea ecx,dword ptr ss:[esp+0x70] ; 将字符串给ecx
00401FBF . E8 CC000000 call demo01.00402090 ;处理我们输入的字符串,加密函数
00401FE2 . 8D4C24 08 lea ecx,dword ptr ss:[esp+0x8] ;将最开始看到的ZDNkM0xqVXljRzlxYVdVdVkyND0=给ecx
00401FE6 . E8 A5020000 call demo01.00402290 ;处理ZDNkM0xqVXljRzlxYVdVdVkyND0=,解密函数
00401FEE . 8D8C24 D00000>lea ecx,dword ptr ss:[esp+0xD0] ;看到这里就知道是strcmp函数了,如果不知道自己写个strcmp看看反编译的结果是不是这样
00401FF5 . 8D8424 380100>lea eax,dword ptr ss:[esp+0x138]
00401FFC . 0f1f40 00 nop dword ptr ds:[eax]
00402000 > 8A10 mov dl,byte ptr ds:[eax]
00402002 . 3A11 cmp dl,byte ptr ds:[ecx]
00402004 . 75 1A jnz short demo01.00402020
00402006 . 84D2 test dl,dl
00402008 . 74 12 je short demo01.0040201C
0040200A . 8A50 01 mov dl,byte ptr ds:[eax+0x1]
0040200D . 3A51 01 cmp dl,byte ptr ds:[ecx+0x1]
00402010 . 75 0E jnz short demo01.00402020
00402012 . 83C0 02 add eax,0x2
00402015 . 83C1 02 add ecx,0x2
00402018 . 84D2 test dl,dl
0040201A .^ 75 E4 jnz short demo01.00402000
0040201C > 33C0 xor eax,eax
0040201E . /EB 05 jmp short demo01.00402025 ;如果上面比较的两个字符串相等会到这里
00402020 > |1BC0 sbb eax,eax
00402022 . |83C8 01 or eax,0x1
00402025 > \85C0 test eax,eax ;因为eax被清空了,所以这里显然不会跳转
00402027 . 75 46 jnz short demo01.0040206F
00402029 . 8D5424 68 lea edx,dword ptr ss:[esp+0x68] ;接下来四行是将输入的字符串显示到屏幕上
0040202D . E8 4E0E0000 call demo01.00402E80
00402032 . 50 push eax
00402033 . E8 38110000 call demo01.00403170
00402038 . 83C4 04 add esp,0x4
0040203B . 68 B4954200 push demo01.004295B4 ; pause
00402040 . E8 628D0000 call demo01.0040ADA7
5.还剩下那加密函数和解密函数,然而
004020BF |. 8B7D 08 mov edi,[arg.1] ; 局部变量1是我输入的那个字符串
004020E9 |. 85D2 test edx,edx ; edx是字符串到长度,没看到哪里运算啊
004020EB |. 0F8E 2F010000 jle demo01.00402220 ; 显然不为0,不会跳转
004020F1 |. B9 02000000 mov ecx,0x2 ; ecx的值为2
00402100 |> /83F8 63 /cmp eax,0x63 ; 初始eax=0,大于等于63时会跳走
00402103 |. |0F8D 57010000 |jge demo01.00402260
00402109 |. |03DE |add ebx,esi
0040210B |. |41 |inc ecx
0040210C |. |03CB |add ecx,ebx
0040210E |. |3BCA |cmp ecx,edx ; 初始ecx是3,大于11则下面跳走
00402110 |. |7F 61 |jg short demo01.00402173
00402112 |. |0FBE0B |movsx ecx,byte ptr ds:[ebx] ; 取字符串第一个字母0,对应ASCII,0x30
00402115 |. |C1F9 02 |sar ecx,0x2 ; 算术右移2位,也就是除以2
00402118 |. |0FB64C0D B0 |movzx ecx,byte ptr ss:[ebp+ecx-0x50] ; 将M赋值给ecx,为什么是M呢?第十六个字母是M, ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
0040211D |. |880C07 |mov byte ptr ds:[edi+eax],cl ; 再将M存储到edi中
00402120 |. |0FBE13 |movsx edx,byte ptr ds:[ebx] ; 将这个0给edx
00402123 |. |8B5D AC |mov ebx,[local.21] ; 还是将字符串给ebx
00402126 |. |83E2 03 |and edx,0x3 ; 将edx和0x3进行与运算
00402129 |. |C1E2 04 |shl edx,0x4 ; 将edx逻辑左移4位,也就是乘以16
0040212C |. |0FBE4C1E 01 |movsx ecx,byte ptr ds:[esi+ebx+0x1] ; 再取出字符串第二个字符1,对应ascii,0x31
00402131 |. |C1F9 04 |sar ecx,0x4 ; 将其算术右移2位,也就是除以4
00402134 |. |0BD1 |or edx,ecx ; edx和ecx进行或运算,值为3
00402136 |. |0FB64C15 B0 |movzx ecx,byte ptr ss:[ebp+edx-0x50] ; 这里很明显就是取第四个字符D
0040213B |. |884C07 01 |mov byte ptr ds:[edi+eax+0x1],cl ; 将D存储到edi+1中
0040213F |. |0FBE541E 01 |movsx edx,byte ptr ds:[esi+ebx+0x1] ; 将这个1给edx
00402144 |. |0FBE4C1E 02 |movsx ecx,byte ptr ds:[esi+ebx+0x2] ; 再取出字符串第三个字符2,对应ascii,0x32
00402149 |. |83E2 0F |and edx,0xF ; 将edx和0xF进行与运算
0040214C |. |C1F9 06 |sar ecx,0x6 ; 将其算术右移6位,也就是除以64
0040214F |. |C1E2 02 |shl edx,0x2 ; 将edx逻辑左移2位,也就是乘以4
00402152 |. |0BD1 |or edx,ecx ; edx和ecx进行或运算,值为4
00402154 |. |0FB64C15 B0 |movzx ecx,byte ptr ss:[ebp+edx-0x50] ; 这里很明显就是取第五个字符E
00402159 |. |884C07 02 |mov byte ptr ds:[edi+eax+0x2],cl ; 将E存储到edi+2中
0040215D |. |0FBE4C1E 02 |movsx ecx,byte ptr ds:[esi+ebx+0x2] ; 将这个2给ecx
00402162 |. |83E1 3F |and ecx,0x3F ; 将ecx和0x3F进行与运算,得到0x32,也就是50
00402165 |. |0FB64C0D B0 |movzx ecx,byte ptr ss:[ebp+ecx-0x50] ; ebp+0x32对应第51个字符y
0040216A |. |884C07 03 |mov byte ptr ds:[edi+eax+0x3],cl ; 将y存储到edi+3中
上面是取到第一轮也就是三个字符时是这么处理的,在看代码的时候不能只关注值,看esi+ebx+0x1,esi+ebx+0x2,我们就要想到数组或者连续存储结构,这里通过移动指针访问我们输入的字符串中的每一个字符,edi+eax+0x1,edi+eax+0x2这里连续存储着处理后(加密)的字符串,ebp+edx-0x50这里为什么知道取的是哪一个字符呢,直接数据窗口中跟随过去就能看到码表,像这种类似的结构多次出现时,我们要多加关注,知道它的结构,可能的含义。
4.解密部分同样按照上面的方法逐步分析出来,这里我就不再贴分析代码了,一种字符串变成另一种字符串我们要观察规律,根据经验简单判断可能的加密方式,如果特征都对得上,但是解密出来的不对,有可能就是码表等地方动了手脚,我们就需要像这样去分析算法。
0x5总结
1.这篇帖子是我学的最吃力的一次,花费了数天的时间,算法真的是让人头脑爆炸,虽然base64实现起来并不困难。
2.看原理看了很多,始终不明白base64咋回事,然后自己照着步骤一步一步去实现,最终终于知道base64是怎么加密和解密的了。
3.二进制移位是难点,光看左移一下,右移一下就加密了,完全不懂,只有把每一个字母变成二进制,模拟运算过程,才容易理解。
4.自学的,所以编码可能不规范,算法的实现有没有bug也不知道,发现主要还是缺乏二进制的概念。
5.这个我尽可能的写的很详细了,一是怕我过段时间又忘了base64咋回事,另一方面方便论坛和我一样基础差的坛友们参考学习。
参考资料:
https://zhidao.baidu.com/question/87965568.html
https://www.cnblogs.com/qianjinyan/p/9541368.html
https://blog.csdn.net/lazyer_dog/article/details/82628076
https://www.cnblogs.com/onroad/archive/2009/07/13/1522670.html
总结:楼主是个小菜鸟,离了教程啥都不会!