至此,攻防世界得新手题全部做完。但是我感觉还有很多可以挖掘学习得点,比如通过动调解答最前面得几道题等等。
一直在路上呢~~
0x01 re1
由上图可发现是32位程序,故使用ida_32打开
alt+T查找关键字flag
双击进入此指令所在的函数处
按下f5,汇编转伪c
容易发现,1处为v5赋值,2处读入v9(也就是flag),3处比较v5和v9,相同则输出flag。
因此flag存储于&xmmword_413E34处,双击此处,跳转其所在的地址
选择上图的两个大数,按下r键转化成字符串
flag到手,要倒着读。
0x02 game
打开程序,发现程序的意思是有八盏灯,开始都关着,输入一个数(0-8),将它和与它临近的两盏灯按下开关(灯亮变灯灭,灯灭变灯亮),如输入1,则8,1,2这三盏灯由灭变亮。当八盏灯全部亮时,拿到flag
拖进ida pro 7.0后,alt+T查找flag关键字,双击跟进
进入此处
选择涂黄处的变量,按下x键,查看交叉引用(就是谁调用了它的意思)
点击ok跟进
找到此函数的开始的地方,选择涂黄处,x键查看交叉引用并跟进
再次查看交叉引用并跟进
可以分析这就是判断8个数是否同时为1的函数(很明显8个代码段流向最后的代码段)
f5查看伪c代码,关键代码:
显然开始时有8个0,每次输入一个数字i(0-8),对i-1,i,i+1这三个位置上取反(0变成1,1变成0),当8个位置均为1时拿到flag。
有两种做法
方法一
可以看出,8个数全为1时,调用函数sub_457AB4,所以flag大概率在这函数里。查看交叉引用并跟进,发现另一个函数
继续跟进
进入到函数
f5之后,发现先是定义了v59-v115和v2-v58,并给出了具体的数值。
之后进行加密的关键代码:
加密十分简单,分别以v2,v59为起始位置建立数组a,b
a[i] = (a[i] ^ b[i]) ^ 0x13
将形如v40 = 107;的那段赋值代码复制下来,保存在本地文件中,写出将他们的格式转化进python列表并进行加密的脚本
1 | import re |
方法二
回到刚刚提到的这个判断八盏灯是否全亮的函数
你会发现,这个就是先判断第一盏灯是否为1,为1则判断第二盏,一直判断到第8盏,其中判断的关键语句是
它使用的jnz,判断标志位是否为0,如果不为0则跳转。
具体意思我们不太需要知道,只需要知道,如果我们把这个判断条件取反,就可以直接拿到flag。
为什么呢?你可以想一下,如果真,则拿到flag。那么我们现在改了条件,如果假,则拿到flag。是不是不用满足条件就可拿到flag。
那么怎么取反呢?与jnz相对应的有个jz,如果标志位为0则跳转。
我们选择jnz,再在菜单栏找到Edit,从下拉菜单中找到Patch program(patch,补丁),再选择change byte
弹出窗口
第一个字节是75,jnz是75,jz是74,所以我们将75改为74
接着你会发现,jnz改成了jz
接着我们要Edit->Patch program->apply patches to input file
,这样才能将改动保存进原来的程序里
弹窗点击ok即可
注意,你在保存的时候,一定不要正在运行着所要修改的程序,否则会提示没有权限(就类似于你在打开文件时无法删除文件一样),如下图
那我们再来分析一下,打开程序后,我们必须要输入一个数字,这会使相邻的三盏灯变为1,而其他的5盏灯仍为0,按照原先的逻辑,这并不会给你flag,但是我们可以提前将这判断这5盏灯的逻辑反转,全部改成灯为0则为真,这样我们再次运行程序时,把应该点亮的灯点亮了,程序进判断,这三盏灯亮,其余5盏灯均不亮,满足为真的逻辑,直接给你flag。
修改后的逻辑:
所以我们不修改1,2,3这三盏灯,只修改后面的5盏灯,将jnz改为jz,然后Edit->Patch program->apply patches to input file
,将改动保存进原来的程序里。
为什么我要选择不修改1,2,3?因为你会发现,灯2-8的函数结构相同,都是这样的,将下图的7换个数字
而第一盏的函数
特别判断的那条语句
和之前看到的75开头的不同。
为了减少不必要的分析,所以我直接选择1,2,3,这样就不必修改1号灯了。
修改并apply patches to input file后,我们直接运行程序,输入2(亮起1,2,3)
拿到flag
顺便写一下动态调试
最初想用动调解题,毕竟flag的加密与输入无关,后来想了下,要想跳转到加密那一步,先得八盏灯全亮,所以放弃了。但是此处还是写一下动态调试的过程吧,权当学习。
IDA PRO 7.0不能动态调试win32的程序,会报错1491,这是官方承认的bug,在7.0sp1及7.2中已经修复
所以这里我用ida6.8plus
先找到判断8盏灯的函数,记录此函数的地址(函数名sub_84F400,故地址为84F400)
按f9(一下没反应就按两下)
这是提醒你动调过程可能会出错,点击yes
此时界面(请右键在新页面打开图片,不然字体太小)
鼠标点击一下下面的这个窗口,按g并输入地址
点击ok即可跳转至:
f5转c
在55行下断点(因为下一步就是要判断8盏灯的状态,这一步之前一定将灯的变化,即0/1的转化写入了数据中了)
观察到byte_922E28,所以第一个数据位于地址922E28处。
点击hex-view窗口,按g并输入地址,跳转
可以发现现在都是0
输入1并回车,在ida中按f9单步运行(每运行一步都要按一次f9),可以发现数据已经变了
补充
题解中的地址未必和你的地址是完全相同的。我做了好几次,有时候相同的函数地址确实是不同的,也有时候是相同的。
刚打开拖进文件进来的时候,要等它加载一会,如果紧接着动调的话,可能根据地址搜索不到函数
0x03 Hello, CTF
题目描述:菜鸡发现Flag似乎并不一定是明文比较的
主函数:
可以看出v13的字符串大概率是base16,直接转码,得到的flag是CrackMeJustForFun,并不需要添加{},也算是奇葩
看题解,题解是将字符串转换为十六进制,看来base16加密就是把字符串转化为对应得十六进制构成得字符串
0x04 open-source
此题直接给出一段源代码
1 |
|
可以分析,难道hash需要三个值,第一个已知为0xcafe,第三个为字符串长度。
关键是找到第二个:模5不为3,且模17为8的数(注意:满足模5为3,且模17不为8的结果时exit),但是不需要求出这个数,因为求hash时,我们只需要知道second % 17
即可,显然second % 17 = 8
print('%#x'%(0xcafe * 31337 + 8 * 11 + len('h4cky0u') - 1615810207))
c语言的printf中的%x
是输出十六进制或字符串的地址。
python与之对应的是%#x
最后输出0xc0ffee,flag是c0ffee,没有0x(因为如果将第二个数求出来后,运行c语言的这个程序,结果是c0ffee,我现在还不会如何运行这种带有控制台参数的程序,所以就不先将就吧)
0x05 simple-unpack
题目描述:菜鸡拿到了一个被加壳的二进制文件
发现加壳了,upx壳,最下面的一个文本框提示try unpack with "upx.exe -d" from http://upx.sf.net
将upx所在路径加入PATH中后,在cmd中
upx -d D:\桌面\b7cf4629544f4e759d690100c3f96caa
不会生成新文件,而是在原来程序的基础上脱壳。
现在已经没有壳了,而且发现是64位ELF,拖进ida64
flag在很明显的地方
0x06 logmein
在ida中打开,并将变量名改为实际意义的变量名(选择变量名,按n键,按照你容易理解的方式命名)
可以看出算法十分简单,就是s1和s2逐个字符异或,s2到字符串末尾的时候,再从字符串头开始,循环。
python脚本
1 | s1 = r':"AL_RT^L*.?+6/46' |
有两个致命的问题我卡住了,看的题解才知道:
1、ida中汇编转c的代码中,s1 = ":\"AL_RT^L*.?+6/46"
,注意此时的字符串是c风格的,所以实际的字符串实际是:"AL_RT^L*.?+6/46
,没有反斜杠,反斜杠加上双引号在中转义成双引号
2、题解原话:由于程序是小段的存储方式,所以,ebmarah就得变成harambe
我的理解:s2看着像字符串,但其实不是。因为__int64 s2; s2 = 'ebmarah';
看着像字符串是因为ida中转换的,按下h键就自然显示成了十六进制。为什么上面的s1不用反着读?因为人家真的是字符串,把鼠标放到具体的那个字符串上,会显示
flag是RC3-2016-XORISGUD
0x07 insanity
题目描述:菜鸡觉得前面的题目太难了,来个简单的缓一下
转c后搞不明白flag会在哪里,alt+t输入flag查找字符串后会有惊喜,直接拿到flag
确实很简单,但我想麻烦了,一直就纠结与主程序的代码的意思。
0x08 no-strings-attached
看的题解才做出了来,最初一直想栈里的数据怎么定位到具体的内存,寻思着是不是找基址找偏移量,后来才反应过来不必去栈内找变量的数据,因为最后一定会出栈,此时自然可以找到具体的存储地址(在群里问学长时,学长建议我看看汇编的参数传值那部分,果然基础还是不行)
(回过头来补充:上面的话不太行,其实是动态地找ebp)
主函数内
分别进入四个函数中看一下,很容易看出,关键代码在authenticate中,其中关键代码
可以分析,decrypt对字符串加密后存入eax中(我自己做的时候不知道要可虑这一点,一个劲的想从栈内拿到加密后的字符串)
接着有两种做法。
gdb动态调试(题解做法)
将程序拷贝至kali,打开终端
以下操作的选中处为输入的命令
在函数decrypt处下断电,b是break的意思,给程序下断点
r是run的意思,运行程序至断点处
n表示运行单步(遇到函数,则不单步进入函数内部,而是把一个函数看作一步)
根据上图,decrypt函数return的值在eax中,所以我们
x表示查看内存,/后面的200表示要显示的内存单元的个数,w是四字节,x是十六进制
黄线处值为\x00,字符串在此截断,所以我们将之前的数据提取出来,写出python脚本
1 | import codecs |
注意:
python2中str.encode(‘hex’)
python3中codecs.decode(str,’hex’)
一些gdb的命令https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/gdb.html
静态调试(不可以)
按自己的理解给decrypt中的变量改了一下名
可以看出算法,申请了一块地址(注意,此处数据并非为空,我就是把他当成全为0了,才看不懂后面的-=)形成字符串new_str,这就是待会return的字符串,然后进行new_str[i]处的数据减去dword_8048A90[i]处的数据,然后再赋值给new_str[i])
注意这里的dword_8048A90是我改的变量名,因为我看到原先的这个变量其实是decrypt的第二个参数,而正是dword_8048A90传入了第二个参数。这个源字符串的地址找了了,那么new_str的地址呢?我们会发现如果再decrypt内双击wchar_t或是new_str,都只会进入在栈内的偏移地址。
(顺便提一下wchar_t是C/C++的字符类型,是一种扩展的存储方式。wchar_t类型主要用在国际化程序的实现中,但它不等同于unicode编码。unicode编码的字符一般以wchar_t类型存储。wcscpy计算宽字符的长度)
那我们不妨回到调用decrypt的上级函数,还记得之前的那个源字符串dword_8048A90吗,我们到地址0x8048A90处去看看
你发现了什么?在dword_8048A90处的下方,正好有一个wchar_t类型的字符串。很容易想到,是因为变量的开辟一般就是顺着地址来的。在HEX窗口分别进入0x08048A90和0x08048AA8,查看数据
不对头,做不下去了,以后技术提高后再回来补吧,暂时此题先用gdb做
—-打通新手关后回来补题—-
经过分析,加密后的字符串在eax中,压入地址为[ebp+s_len]的内存处,而ebp是动态分配的,只有程序运行时才会确定,所以只能用动态调试解题。
pwbgdb解此题http://blog.eonew.cn/archives/898
0x09 getit
静态调试
主函数如下:
关键加密段
需要说一下的是
*(&t + (signed int)v5 + 10) = s[(signed int)v5] + v3;
相当于
t[i+10] = s[i] + v3;
t的值为
注意,t的起始地址是0x6010E0,而字符串harifCTF中的h的地址是0x6010E1,所以在h之前还有一个字符,只是没在这端代码中体现出来,打开hex窗口,按g跟进该地址
t的第一个字符是S。第一次提交flag时我提交的是harifCTF开头的flag,不错才怪嘞。坑死我啦。
加密逻辑十分简单,不解释,直接复制代码用c也能拿到加密后的字符串。python代码:
1 | s = 'c61b68366edeb7bdce3c6820314b7498' |
还得说一说主函数的后半段,一开始拿到代码时很懵,我还以为遇到新知识了,没想到分析了一下,就是忽悠人的。
flag写入文件是在23行,后面的for循环跟flag没得关系
动态调试
文件放入阿里云CentOS,开启linux——server64服务。本地进行远程动态调试,根据之前的信息,得到主函数地址0x400756和flag地址0x6010E0,在IDAview窗口跳转到0x400756,并转c,在HEXview窗口跳转至0x6010E0
你会发现在linux下反汇编的c代码和前面静调时在window下反汇编的c代码结构基本相同,但是变量名、函数名并不完全相同。
linux下的函数更加难懂,所以我们可以对照着之前的代码,对应出此时的代码的不同函数的作用。可以对应出此时的第38行至第41行,就是之前的第33行的return 0。所以我们可以把断点下在第38行,此时没有return,数据还存在。
下好断点后,f9运行至断点,拿到flag
0x0A python-trade
在南邮做过,直接将题解复制过来
下载py文件,在线反编译https://tool.lu/pyc/
1 | import base64 |
据此写出解密脚本
1 | import base64 |
0x0B csaw2013reversing2
题目描述:听说运行就能拿到Flag,不过菜鸡运行的结果不知道为什么是乱码
win32程序,打开后弹窗,不过是乱码
ida打开,主函数为
MessageBoxA的弹窗函数,第一个参数为窗口句柄,第二个参数是弹窗的主体内容,第三个参数是弹窗标题,第四个参数指示对话框的内容和行为(包括不限于指示消息框中显示图标)。MessageBoxA用于ANSI字符串,MessageBoxW用于Widechars(即Unicode)字符串。
一开始我没有仔细地看主函数的代码,误以为字符串是Unicode,猜测是因为使用了MessageBoxA才导致的乱码。后来看了题解才知道猜错了。
再来看一下主函数,第10行进判断,sub_40102A的返回值恒为0,可以pass,IsDebuggerPresent()是一个反调试的函数,如果正在使用调试器调试程序,则返回值为真。
可以看出,程序如果正在被调试器调试,则进行一段加密(解密),然后再弹窗。如果没有调试器调试,则直接弹窗。
使用od打开程序。
由于ida基址是0x401000,主函数入口地址0x40103A,od的基址是0x851000,所以od中主函数的入库地址再0x85103A。原理就是偏移地址相同。(注意od的地址是动态的,打开两次,同一地方的地址并不相同,只需要计算好偏移地址即可)
同理可以找到调用IsDebuggerPresent()函数是在0x85108c.
按说我在用od调试程序,按照IsDebuggerPresent()的作用,应该不会直接跳转至0x8510b9(弹窗函数),而是应该顺着进行加密(解密)(即0x851096往后),但是这一段是灰的,表示流程中直接略过了这一段,并不会运行。我猜测可能是我用的这款吾爱破解版的od具有反IsDebuggerPresent()函数的功能。搞不清楚,但是我们得想办法进行进行加密(解密)。
可以得知,程序是运行到0x851094处,进行了判断。je和jnz都是根据零标志位是否为0来进行跳转。ZF寄存器里面保存得就是零标志位的值,ZF在od的寄存器窗口中简写为Z。
我们在0x851094处下断点,f9运行至此处,发现此时零标志位为1
双击Z后面的这个1,就会变成0。原来应该直接跳转至弹窗窗口,但是由于此时零标志位反转,所以不会跳转,而是顺着运行,进行加密(解密)。
但是如果你再按f9,程序会崩溃。
这是因为加密段有个int3指令
我们在调试程序时下的断点,默认情况下都是调试器把我们下断点处的语句替换成了int3指令,运行时捕捉这个错误,然后再把int3这个指令替换成原来的那条指令。
要想不出错,我们可以手动把0x85109a处的int3替换成nop指令,空指令。
替换很简单,双击int3指令,在弹窗中输入nop即可。
回到程序崩溃前的那一步,此时我们的指令停在了0x85094处。
然后一直f8,单步运行程序。
运行完0x85109b(此时EIP指向0x85109e),将地址并放入edx,下一句就是调用加密函数sub_851000(0x85109e处),那么基本上就可以确定edx存的地址处就是加密前的字符串喽。
根据上图得知edx值为031d05b8,在hex窗口按ctrl+g,弹窗中输入031d05b8,即可到达此地址处
再按f8,单步运行,运行完加密函数(0x85109e处),可以发现
拿到flag。
如果想弹窗中输出flag,也是可以的。
ida中主函数的一部分如下图:
可以看出加密函数实现后,并没有转向弹窗函数。所以我们可以把加密函数最后一句跳转至4010EF改为跳转至4010B9。
新的流程图如下
我们来到0x8510a3处,可以看出此处跳转至0x8510ef处,我们修改为跳转至 0x8510a5处即可(弹窗函数第一条指令push 2 的地址)
所以最终在od中需要改动3处地方。
红色的是改动的。
然后右键菜单中找到复制到可执行文件->所有修改
选择全部复制
然后在弹出的窗口中右键菜单中选择保存文件
打开刚刚生成的新exe文件
0x0C maze
题目描述:菜鸡想要走出菜狗设计的迷宫
这是没见过的船新版本,拿到题后一脸懵逼,毫无思路。看了题解后才慢慢理解“迷宫”的意思。
主函数如下:(变量名、函数名被我根据实际作用更改过了,更容易理解)
1 | __int64 __fastcall MEMORY[0x4006B0](__int64 a1, char **a2, char **a3) |
12行输入flag,13行判断要满足三个条件才能继续:长度24、以nctf{开头、以}结尾。、
然后面临四个分支:O o . 0 分别是向左 向右 向上 向下。
怎么看出来的呢?
来到69行,首先解释一下SHIDWORD
#define SHIDWORD(x) (*((int32*)&(x)+1))
https://www.cnblogs.com/goodhacker/p/7692443.html
可以看出SHIDWORD是取x的下一单元的地址。(单元的意思是如果x是int,那就取下一个int,而不是下一个字节)
迷宫一定要有x,y两个坐标,根据8*x+y,可以分析,location是行(因为乘8了呀),location的下一位是列。而且迷宫是8*8的
然后再来看一下四种情况的具体函数。
sub_400650和sub_400670其实是一样的,都是参数的值(传入的是地址)处的数值减一。由于400650的参数是((_DWORD *)&location + 1)
,传入的是列的地址(注意,并不是location的值加一,而是location的地址加一,即列的地址),所以是向左。而sub_400670传入的是行的地址,所以是向上。
向右和向下的原理与此类似。
程序是如何判断能不能走的呢?第57行处的函数代码为:
如果为空格或#则返回真。
好了,原理搞明白了,那就把asc_601060字符串拿出来,形成迷宫,手动写flag吧
asc_601060 db ' ******* * **** * **** * *** *# *** *** *** *********'
nctf{o0oo00O000oooo..OO}
这题主要就是代码审计吧我感觉。再者,由于SHIDWORD的不理解,我卡了很久,一个location是如何同时控制住x和y的呢?一直想不明白,没想到是取x下一个单元地址的作用。C得学习学习了。
至此,攻防世界得新手题全部做完。但是我感觉还有很多可以挖掘学习得点,比如通过动调解答最前面得几道题等等。
一直在路上呢~~