大家周末好,又到了周末时间,给分享一些轻松有趣的内容,希望大家喜欢。
去年鹅厂内部极客圈举办了第二次极客大赛,题目如下:
"实现一个世界上最小的程序来输出自身的MD5"
作为极客圈一员的我也参加了比赛,比赛竞争激烈,为了争夺一个字节的优势,大家都拿出自己的绝活,通过参加比赛,学到很多知识,重新刷新了我底层知识的理解,计算机原理的理解:
大神解法: 顶级极客技术挑战赛,你敢来挑战吗?| 大神登峰造极
接下来,给大家分享一些不走寻常路的野路子方案,欢迎大家点评讨论,脑洞大开。
野路子解法1-md5碰撞原理及实现
人间正道是沧桑
一. 原理
已知A, 可以生成相同长度, 内容不同的X和Y, 使得md5(A+X)=md5(A+Y)
已知A, B, X, Y, md5(A)=md5(B), md5(A+X)=md5(A+Y), 则md5(A+X)=md5(B+X)=md5(A+Y)=md5(B+Y)
二. 构造步骤
通过工具
(http://www.win.tue.nl/hashclash/fastcoll_v1.0.0.5_source.zip),
可以构造出上面的X和Y:
$ echo Hello > A
$ echo World > B
$ ./fastcoll -p A -o AX1 AY1
MD5 collision generator v1.5
by Marc Stevens (http://www.win.tue.nl/hashclash/)
Using output filenames: 'AX1' and 'AY1'
Using prefixfile: 'A'
Using initial value: 7be4ac1acaf95d7b8f73709af7db5c10
Generating first block: ..
Generating second block: S01......
Running time: 4.53125 s
$ cat AX1 | md5
dbbd16368b2eb0f6596004ace403e012
$ cat AY1 | md5
dbbd16368b2eb0f6596004ace403e012
$ cat AX1 B | md5sum
8b6ddb8f03cd3f7fe63f288f6688018a
$ cat AY1 B | md5sum
8b6ddb8f03cd3f7fe63f288f6688018a
这样我们就有了指定1个bit的能力 (同时文件也会增大128字节).
循环执行128次:
fastcoll A -o AX1 AY1
cut_tail_128 AX1 > X1
cut_tail_128 AY1 > Y1
fastcoll AX1 -o AX1X2 AX1Y2
cut_tail_128 AX1X2 > X2
cut_tail_128 AX1Y2 > Y2
...
每次生成的2个文件, 取末尾128字节, 得到X[i]和Y[i];
然后就可以根据md5反推出Xi/Yi该选哪个了
三. Show Me The Code
test.c:
#include <stdio.h>
char const reserve[32] = "RR...",
md5coll[128][128] = { "MM...", "MM...", ... },
chkpos[128] = "PP...", chkval[128] = "VV...";
int main() {
int i, j, p1, p2, x;
for (i = 0; i != 16; ++i) {
x = 0;
for (j = 0; j != 8; ++j) {
x <<= 1;
p1 = i * 8 + j;
p2 = ((unsigned char)chkpos[p1]) % 128;
if (md5coll[p1][p2] == chkval[p1])
x |= 1;
}
printf("%02x", x);
}
printf("\n");
return 0;
}
先编译test.c得到test, 再想办法填充里面的md5coll, chkpos, chkval, 就可以达到反推md5的效果
讲解一下算法:
整个程序分为5部分:
elf头等
reserve数组, 用来64字节对齐
md5coll数组, 长度128*128, 用来放上面算出来的X[i]/Y[i]
chkpos/chkval数组, 用来查md5coll, 反推出自身md5输出
main函数及其它
阶段一:
编译确定1 2 5
阶段二:
跑128次fastcoll, 对比X[i]和Y[i]的差异, 可以构造出Y[i][chkpos[i]] == chkval[i], 从而算出chkpos和chkval
阶段三:
将X[i]填充到md5coll数组, 至此可执行程序的md5就不会变了
根据md5的二进制位, 填充对应的Xi或者Yi
这样就可以构造出能算出自身md5的程序了。
野路子解法2-捡"漏"
始从正道,因久攻不克,遂智取
邪念初生
最初是题目描述中的一个细节提醒了我:
这里禁用socket,想必是为了防止对外通信,反过来说,如果能突破通信的限制,确实就可以不用自己辛苦计算结果了,通信的另一方直接把结果塞给它就好了。
投石问路
要想对外通信,不管用何种通信方式,都不免要通过一些系统调用的,所以先用一些不太常用的系统调用试探一波,看有没有漏网的系统调用:
int main() {
int sv[2];
int ret = socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
if (ret != 0)
{
*(int *)0 = 0;
}
printf("xxx\n");
return 0;
}
这里有一点要说明一下,就是每次提交结果,如果不通过,系统会有比较详细的反馈(大概有使用了禁止的系统调用、crash,结果错误、返回值不为0这几个),这个有助于我们试探信息。注意到上面代码第6行,这里会故意crash,以此区分socketpair在没有被禁止的前提下,这个调用究竟有没有成功。
上面程序的提交结果是Wrong Answer,表明这个调用成功了,证明确实有些系统调用没有被禁止,此事可搞。
首战不利
最初想到的是利用共享内存来传递信息:
准备两个可执行程序A和B
先提交A,A的功能是把MD5(B)写到共享内存
接着再提交B,B从共享内存读取A留下的内容并输出即可达到目的。
以下是A的代码:
int main() {
int id = shmget(0x32147658, 1024, IPC_CREAT | 0666);
if (id == -1) *(int *)0 = 0;
char *data = (char *)shmat(id, 0, 0);
if (!data) return 2;
memcpy(data, "9afd0fbf6a61da4d64e92095476716ec", 32);//这串即是B的md5
int ret = shmdt(data);
if (ret == -1)return 3;
return 0;
}
然后是B的:
int main() {
int id = shmget(0x32147658, 1024, IPC_EXCL);
char *data = (char *)shmat(id, 0, 0);
write(1, data, 32);
_exit(0);
}
由于B做的事情及其简单,所以体积很容易做得很小。
然而,想法是很美好,提交A的时候却得到 ”非法系统调用“。很简单,共享内存相关的系统调用被禁了,浪费了上面十几行代码。
再战又不利
接着又想到利用文件来传递信息:
还是两个可执行程序A和B
A把MD5(B)写到一个文件
B读出来输出
想得依然很美,但试遍系统中所有可能成功的地方(/tmp /run /var /dev等),都无法找到一个可以创建文件的目录,或者一个可读可写的文件,此路依然不通。
三战还不利
这时还没有放弃文件的思路,而且想了个比较粗暴的招(也是实在没办法了):
还是倒霉的A和同样倒霉的B
A遍历整个文件系统,找出可以创建文件的地方创建文件,或者找到一个可读可写的文件,写入内容
B以同样的方式遍历文件系统,必能找到A之前留下的内容,读之并输出
试了一下提交A居然成功了,但提交B的时候显示Wrong Answer。此时怀疑是踩到了/dev下的某些设备文件,它们是可读可写的,但语义不是普通文件的语义,读出来的和写入的内容不一样,典型的如/dev/uramdom,写入的语义是给系统的随机数生成器贡献随机事件,而读取则是获取随机数。
有错就改,在A中添加校验信息:
A先写一个固定串TEST,再写如MD5(B)
B在读取的时候先看能不能读到TEST,如果能的话,则继续读取后面的内容
加上校验之后,A可以成功找到通过校验的文件,而B则始终无法读到校验信息TEST。。。
这里终于开始怀疑判分系统除了禁用系统调用,还可能做了更深的隔离,并不会在同一个环境里测试不同的可执行文件,路路不通,一度打算放弃。
山穷水尽
放弃之前,照例是要再挣扎一番的,文件不行,再看看有没有其它途径,然后又试了另一种进程间通信方式fifo(命名管道),mknod(创建fifo的系统调用)系统调用确实没有被禁,但fifo是存在于文件系统中的,无法创建文件,也就无法创建fifo,此法不靠谱,越想越绝望。
峰回路转
此时大概只剩最后一种进程间通信方式了:消息队列。死马当活马医吧,反正这条路也快到头了,不管结果怎么样,能解脱是肯定的,直接上代码,A的:
int main(int argc, char **argv)
{
int msqid;
int key=0xDEADDEAD;
msqid = msgget(key, 0600 | IPC_CREAT);
char buf[TEXT_SIZE];
strcpy(buf, "eb861d89e1c8e775c39dab7878216093");
int flag = msgsnd(msqid, buf, TEXT_SIZE, 0);
if (flag < 0)
{
perror("send messag eerror");
return -1;
}
return 0;
}
B的:
int main(int argc, char **argv)
{
int msqid;
int key=0xDEADDEAD;
msqid = msgget(key, 0600);
char buf[TEXT_SIZE];
int flag = msgrcv(msqid, buf, TEXT_SIZE, 0, 0);
write(1, buf, TEXT_SIZE);
_exit(0);
}
苦心人,天不负,就这样,成功了,,,也就是说,系统禁了很多系统调用,唯独忽略了消息队列。
上面的这个程序,稍加裁剪便达到了310字节,成功到顶一游。
查漏补缺
印象中Linux的进程间通信方式普遍有多个版本,除了上面的消息队列,会不会还有漏网之鱼呢,再次翻阅系统调用表,果然发现了另一个版本的消息队列:
mq_open
mq_timedreceive
mq_timedsend
这个应该也是可以利用的,不过没有尝试,连同上面的msgget一起,找组委会自首,请求原谅。。
看完记得一键三连在看,转发,点赞
是对文章最大的赞赏,感谢
推荐阅读