某校水卡数据算法分析

本文作者:@M4X
来源:90WiKi
发表时间: 2016-2-6 08:06:33

0x00 Preface

个人认为我等屌丝在大学必干的几件事:

  • 拿下学校的网站。
  • 破解水(fan)卡。
  • 拿下运营商DHCP服务器从而免费上网。

这些我都干过了,而且基本都干完了……只是学校太low,安全性实在不敢恭维,没啥写的意义,网络中心的人居然上班时间看电视剧……我都不忍心吐槽了……倒是这个热水,打开水和洗澡是共用的,洗澡的时候钱流的飞快,心痛啊,上次还在隔壁澡堂听见一人说刚冲满才用了十天就完了,怎一个坑字了得

九零有活动了,所以就挑了其中有一点科普意义的发出来,支(duo)持(na)一(yi)下(dian)九(jiu)零(piao)!另外,那么多安全论坛最喜欢九零了…每次开完电脑都要打开九零看看…都养成习惯了…希望九零长盛不衰 ^_^ 在此也提前恭祝各位新春快乐!祝九零越来越棒!

0x01 Prerequisite

MIFARE Classic card提供1k-4k的容量,我们常见的很多都是MIFARE Classic 1k卡,也就是常说的M1卡。很多学校的水卡、饭卡之类的都是M1卡。

M1卡有从0到15共16个扇区(Sector),并且每个扇区都有独立的密码,每个扇区包含从0到3共四个段(Block),每个段可以保存16字节的数据。 每个扇区的前三段用来保存数据,最后一段用来存放KEY A,KEY B和存取位(Access Bits),每张卡的第0扇区的第0段还有一个唯一标识的UID号。UID号从出厂就固化了的,一般不可改。但某宝上卖的那些UID白卡可以改(说白了就是不按规范生产的卡)。

以上这些是最基本的关于M1卡的知识,详细的大家可以Google一下,另外这方面的资料大部分都是英文的。

0x02 Detail

网上关于M1卡的破解文章已经很多了,所以一些基本的攻击方法就不讲了,主要讲的是数据算法的分析方法,基于已知数据对未知数据的猜测方法。

使用MFOCGUI破解卡密钥并导出数据后,拿到了第一个dump文件,然后再去刷点钱,再dump一次,拿到了第二个dump文件,对比一下找出金额数据。

密钥部分打码,水表已拆。图片不方便,我把关键数据贴出来,金额部分用橙色字体标注,校验位用红色字体标注,常量则是黑色不用管:

19.90
3C CF C6 07 02 30 00 00 00 00 01 00 00 01 00 2F

17.84
09 06 F8 06 08 F9 00 00 00 00 01 00 00 01 00 F8

从上面的数据可以看到,校验位(即除金额位以外的数据变动位数)一共有五个,我们首先把金额位给确定下来,怎么判断金额位呢?

金额一般是以十六进制的形式保存在卡中(也有直接十进制保存的,不过比较少),因此把十进制的金额转成十六进制的数据即可看出,这里十进制金额1990 转成十六进制为7C6,前面补个0再倒序一下即为C607。

找出金额以后,想到学校那破旧不堪的设施,于是就不管校验位,强行修改金额去试试,看看这些校验位到底有没有,然后…..

好吧,意料之中,老老实实分析吧。找出金额位后接下来就是找校验位的计算方法了,不过这里有五个校验位,有点多了,为了后面描述数据方便,我们给金额和每个校验位编个号。并且因为是两个样本,为了防止因偶然性引起的计算错误,那么再引入第三个金额为19.70的样本用来降低偶然性,主要计算还是基于19.90和17.84这两个样本。

X1 X2 P1 P2 X3 X4 X5
3C CF C6 07 02 30 00 00 00 00 01 00 00 01 00 2F (19.90) 样本一
09 06 F8 06 08 F9 00 00 00 00 01 00 00 01 00 F8 (17.84) 样本二
50 D3 B2 07 1A 2C 00 00 00 00 01 00 00 01 00 2B (19.70) 样本三

简单目测一下可以看出,X4减1即为X5的值,第三个样本的X4与X5同样满足减1的关系,当然也可以是X5加1等于X4,不过写算法的程序员应该没这么非主流吧……我们可以假设他是习惯性的从左往右做的减法。
先记下:

X5 = X4 – 1

那么也就是说X5的出现就必须依赖X4,即先有X4,才有X5。

再看仔细一点,两眼能看出

NOT X2 = X4即NOT CF = 30

再在下面两个样本试试

NOT 06 = F9
NOT D3 = 2C

OK,满足,这样又找到了X2和X4的关系:

X4 = NOT X2

一眼能看出来的地方似乎已经没有了,接着分析金额部分。经过分析发现金额异或之后再与X1异或然后把所得数NOT一下,即可得到X3

拿样本一的数据来计算则是

NOT (C6 XOR 07 XOR 3C) = 02

这样又得出一个先后关系:X3的出现必须依赖于X1。
再记下:

X3 = NOT (P1 XOR P2 XOR X1)

稍微汇总一下这三步,如下图

以样本一的数据为基础,反正都一样。电脑上找不到画这个比较顺手的…就手画了…
从图中可以看到,已经有三个校验位能算出来了,但是还剩两个校验位X1、X2没有出来,即图中的3C和CF。

在这里卡了一会,经一朋友@lkboboy提醒:

C6 + 07 + 02 = CF

瞬间明了…这个在之前居然没又一眼看出来…
再记下:

X2 = P1 + P2 + X3

然后再汇总一遍:

现在可以看到,就剩下X1这一个变量还未出来了,到了这里拖了更久,因为问了几个朋友都没想出来,没办法了,只能自己动动脑子花点时间慢慢想了。

首先把之前得出的数据的相互依赖关系给汇总一下:

X5 = X4 – 1
X4 = NOT X2
X3 = NOT (P1 XOR P2 XOR X1)
X2 = P1 + P2 + X3

通过上面的汇总可以得出结论:X1一定与金额数据即P1、P2有关,但我用遍了加、减、或、与、异或、取反运算始终得到不到一个有效的算法。

这时我想到会不会是P1和P2还牵扯到另外一个未知的HEX进行了运算,但0x00到0xFF之间这么多数难不成一个个试过去?这得试多久?正好最近学到C语言的位运算…就写个让程序帮我算好了…
C语言中位运算符有:

&(按位与)、|(按位或)、^(按位异或)、~ (按位取反)

更加具体的可以百度。代码如下:

#include <stdio.h>

int main(int argc, char const *argv[])
{
        const int target[2] = {0x3C, 0x09};        //定义计算结果,0x3C为19.90的校验位,0x09为17.84的校验位
        int p1[2] = {0xC6, 0x07};        //19.90的金额数据
        int p2[2] = {0xF8, 0x06};        //17.84的金额数据
        unsigned char result[2];        //定义数组存放运算结果,unsigned char型能很好的避免出现六个f的值
        for (int i = 0x00; i <= 0xFF; i++) {
                result[0] = ~ (p1[0] - p1[1] + i);
                result[1] = ~ (p2[0] - p2[1] + i);
                if (result[0] == target[0] && result[1] == target[1]) //判断计算结果是否与目标结算结果相等
                        printf("Founded!\ni = %#04x\n", i);        //判断成功则输出变量i的值
        }
        return 0;
}

然后我们要改的就是代码中第10行和11行的运算符号,因为也就加减和四个位运算,用Sublime text改起来特别快,试了大概五六个之后…

果然有一个未知量参与了运算,找到值了,总结计算方法为:

X1 = NOT (P1 - P2 + 04)

用样本三运验算一下:

NOT (B2 – 07 + 04) = 50

OK,没问题,至此,所有的校验位都出来了,全部汇总一遍:

X1 = NOT (P1 – P2 + 04)
X2 = P1 + P2 + X3
X3 = NOT (P1 XOR P2 XOR X1)
X4 = NOT X2
X5 = X4 – 1

算法出来以后,就是写程序来帮我们自己算了。python有个hex函数,然而我还是打算用C写(其实是我目前对python并不熟)...因为涉及到金额倒序,需要将金额给分开,我想了半天,没想出特别好的方法,简而言之,流程如下:

获取金额---->转化为十六进制字符串---->分别取前两位和后两位---->
将取得的值转化为十六进制数值型---->写函数运算

大家若是有什么比较好的方法可以在下面回复,望不吝赐教

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char *chk_if_lack(char *waiting_checking);
int get_top_two(char *total_price_pick_top_two);
int get_post_two(char *total_price_pick_post_two);
void calc_and_print(int P1, int P2);
void print_constant(void);

int main(int argc, char const *argv[])
{
        printf("A Certain IC Card Data Algorithm Reverser. By 90Sec Team\n");
        char Price[10], *Checked_Price;
        itoa(atoi(argv[1]), Price, 16);                //获取命令行参数作为金额并将其转化为十六进制字符串存入数组Price
        Checked_Price = chk_if_lack(Price);        //经测试有时金额转化为十六进制后首位为0然后被省略导致四位变三位(比如19.90的输入不经此函数处理输出为7c6),此函数用来检查此种情况
        printf("The money you just input be converted to Hex-String is: %s\n", strupr(Checked_Price)); //大写字符串输出,纯为美观 -.-
        calc_and_print(get_post_two(Checked_Price), get_top_two(Checked_Price));        //因为金额是倒序计算的,所以后两位在前
        return 0;
}

char *chk_if_lack(char *waiting_checking) //如果检测到传入的字符串小于四位(因为只有首位的0会被省略,所以为三位),就将各元素往后移一个位置,最后把0赋给首元素凑满四位;否则不做修改直接return
{
        if (strlen(waiting_checking) < 4) {
                for (int i = 5; i > 0; i--)
                        *(waiting_checking + i) = *(waiting_checking + i - 1);
                *waiting_checking = '0';
        }
        return waiting_checking;
}

int get_top_two(char *total_price_pick_top_two) //此函数用来获取金额的前两位并转化为十六进制数值型数据然后return
{
        char P_P[3];
        strncpy(P_P, total_price_pick_top_two, 2);
        return strtol(P_P, NULL, 16);
}

int get_post_two(char *total_price_pick_post_two) //与上面的函数一样,只是取金额的后两位
{        
        char P_R[3];
        strcpy(P_R, total_price_pick_post_two + 2);
        return strtol(P_R, NULL, 16);
}

void calc_and_print(int P1, int P2)
{
        printf("After reversed: %02X %02X\n", P1, P2);        //输出倒序后的值
        unsigned char X1, X2, X3, X4, X5;    //初始化各校验位
        X1 = ~ (P1 - P2 + 0x04);   // 校
        X3 = ~ (P1 ^ P2 ^ X1);     // 验
        X2 = P1 + P2 + X3;         // 位
        X4 = ~ X2;                 // 算
        X5 = X4 - 0x01;            // 法
        printf("All of check bits is: %02X %02X %02X %02X %02X\n", X1, X2, X3, X4, X5);        //输出计算好的各校验位的值
        puts("The complete datas you should copy and then write in the card is:");
        printf("%02X %02X %02X %02X %02X %02X ", X1, X2, P1, P2, X3, X4);                //组合各变量与金额并输出
        print_constant();        //此函数用来输出扇区数据中各00或01的常量
        printf("%02X\nGood fortune.", X5);        //输出最后一位校验位
}

void print_constant(void)
{
        for (int i = 0; i < 9; i++) {
                if (i == 4 || i == 7)
                        printf("%02X ", 0x01);
                else
                        printf("%02X ", 0x00);
        }
}

应该没有过分模块化吧...

然后就是验算:

OK,没问题

最后算个100的数据

然后copy

image

接着write

image

最后

关于金额,本来想写个FFFF即655.36的,没想到超出100就报错 = = 学校药丸…至于金额为什么会少了点…说出来让大家笑一笑…

当我晚上偷偷摸摸(必须偷偷摸摸啊,你见过谁洗澡掏手机的?)的进澡堂,然后把卡一插,手机一掏。不知道哪个XXX没有关热水水龙头,就要拍照的时候一股“洪水”袭来,吓得我差点没把手机摔了,结果我身上都湿了,尼玛的…然后赶紧把卡拔了关了水龙头再插再拍照… 这张照片也许能成为我未来回忆大学生活最好的“缓存”了吧...

0x03 Conclusion

本篇文章旨在为大家提供一个通用的IC卡数据分析思路,当然,这张卡的校验位是纯计算出来的,不排除有些卡会记录刷卡次数或者时间混淆视听……同时,个人有个思路即可以通过程序来实现算法的自动猜测,即输入金额,然后输入目标hex,程序内置一些简单的运算过程如异或和取反来进行尝试,可以省去大量时间。

目前我的水平还不够…看以后能否实现。

1 Like

直接看得自闭:rofl:

:laughing: 哈哈 算法分析 是看的有些头大