学了几周的网络了,感觉可以开始搞一搞安全方面的内容了,之前我们说了数据包在网络中的重要性,每个人都可以通过任意构造数据包来任意欺骗任何设备。就算是不懂协议,不懂如何构造数据包的小白,只要会用一些工具,仍然可以进行网络攻击,这种只会借助工具输入命令然后让工具任意进行自动攻击的人也被叫做脚本小子,他们可能连最基础的网关路由程序语言都不懂。而就是这种愣头青造成了全世界最多的网络安全事故。想要保护我们的信息,首先就要防止它被人在网络上截取,就算被截取,也要保证没人能看懂。所以编码和加密就出现了。
编码
编码其实就是一种翻译规则,将字符替换成某些特定的字符,比如“你好=hello”, “蒂姆=tim”就是一种编码,这样当我们传输的参数是“你好蒂姆”时别人就只能看到“hello tim”,如果一个人不懂英语就会一脸懵逼。以前谍战电视剧里不是经常演抢密码本的情节吗,现在的谍战天天混上流社会和靠脑补推理,已经没这些情节了。有的编码甚至可以直接在语言上误导对方,比如将hello mr. sam替换为damn you kal,当对方发现得到的结果是可以读通的,就会下意识地觉得这是原本的意思,而不会去思考字符被替换的可能。当然这只是一种思路,简单来说编码一定是一一对应的,只要知道编码规则,解码就极其简单。那么编码到底有什么用呢,第一句话就说了嘛,翻译。我们在电脑上看见的是汉字而不是01就是编码的结果,将计算机内部的语言翻译成各种其它语言,只要保证我们想要传递信息的对象能看懂即可,不然为什么英语的编码叫encoding。而且还有种用法是多重编码,只要不知道编码顺序就没法看懂,但是这样会有很大的问题,后面逆向时会讲怎么利用这个漏洞。至于怎么编码,怎么解码,百度一搜就知道了,没什么好说的。
加密
我们说到编码其实是一种翻译规则,这种规则是公开的,就好像我们都知道“hello”是“你好”而不是“去你的”一旦人学会了英语,他们就可以做一个翻译器,今后就可以直接靠翻译器交流而不需要重新学一门语言。编码也是一样,我们不需要知道编码规则具体是怎样的,只要知道它的名字就可以依靠各种工具随意编码解码。所以,编码是极度不安全的,一个小白完全可以依靠解码工具读出编码的内容,百度上一堆,于是我们就需要一个新的方法来让传输的字符更加不可控,于是就有了加密。
加密一般有两种,对称和非对称。对称很好理解,我们注册账号需要输入密码,注册后登录账号还是需要输入注册时的密码,加密解密使用同一个码,这就是对称。同理可证,非对称就是加密解码不同码,比如我分享一个百度云文件,提取码是1234,别人看到的其实我账号的一部分,但是绝对不可能靠这个提取码去登录我的账号以看到全部文件,因为我的密码和公开的提取码不是同一个。和编码的区别在于,编码是一种公开规则,是希望有人能看懂的,但是加密的目的就是让人看不懂,而且几乎不可逆。
知道了原理,我们来看看加密是如何工作的,先从对称加密开始,这里以DES为例子:
- 随便来段字符W = 1234567890abcdef,再来一个密钥key=a860e329be97a923。注意,任何字符最终都会在计算机上变成数字,这个过程所依赖的就是编码,无论输入的是英语还是中文,所以不存在文字怎么加密的情况,文字只是我们看到的像素画而已,计算机读到的不是文字。
- 然后我们把上面的字符变成二进制得到:B = 00010010 00110100 01010110 01111000 10010000 10101011 11001101 11101111。
- 然后同样的,把key拿来转换了得到:10101000 01100000 11100011 00101001 10111110 10010111 10101001 00100011
- 然后建立一张8x8的表,1-64作为序号填进去,然后去掉最后一列只留前面7列,拆成两半,然后开始置换,前面一半从第一列从下往上数57 49 41...,后面一半从63开始往上数,依次将数字按从左往右的第一行去填入。要注意保留原表方便我们看这些格子原本的序号,这个代表的是key转换成2进制后每个数字的序号。
1, 0 2, 0 3, 1 4, 0 5, 1 6, 0 7, 0 8, 0 9, 0 10, 1 11, 1 12, 0 13, 0 14, 0 15, 0 16, 0 17, 1 18, 1 19, 1 20, 0 21, 0 22, 0 23, 1 24, 1 25, 0 26, 0 27, 1 28, 0 29, 1 30, 0 31, 0 32, 1 33, 1 34, 0 35, 1 36, 1 37, 1 38, 1 39, 1 40, 0 41, 1 42, 0 43, 0 44, 1 45, 0 46, 1 47, 1 48, 1 49, 1 50, 0 51, 1 52, 0 53, 1 54, 0 55, 0 56, 1 57, 0 58, 0 59, 1 60, 0 61, 0 62, 0 63, 1 64, 1 57 49 41 33 25 17 9 8 1 58 50 42 34 26 18 16 10 2 59 51 43 35 27 24 19 11 3 60 52 44 36 32 63 55 47 39 31 23 15 40 7 62 54 46 38 30 22 48 14 6 61 53 45 37 29 56 21 13 5 28 20 12 4 64 -
于是我们得到了新的序号,那么把原key按照这个新序号变化一下,得到了key0 = 0111010 0000001 1011011 1110011 1011010 0101100 0001011 0010000,然后再给这key1拆成两半,C0= 0111010 0000001 1011011 1110011;D0= 1011010 0101100 0001011 0010000
-
然后我们开始将C0,D0的数字进行位移,也就是每个数字向左移动一位,第一位放到最后,每移动一次0+1,所以C1就是1110100 0000011 0110111 1100110,D1=0110100 1011000 0010110 0100001。组合起来就得到了C1D1 = 1110100 0000011 0110111 1100110 0110100 1011000 0010110 0100001。CnDn代表C和D分别向左位移了n次。这个n是可以随意设置的,一般是0-16,28次就相当于还原了,没什么意义。
-
然后我们再次得到一个顺序表,接着按照这个顺序来再次取数字排列CnDn,排序一次得到key1,排序n次得到keyn。所以key1 = 110011 100010 110010 101111 001010 101111 001100 000100。这个表是固定的,我不知道它是怎么来的,可能是通过反复实验固定下来的,或者基于某些数学原理来确定的,百度,AI都搜不出答案。要是谁知道原理可以说一下。
14 17 11 24 1 5 3 28 15 6 21 10 23 19 12 4 26 8 16 7 27 20 13 2 42 52 31 37 47 55 30 40 51 45 33 48 44 49 39 56 34 53 46 42 50 36 29 32 -
好了我们开始加密,终于要用到明文B了,还是老套路,1-64的表格,这次我们跳着列从下往上读列,从58 50开始,得到11001100 00011111 11000110 11100000 11110000 10101010 11101000 10100101
1, 0 2, 0 3, 0 4, 1 5, 0 6, 0 7, 1 8, 0 9, 0 10, 0 11, 1 12, 1 13, 0 14, 1 15, 0 16, 0 17, 0 18, 1 19, 0 20, 1 21, 0 22, 1 23, 1 24, 0 25, 0 26, 1 27, 1 28, 1 29, 1 30, 0 31, 0 32, 0 33, 1 34, 0 35, 0 36, 1 37, 0 38, 0 39, 0 40, 0 41, 1 42, 0 43, 1 44, 0 45, 1 46, 0 47, 1 48, 1 49, 1 50, 1 51, 0 52, 0 53, 1 54, 1 55, 0 56, 1 57, 1 58, 1 59, 1 60, 0 61, 1 62, 1 63, 1 64, 1 - 接下来,仍然分为两半,L0=11001100 00011111 11000110 11100000,R0=11110000 10101010 11101000 10100101,然后开始计算L1=R0,R1=L0+f(R0, key1),这里的f()是一个扩展,怎么扩展呢,现在R0不是只有32位吗,来继续列个表,保证位数和key1相同。每一行都会重复上一行最后两位再按顺序排列。同理,我们得到了E(R0)=111110 100001 010101 010101 011101 010001 010100 001011,然后E(R0)XORkey1=001101 000011 100111 111010 010111 111110 011000 001111,XOR计算是同一位上相等为0不等为1。0xor1=1
32 1 2 3 4 5 4 5 6 7 8 9 8 9 10 11 12 13 12 13 14 15 16 17 16 17 18 19 20 21 20 21 22 23 24 25 24 25 26 27 28 29 28 29 30 31 32 1 - 好了重点来了,现在我们得到的E(R0)+key1是一个8段的6bit值,那么每段我们取出头尾组成一个数,换算成十进制,中间的4位数也换算为10进制,得到一个横纵坐标。比如001101,取头尾01,中间0110,换算得(1,6),因为这个是第一段,所以我们要去第一张表S1的1行6列看看是哪个数,然后替换原来的二进制。所以得到了(1,6), (1,1), (3, 3), (2, 13), (1, 11), (2, 15), (0, 12), (1,7),然后查表就好。S盒太多了就不列了,总之结果是13 13 0 2 10 6 5 4,分别转换为二进制 1101 1101 0000 0010 1010 0110 0101 0100
-
最后将上面一步得到的32bit再拿去进行换位,根据P表来,同样的操作,同样的排序得到R1=0000 0011 1111 1000 1100 0000 1011 1010
16 7 20 21 29 12 28 17 1 15 23 26 5 18 31 10 2 8 24 14 32 27 3 9 19 13 30 6 22 11 4 25 -
将L1=R0=1111 0000 1010 1010 1110 1000 1010 0101和R1拼在一起得到11110000 10101010 11101000 10100101 00000011 11111000 11000000 10111010,转换为16进制得到被加密的明文f0aae8a503f8c0ba,加密完成。
从上面的例子来看,对称加密的核心就是通过复杂的运算过程同时增加大量的子密钥,也就是D1C1这种,使得解密所需的成本大大提高。我单是检查错误都感觉脑袋快炸了,因为有个进制转换网站算出来的结果是错的,它最后9位永远是0,最后不得不自己写程序转一遍,再去比对另外的网站的结果。但是对称加密的解密方同样需要逆向上面的步骤来,一旦密钥被偷取,整个加密过程就可逆。对称加密最大的问题在于,很多加密编码过程是在客户端进行的,如果想要加密,那就必须要生成密钥,而且解密也必须要这个密钥,也就是说密钥会随着文件一起传输,这是极度不安全的。因此,对称加密解决的问题其实是针对公开编码和明文的直接爆破。
接下来我们看看非对称的代表RSA:
了解RSA之前我们先看看它的核心,欧拉函数:
- 假设有一个数为z,有n个数小于等于它且和它互质表示为
。互质是指相互之间的公约数只有1。注意,这里指的是个数,而不是互质数,1和任何数都互质。
- 如果说两个数p和q本身都是质数,那么p*q不互质的也就是p的1-q个倍数、q的1-p个倍数以及p*q自己,排除掉这些剩下的数都和它互质,所以
- 模反数是指如果两个数a和n互质,则选取一个数b使得ab除以n的余数为1,或者ab-1可以被n整除,记录为
,这个b一定存在,这里就不证明了,这是根据欧拉函数和费马小定理推出的结果。
好了,开始加密吧
- 首先要求出两个密钥
选取两个质数p和q n = p*q = 7*3 = 21 z = (p-1)*(q-1) = 6*2 = 12 找一个数d和z互质,d<12,得到解密用的私钥 d = 5 私钥secret key(d, n) sk(5, 21) 再找一个数e = 17来求加密用的公钥 d*e == 1(mod z) 5*17/12=7——1 于是我们得到公钥public key(e, n) pk(17, 21)
- 然后开始加密和解密
加密明文m=3 c = mod(m^e, n) = mod(3^17, 21) = 12 解密 m = mod(c^d, n) = mod(12^5, 21) = 3
从上面的步骤可以看出,RSA使用了质因数这个没有规律、无法计算、只能穷举的东西使得逆向工程的工作量和计算量大大提升。而且,在RSA的交换流程下,一个算法只能生成一对公私钥,只有公钥才会参与传输,而私钥是保存在本地的。具体流程是这样:
- 客户端发送了一个会话请求,服务器返回加密套件和自己的公钥,此时没有内容传输
- 然后客户端会根据加密套件自己生成一对客户的公私钥,然后根据服务器的公钥将传输文件进行加密,最后把自己的公钥打包在里面一起发过去。这样服务器就可以用自己的私钥读自己公钥加密的内容。
- 服务响应也是一样,服务器根据客户端提供的公钥来加密内容,同时打包自己的公钥。这样客户也能根据自己的私钥读懂自己公钥加密的文件,并且还能用服务器的公钥再次加密请求。
发现问题了吗,私钥在整个流程中根本不参与任何的传输,只保存在本地,连网卡都不过。知道了加密套件因为难以逆向所以没太大用,知道了公钥也没用,因为这个加密内容根本不是用传输内容上的公钥加密的。所以,非对称加密被认为是安全的,能破解它的只有算力和人为错误。
逆向
随着密码的出现,逆向变成了每一个黑客的梦想,逆向其实不是数学,不需要懂算法,不需要会反推代码;而是一种社会学。最基础的密码破解其实就是穷举,因为最早使用密码锁的设备上密码就那么几位,使用穷举是可以举出来的。后来密码变复杂了,似乎变得很难暴力破解,但是对用户本身而言,过于复杂的密码本身也是在为难自己,为了那么点价值的账号搞几十位大小写数字字符组合的密码完全没必要,每次输入时都是在为难自己,所以大家会把复杂的密码记录在某个地方,可是这样被偷的概率比简单靠脑子记的密码还高,于是干脆设一些简单好记的密码。一些军事设备的密码也会比较简单,为的是能够快速使用,否则一旦忘记那就是一堆废铁。
基于此,弱口令爆破就出现了。爆破其实就是在使用一个预设的、猜测出来的字典,将里面的内容一点一点反复通过数据包发送给服务器,以求得一次成功的响应,说白了就是有一定依据的模糊的穷举。比如大家喜欢用生日设密码,那就收集这个人亲近的所有人的名字和生日然后将字符排列组合起来一个一个试就行了。爆破不仅仅限于密码,数据包中任何内容都可以用来爆破,包括文件、请求、参数、域名等等,范围极广,只要胆子够大,思维够活跃,万物皆可。
但是爆破和逆向有什么关系呢?在抓包的过程中,我们经常会发现出现看不懂的加密编码字符,以前我们拿这个毫无办法,或者有办法逆向但是极其麻烦。现在我们在分析了加密内容的传输流程后会发现一个问题,那就是无论何种加密和编码,保护的都是传输过程,所有的加密解密流程都是在本地完成的,传输出去之后就和本地没有任何关系,加密的那一串字符跟加密没有半毛钱的关系,就是字符而已。服务器读取的并不是原始参数,而是加密字符,能读懂是因为它会解密。所以逆向的方法其实并不是去搞算法,也不是反向分析代码,更不需要一字一句全部读懂。我们需要的是在抓到数据包后,找到可以修改的注入的地方,将我们的文字用本地存在的加密数据包加密一遍,只要发现最终结果相同,那就说明我们成功掌握了字符转换规则,那么,只要将我们的字典全部加密一遍,就变成了专门针对这个网站的爆破字典,这种方法会很比较麻烦,但是绝对是目前效率最高的排查方法,而且是纯手动,比自动化成功的概率会更高。加密方法是现成的,我们要做的只是写脚本而已。不会写脚本?找AI呀。看不懂代码?学呀。
这里是我从网站上获取编码规则后手写的一个脚本,js文件是网页脚本,python是利用网页编码规则来编码自己字段的方法。至于爆破,后面再讲,因为现实中远远没有这么简单。
//array.js,我只修改了最前面的输入内容的部分,把整个文件打包成一个函数方便调用,因为源文件不能批量处理数组。
function allarray(inputArray) {
var base64 = new Base64();
var INTEXT2 =[]
inputArray.forEach(function (item) {
if (typeof item === 'string') {
var base64Encoded = base64.encode(item);
var encodedResult = encodeURIComponent(base64Encoded);
INTEXT2.push(encodedResult);
} else {
console.error('不是字符串', item);
}
});
return INTEXT2
function Base64() {
// private property
_keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
// public method for encoding
this.encode = function (input) {
var output = new Array();
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = _utf8_encode(input);
while (i < input.length) {
chr1 = input[i++];
chr2 = input[i++];
chr3 = input[i++];
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output.push(_keyStr.charAt(enc1) + _keyStr.charAt(enc2) + _keyStr.charAt(enc3) + _keyStr.charAt(enc4));
}
return output.join('');
}
// public method for decoding
this.decode = function (input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = _keyStr.indexOf(input.charAt(i++));
enc2 = _keyStr.indexOf(input.charAt(i++));
enc3 = _keyStr.indexOf(input.charAt(i++));
enc4 = _keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
}
output = _utf8_decode(output);
return output;
}
_utf8_encode = function (string) {
string = string.replace(/\r\n/g, "\n");
var utftext = new Array();
var utftextlen = 0;
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext[utftextlen++] = c;
} else if ((c > 127) && (c < 2048)) {
utftext[utftextlen++] = (c >> 6) | 192;
utftext[utftextlen++] = (c & 63) | 128;
} else {
utftext[utftextlen++] = (c >> 12) | 224;
utftext[utftextlen++] = ((c >> 6) & 63) | 128;
utftext[utftextlen++] = (c & 63) | 128;
}
}
return utftext;
}
_utf8_decode = function (utftext) {
var string = "";
var i = 0;
var c = c1 = c2 = 0;
while (i < utftext.length) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
} else if ((c > 191) && (c < 224)) {
c2 = utftext.charCodeAt(i + 1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
} else {
c2 = utftext.charCodeAt(i + 1);
c3 = utftext.charCodeAt(i + 2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
}
_exchange_character = function (charArray) {
charArray = charArray.split("");
for (var i = 0; i < charArray.length; i++) {
if (i > 0 && i % 2 == 0) {
var c = charArray[i];
charArray[i] = charArray[i - 1];
charArray[i - 1] = c;
}
}
return charArray.join("");
}
this.encodePostParam = function (input) {
input = this.encode(input).split("").reverse().join("");
return _exchange_character(input);
}
this.decodePostParam = function (input) {
input = _exchange_character(input).split("").reverse().join("");
return this.decode(input);
}
}
}
import execjs
#文件读取
def read_file_to_array(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
lines = file.readlines()
content_array = [line.strip() for line in lines]
return content_array
#文本数组转换
def write_array_to_txt(output_array, output_file_path):
with open(output_file_path, 'w', encoding='utf-8') as file:
for item in output_array:
file.write(item + '\n')
#读取js脚本
with open('array.js','r',encoding='utf-8')as f:
js_code = f.read()
context = execjs.compile(js_code)
#文件输入
input_file_path = 'D:/work/web/sqlDict/sql.txt'
input_array = read_file_to_array(input_file_path)
#使用js处理数组
result_array = context.call('allarray', input_array)
#输出字典
output_file_path = 'D:/work/web/sqlDict/encsql.txt'
write_array_to_txt(result_array, output_file_path)
print(result_array,"all the results are in the file up there.")