实验参考资料:
Decoding Lab: Understanding a Secret Message
You have just intercepted an encoded message. The message is a sequence of bits which reads as follows in hexadecimal:
6363636363636363724646636F6D6F72 466D203A65693A7243646E206F54540A 5920453A54756F0A6F6F470A21643A6F 594E2020206F776F797275744563200A 6F786F686E6963736C206765796C656B 2C3365737420346E20216F74726F5966 7565636F202061206C61676374206C6F 20206F74747865656561727632727463 6E617920680A64746F69766120646E69 21687467630020656C6C786178742078 6578206F727478787863617800783174
You have no idea how to decode it, but you know that your grade depends on it, so you are willing to do anything to extract the message. Fortunately, one of your many agents on the field has stolen the source code for the decoder. This agent (007) has put the code and the message in the file secret.cpp, which you can download from the laboratory of your technical staff (Q).
Q has noticed that the decoder takes four integers as arguments. Executing the decoder with various arguments seems to either crash the program or produce unintelligible output. It seems that the correct four integers have to be chosen in order for the program to produce the decoded message. These four integers are the “secret keys.”
007 has been unable to find the keys, but from the desk of the encrypting personnel he was able to cunningly retrieve the first five characters of the unencoded message. These characters are:
From:
Assignment
Your assignment is to decode the message, and find the keys. Reminders This exercise is not extremely difficult. However, the strategy of trying things until something works will be ineffective. Try to understand the material in the course, particularly the following:
Strategy
The designers of this decoder weren’t very good. They made it possible for us to attack the keys in two independent parts. Try to break the first two keys first, and do not try to break the third and fourth keys until you have succeeded with the first two.
You can do the first part by specifying only two integer arguments when you execute the decoder. If you get the first and second keys right, a message that starts with From: will appear. This message is not the true message, but a decoy. It is useful, however, to let you know that you have indeed broken the first two keys.
In breaking the first two keys, realize that the function process_keys12 must be somehow changing the value of the dummy variable. This must be so, because the variables start and stride control the extraction of the message, and they are calculated from the value of dummy.
In breaking the third and fourth keys, try to get the code to invoke extract_message2 instead of extract_message1. This modification must somehow be controlled from within the function process_keys34.
Files
When you are done, write a brief report that includes at least the following:
Be precise, clear, and brief in each of the points above. Your report should not, in any case, be longer than one page. Do not get frustrated if this takes a little longer than you expected: brief and clear text often requires more time to write than rambling prose.
Your teacher can tell you what word processors you may use to write your report. Chances are that you can write your report in a number of formats, and for simplicity’s sake, you might even want to write it using Notepad. Enjoy!
这么大一段英文,简直了!看起来比看电视剧还费劲!不过,我们也不要着急,跟着我慢慢来看!
我们慢慢来看,看看代码,无非就是把data[]数组转换一下,保存到message数组里,所以我们来一步一步地分析这段代码。
从源代码来看,我们可以看到,msg1是由start和stride的值决定的。那么start和stride怎么确定呢?
从这里我们可以看到,start和stride的值是由dummy的值决定的。由于dummy是int型的,所以
如果就这样来看的话,那我们很容易得到start=1, stride=0,这个带入到extract_message1()中,显然是不正确的。那么我们一定是通过某种方式对dummy的值进行了修改。
好,我们再来看指导书:
这样我们就可以很容易看出很容易看出是process_keys12(&key1, &key2)这样一个方法,通过修改相对的内存地址,来达到对dummy值得修改。好,这里搞明白了,我们接下来看这个函数:
用通俗一点的话说呢(我的c语言也很差劲,因为也是自己实验课的缘故,所以稍微看了一点,可能某些说的不准确,希望大家见谅):
也就是说,(int *)(key1 + *key1)的值即等于dummy变量的地址值。(当然,这里key1是main中key1的地址,*key1等于main中的key1)。可见偏移量就是*key1的值。
那么,如何确定key1的值呢?这个我们可以在main函数中确认。
变量声明的顺序如上图所示,根据,这样在指针层面key1(main中)=&dummy-&key1,在本题中这个值很容易看出来,因为它们之间就有3个int型的变量,所以key1=3。
如果这样不够明白,那么我们只需要通过 debug找出 key1和 dummy的距离(这个距离就是通过 *key1来赋值的)即可,也就是看内存中这两个变量的地址差,再除以4同样得到key1的值。
这里值得注意的一点是,用vs调试时候发现这几个连续声明的int型变脸的内存地址竟然不是连续,间隔不是4,而是12,多用的8个字节不知道做什么了,应该是默认的编译选项,要修改编译选项的话,大家可以参照下面这篇博文, http://blog.csdn.net/pngfiwang/article/details/49624845。当然如果不修改编译选项,这时候要让key1=9了;
我在测试的时候,没有进行修改,所以我令key1 = 9。
我写了一段代码,如下:
大家可以用它来进行测试。
好,终于求出来key1=9,我们的实验也已经完成了1/4了, 开心。
我们继续看指导书:
重点就是这个From:
我们返回头看这个函数,从上面代码可以得知,key2的值也就是dummy的值。只有解出start和stride才能知道dummy应该满足什么条件。所以我们要分析start和stride各等于多少时才使输出以From:开头。
除此之外,我们还要分析extract_message1(start, stride):
该方法的作用就是把一个int型的数组中的每一个int数值转换为4个字符,最终从得到的char数组中读取部分字符放入数组message中,当读到‘\0’字符时结束。其中参数:
因此,我们可以使用如下代码进行测试:
输出结果,如下:
结合我们队start和stride参数的分析,很容易知道,当start=9,stride=3时候符合情况。读两个Fr隔一个再读om,再隔一个,读:
Emmmm,这样看来,结合我们分析出来的
我们可以看出dummy为:
最高地址 —— ——- 最低地址
———— ———— 03 09
也就是说dummy的值只要后两个字节满足03,09(这里是以十六进制表示的)的情况就可以了。
因为内存中是按2进制来保存的,因此就符合16进制的计算规则。所以满足题目的key2值就可以有多个了,列举几个777,66313,131849等等无穷尽。
好,我们在VS中测试一下,设置命令行参数为 9 777,看输出结果:
Nice,key2=777,搞定了!!!
key3和key4的求值,可能比较难以理解,大家如果碰到不懂的地方,可以去百度一下基本原理,我相信大家也都会看懂的。
一样的套路,继续看指导书:
也就是说,我们需要想办法使程序进入并执行 msg2 = extract_message2(start, stride); 而进入并执行这段代码的条件是:*msg1 = ‘\0’
但是,正常来看,我们是无法使*msg1=’\0’的。那怎么办呢?emmm很容易想到,我们需要通过执行process_keys34()这个函数,悄无声息地改变msg1的值。对,就是这样。要不然,这段代码干嘛用的呢?
也就是上面这段代码。既然要先执行process_key34(),那么首先我们可以确定key3和key4都不等于0。这也算是一大突破啦~~~
好,我们继续来看process_key34这个函数。
我的天,这是什么东东~~~~嘿嘿,不要害怕,我们来分析一下。
1. 获得形参key3的地址。
2. 转为一个int型指针并加上一个常数(key3的值),也就是偏移到内存中另一个地址。
3. 然后修改了这处地址上的值。
这是干嘛?不懂,那我们也先不急着说明它。
这时候,小明同学就问我?那修改了start和stride的值,也可能让msg1为’\0’啊?
嘿嘿嘿,答案是nonono。
在说明原因之前,我们先来看看extract_message2都干了什么?
类似于extract_message1(),把一个int型的数组中的每一个int数值转换为4个字符,最终从得到的char数组中读取部分字符放入数组message中,当读到‘\0’字符时结束。参数呢:
好,明白了extract_message2干嘛用的,我们来回答小明的问题。
我们这样想啊,如果是修改了start的值,那么让*msg=’\0’的情况就是直接从char数组中’\0’的位置读取。好,没毛病,继续。’\0’的ASCII码是0,那么我们看看data数组。
很明显只有最后一个数组0x00783174的前两位等于0,也就是转成的char数组的最后一个字符时’\0’,如果是这样extract_message2方法得到的字符串肯是没有值的。再来看stride,很明显只有让stride<=1,那么extract_message2肯定不会得到我们要的结果。
好,解决了小明的问题,我们可以得出:
我们的程序不是顺序执行的,在某个时刻发生了跳转。厉害了啊!
好嘛,肯定是process_key34()使程序发生了跳转。怎么跳呢?回头看看指导书。
Emmmm,这好说,就是修改了函数的返回地址了嘛。OK。好,那么跳转到哪里呢?
这也是很容易看出来的,肯定跳过了if条件语句内的process_keys34(&key3, &key4);语句,否则程序将陷入死循环。
(这里,至于如何获取一个函数的起始地址和返回地址,大家可以参照http://5412097.blog.51cto.com/5402097/1641374/)
然后,接下来就需要我们理解函数调用堆栈的原理啦。来看一张图:
可能很难理解(我也是)。我们来看看调用process_key34函数之前整个栈帧结构中数据的存储细节。首先是将参数 key3、 key4压入栈中保存,然后在传入这些参数。然而这些参数的传递都是传地址的,所以我们在 process_keys34中可以通过 &key3的相对定位来修改调用函数的栈帧数据。好,最后差不多就是这样:
既然要修改函数的返回地址,我们返回头看,结合process_key34:
就可以看出key3 = -1。也就是让key3偏移一个地址,到返回地址。
最后,我们就剩key4了,我们可以看出,process_key34就是修改我们返回地址里所保存的值。
这个,还是看汇编代码吧:(获取汇编代码的方式,在VS中,通过加断点的方式调试,然后依次 调试->窗口->反汇编 即可)
if (key3 != 0 && key4 != 0) {
000C1CE2 cmp dword ptr [key3],0
000C1CE6 je main+13Eh (0C1CFEh)
000C1CE8 cmp dword ptr [key4],0
000C1CEC je main+13Eh (0C1CFEh)
process_keys34(&key3, &key4);
000C1CEE lea eax,[key4]
000C1CF1 push eax
000C1CF2 lea ecx,[key3]
000C1CF5 push ecx
000C1CF6 call process_keys34 (0C1168h)
000C1CFB add esp,8
}
msg1 = extract_message1(start, stride);
000C1CFE mov eax,dword ptr [stride]
000C1D01 push eax
000C1D02 mov ecx,dword ptr [start]
000C1D05 push ecx
000C1D06 call extract_message1 (0C1172h)
000C1D0B add esp,8
000C1D0E mov dword ptr [msg1],eax
if (*msg1 == '\0') {
000C1D11 mov eax,dword ptr [msg1]
000C1D14 movsx ecx,byte ptr [eax]
000C1D17 test ecx,ecx
000C1D19 jne main+191h (0C1D51h)
process_keys34(&key3, &key4);
000C1D1B lea eax,[key4]
000C1D1E push eax
000C1D1F lea ecx,[key3]
000C1D22 push ecx
000C1D23 call process_keys34 (0C1168h)
000C1D28 add esp,8
我们可以看到原先的返回地址和我们需要抵达的返回地址。
这样,就可以计算出key4=000C1D28-000C1CFB=793896 – 793851 = 45;
终于,皇天不负苦心人啊,慢慢磨还是可以磨出来的。
最终结果就是:Key1 = 9,key2 = 777, key3 = -1, key4 = 45
好,我们来进行一下测试,看看这段折磨人的密文到底是什么?
YES!!! YES!!! YES!!!
最后附上,我写的word文档,大家可以选择性的下载,里面的内容和博客里的差不多。
Lab Decoding Lab-分析文档
Lab Decoding Lab-secret.cpp
Lab Decoding Lab-English-Analysis