有些人不走寻常路

分享了一次内部极客大赛中的创意解法,利用MD5碰撞原理和不寻常路径,如文件操作、系统调用限制突破、进程间通信等技巧,构建出世界上最小输出自身MD5的程序。展示了技术高手如何逆向工程和创新思维解决技术难题。
摘要由CSDN通过智能技术生成

大家周末好,又到了周末时间,给分享一些轻松有趣的内容,希望大家喜欢。

去年鹅厂内部极客圈举办了第二次极客大赛,题目如下:

"实现一个世界上最小的程序来输出自身的MD5"

作为极客圈一员的我也参加了比赛,比赛竞争激烈,为了争夺一个字节的优势,大家都拿出自己的绝活,通过参加比赛,学到很多知识,重新刷新了我底层知识的理解,计算机原理的理解:

题目介绍和常规解法:最小MD5挑战赛,你敢来战吗?

大神解法: 顶级极客技术挑战赛,你敢来挑战吗?| 大神登峰造极

接下来,给大家分享一些不走寻常路的野路子方案,欢迎大家点评讨论,脑洞大开。

野路子解法1-md5碰撞原理及实现

人间正道是沧桑

一. 原理

  1. 已知A, 可以生成相同长度, 内容不同的X和Y, 使得md5(A+X)=md5(A+Y)

  2. 已知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部分:

  1. elf头等

  2. reserve数组, 用来64字节对齐

  3. md5coll数组, 长度128*128, 用来放上面算出来的X[i]/Y[i]

  4. chkpos/chkval数组, 用来查md5coll, 反推出自身md5输出

  5. main函数及其它

阶段一:

  1. 编译确定1 2 5

阶段二:

  1. 跑128次fastcoll, 对比X[i]和Y[i]的差异, 可以构造出Y[i][chkpos[i]] == chkval[i], 从而算出chkpos和chkval

阶段三:

  1. 将X[i]填充到md5coll数组, 至此可执行程序的md5就不会变了

  2. 根据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,表明这个调用成功了,证明确实有些系统调用没有被禁止,此事可搞。

首战不利

最初想到的是利用共享内存来传递信息:

  1. 准备两个可执行程序A和B

  2. 先提交A,A的功能是把MD5(B)写到共享内存

  3. 接着再提交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的时候却得到 ”非法系统调用“。很简单,共享内存相关的系统调用被禁了,浪费了上面十几行代码。

再战又不利

接着又想到利用文件来传递信息:

  1. 还是两个可执行程序A和B

  2. A把MD5(B)写到一个文件

  3. B读出来输出

想得依然很美,但试遍系统中所有可能成功的地方(/tmp /run /var /dev等),都无法找到一个可以创建文件的目录,或者一个可读可写的文件,此路依然不通。

三战还不利

这时还没有放弃文件的思路,而且想了个比较粗暴的招(也是实在没办法了):

  1. 还是倒霉的A和同样倒霉的B

  2. A遍历整个文件系统,找出可以创建文件的地方创建文件,或者找到一个可读可写的文件,写入内容

  3. B以同样的方式遍历文件系统,必能找到A之前留下的内容,读之并输出

试了一下提交A居然成功了,但提交B的时候显示Wrong Answer。此时怀疑是踩到了/dev下的某些设备文件,它们是可读可写的,但语义不是普通文件的语义,读出来的和写入的内容不一样,典型的如/dev/uramdom,写入的语义是给系统的随机数生成器贡献随机事件,而读取则是获取随机数。

有错就改,在A中添加校验信息:

  1. A先写一个固定串TEST,再写如MD5(B)

  2. B在读取的时候先看能不能读到TEST,如果能的话,则继续读取后面的内容

  3. 加上校验之后,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一起,找组委会自首,请求原谅。。

看完记得一键三连在看转发,点赞

是对文章最大的赞赏,感谢

推荐阅读

顶级极客技术挑战赛,你敢来挑战吗?| 大神登峰造极

Linux Kernel TCP/IP Stack|Linux网络硬核系列

顶级C程序员之路

sha碰撞,MD5碰撞实现,#!/usr/local/bin/perl # It was noted that Intel IA-32 C compiler generates code which # performs ~30% *faster* on P4 CPU than original *hand-coded* # SHA1 assembler implementation. To address this problem (and # prove that humans are still better than machines:-), the # original code was overhauled, which resulted in following # performance changes: # # compared with original compared with Intel cc # assembler impl. generated code # Pentium -25% +37% # PIII/AMD +8% +16% # P4 +85%(!) +45% # # As you can see Pentium came out as looser:-( Yet I reckoned that # improvement on P4 outweights the loss and incorporate this # re-tuned code to 0.9.7 and later. # ---------------------------------------------------------------- # Those who for any particular reason absolutely must score on # Pentium can replace this module with one from 0.9.6 distribution. # This "offer" shall be revoked the moment programming interface to # this module is changed, in which case this paragraph should be # removed. # ---------------------------------------------------------------- # $normal=0; push(@INC,"perlasm","../../perlasm"); require "x86asm.pl"; &asm_init($ARGV[0],"sha1-586.pl",$ARGV[$#ARGV] eq "386"); $A="eax"; $B="ecx"; $C="ebx"; $D="edx"; $E="edi"; $T="esi"; $tmp1="ebp"; $off=9*4; @K=(0x5a827999,0x6ed9eba1,0x8f1bbcdc,0xca62c1d6); &sha1_block_data("sha1_block_asm_data_order"); &asm_finish(); sub Nn { local($p)=@_; local(%n)=($A,$T,$B,$A,$C,$B,$D,$C,$E,$D,$T,$E); return($n{$p}); } sub Np { local($p)=@_; local(%n)=($A,$T,$B,$A,$C,$B,$D,$C,$E,$D,$T,$E); local(%n)=($A,$B,$B,$C,$C,$D,$D,$E,$E,$T,$T,$A); return($n{$p}); } sub Na { local($n)=@_; return( (($n )&0x0f), (($n+ 2)&0x0f), (($n+ 8)&0x0f), (($n+13)&0x0f), (($n+ 1)&0x0f)); } sub X_expand { local($in)=@_; &comment("First, load the words onto the stack in network byte order"); for ($i=0; $i<16; $i+=2) { &mov($A,&DWP(($i+0)*4,$in,"",0));# unless $i == 0; &mov($B,&DWP(($i+1)*4,$in,"
MD5是目前最热门的加密算法,我们通常用MD5值来验证文件的完整性。例如在一些比较正规的下载网站,通常会提供软件的MD5值,这样我们就可以对下载回来的文件用MD5校检软件(如HashX等)做一次MD5校验,以确保我们获得的文件与该站点提供的文件为同一文件。但当两个不同文件的MD5值完全一样时,你还会信任MD5吗? 找出破解MD5加密方法的专家是我国山东大学的王小云教授,这则新闻在以前的软件版块曾详细报道过。但之后MD5的破解一直没有进展,直到最近,国外的科学家研究出了新的MD5碰撞破解方法,可以让两个不同文件的MD5值完全一样,而之前我们一直认为一个文件的MD5值在世界上是独一无二的,这就像一个人克隆了你的指纹然后冒充你一样恐怖! 为了验证MD5值的独一无二性,我们来做一个简单的试验: 在桌面上新建一个文本文档,文件名为“test.txt”,内容为“OfficeBa”。然后将这个文本文档拖动到校验工具HashX中,点击左上角的“Hash File”按钮,得到其MD5值为051cb2917a5b70505e1687dee449c765,然后为文档中的“OfficeBa”加上双引号,保存后再通过HashX进行校检,发现MD5值变成了9ab117400993b70bc9945a9b15749d5d了。可见,一个极细微的变动都会导致文件的MD5值不同! 那么我们能让两个程序文件的MD5一致,却又都能正常运行,并且可以做完全不同的事情么?答案是:“可以!”。要让两个不同文件的MD5值相同,可以通过一款名为fastcoll的小工具来完成我们同样以刚才的test.txt来做试验: -h [--help] 显示选项 -q [--quiet] 简化 -i [-ihv] arg 使用指定的初始值,默认是md5初始值 -p [-prefixfile] arg 使用给定的前缀计算初始值,仍然把数据复制到输出文件中(必须是个文件名) -o [--out] arg 指定输出文件名,此选项必须是最后一个参数,而且两个文件名必须同时指定 默认的是 -o msg1.bin msg2.bin 把解压出来的fastcoll_v1.0.0.5.exe与test.txt放在同一目录,然后在“命令提示符”中输入:“fastcoll_v1.0.0.5.exe -i test.txt -p test.txt -o cbi.exe cbi2.exe”并回车,在同目录中会生成名为cbi.exe和cbi2.exe文件,我们用HashX校验他们的MD5值,可以发现是完全一样的,但是在HashX中用“SHA-1”加密算法进行校验的时候,结果竟然是不同的(SHA-1加密算法生成的结果也是独一无二的)!可见这已经是完全不同的两个文件,但是他们的MD5值竟然完全相同。 如果黑客从网上下载一个工具,给其捆绑上木马,然后通过工具让其MD5值和原文件一样。那么当用户下载了文件后用MD5校验工具进行校验时就会发现带毒文件和原文件MD5值完全一样,就会放心地去运行,结果可想而知。所以,MD5加密已经不再可信!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值