至此,攻防世界得新手题全部做完。但是我感觉还有很多可以挖掘学习得点,比如通过动调解答最前面得几道题等等。
一直在路上呢~~
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列表并进行加密的脚本
import re
with open('1.txt')as f:
file = f.readlines()
list1 = []
list2 = []
list3 = []
for i,line in enumerate(file):
if i < 57:
list1.append(re.search(r'v\d+ = (\d+);',line).group(1))
else:
list2.append(re.search(r'v\d+ = (\d+);',line).group(1))
print(list1)
print(list2)
for i in range(56):
list2[i] = int(list1[i]) ^ int(list2[i])
list2[i] ^= 0x13
print(chr(list2[i]),end='')
方法二
回到刚刚提到的这个判断八盏灯是否全亮的函数
你会发现,这个就是先判断第一盏灯是否为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
此题直接给出一段源代码
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
if (argc != 4) {
printf("what?\n");
exit(1);
}
unsigned int first = atoi(argv[1]);
if (first != 0xcafe) {
printf("you are wrong, sorry.\n");
exit(2);
}
unsigned int second = atoi(argv[2]);
if (second % 5 == 3 || second % 17 != 8) {
printf("ha, you won't get it!\n");
exit(3);
}
if (strcmp("h4cky0u", argv[3])) {
printf("so close, dude!\n");
exit(4);
}
printf("Brr wrrr grr\n");
unsigned int hash = first * 31337 + (second % 17) * 11 + strlen(argv[3]) - 1615810207;
printf("Get your key: ");
printf("%x\n", hash);
return 0;
}
可以分析,难道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脚本
s1 = r':"AL_RT^L*.?+6/46'
s2 = r'harambe'
print(s1)
for i in range(len(s1)):
#print(s1[i],' ',s2[i%7])
print(chr(ord(s1[i]) ^ ord(s2[i%7])), end='')
有两个致命的问题我卡住了,看的题解才知道:
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脚本
import codecs
s = '''0x00000039 0x00000034 0x00000034 0x00000037
0x0000007b 0x00000079 0x0000006f 0x00000075
0x0000005f 0x00000061 0x00000072 0x00000065
0x0000005f 0x00000061 0x0000006e 0x0000005f
0x00000069 0x0000006e 0x00000074 0x00000065
0x00000072 0x0000006e 0x00000061 0x00000074
0x00000069 0x0000006f 0x0000006e 0x00000061
0x0000006c 0x0000005f 0x0000006d 0x00000079
0x00000073 0x00000074 0x00000065 0x00000072
0x00000079 0x0000007d '''
s = s.replace('0x000000','').replace(' ','').replace('\n','')
print(s)
f = codecs.decode(s,'hex')
print(f)
注意:
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代码:
s = 'c61b68366edeb7bdce3c6820314b7498'
flag = ''
for i in range(32):
if i & 1:
v3 = 1
else:
v3 = -1
flag += chr(ord(s[i]) + v3)
print(flag)
还得说一说主函数的后半段,一开始拿到代码时很懵,我还以为遇到新知识了,没想到分析了一下,就是忽悠人的。
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/
import base64
def encode(message):
s = ''
for i in message:
x = ord(i) ^ 32
x = x + 16
s += chr(x)
return base64.b64encode(s)
correct = 'XlNkVmtUI1MgXWBZXCFeKY+AaXNt'
flag = ''
print 'Input flag:'
flag = raw_input()
if encode(flag) == correct:
print 'correct'
else:
print 'wrong'
据此写出解密脚本
import base64
def decode(message):
s=''
message = (base64.b64decode(message))
print(message)
for i in message:
s += chr((ord(chr(i))-16) ^ 32)
#需要注意的是,base64解码出来的是二进制流,所以先要用chr(i)转化为字符,再操作
return s
correct = 'XlNkVmtUI1MgXWBZXCFeKY+AaXNt'
flag = decode(correct)
print(flag)
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
题目描述:菜鸡想要走出菜狗设计的迷宫
这是没见过的船新版本,拿到题后一脸懵逼,毫无思路。看了题解后才慢慢理解“迷宫”的意思。
主函数如下:(变量名、函数名被我根据实际作用更改过了,更容易理解)
__int64 __fastcall MEMORY[0x4006B0](__int64 a1, char **a2, char **a3)
{
signed __int64 i; // rbx
signed int char_1; // eax
bool not_out_of_range_2; // bp
bool not_out_of_range; // al
const char *print_str; // rdi
__int64 location; // [rsp+0h] [rbp-28h]
location = 0LL;
puts("Input flag:");
scanf("%s", &input_str, 0LL);
if ( strlen(&input_str) != 24 || strncmp(&input_str, "nctf{", 5uLL) || *(&byte_6010BF + 24) != '}' )
{
LABEL_22:
puts("Wrong flag!");
exit(-1);
}
i = 5LL;
if ( strlen(&input_str) - 1 > 5 )
{
while ( 1 )
{
char_1 = *(&input_str + i);
not_out_of_range_2 = 0;
if ( char_1 > 78 )
{
char_1 = (unsigned __int8)char_1;
if ( (unsigned __int8)char_1 == 'O' )
{
not_out_of_range = sub_400650((_DWORD *)&location + 1);// 向左
goto LABEL_14;
}
if ( char_1 == 'o' )
{
not_out_of_range = sub_400660((int *)&location + 1);// 向右
goto LABEL_14;
}
}
else
{
char_1 = (unsigned __int8)char_1;
if ( (unsigned __int8)char_1 == '.' )
{
not_out_of_range = sub_400670(&location);// 向左8字符(相当于8*8的向上)
goto LABEL_14;
}
if ( char_1 == '0' )
{
not_out_of_range = sub_400680((int *)&location);// 向右8字符(相当于8*8的向下)
LABEL_14:
not_out_of_range_2 = not_out_of_range;
goto LABEL_15;
}
}
LABEL_15:
if ( !(unsigned __int8)can_go((__int64)asc_601060, SHIDWORD(location), location) )// 前面还有一个取非符号
goto LABEL_22;
if ( ++i >= strlen(&input_str) - 1 )
{
if ( not_out_of_range_2 )
break;
LABEL_20:
print_str = "Wrong flag!";
goto LABEL_21;
}
}
}
if ( asc_601060[8 * (signed int)location + SHIDWORD(location)] != '#' )// 终点是#
goto LABEL_20;
print_str = "Congratulations!";
LABEL_21:
puts(print_str);
return 0LL;
}
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得学习学习了。
至此,攻防世界得新手题全部做完。但是我感觉还有很多可以挖掘学习得点,比如通过动调解答最前面得几道题等等。
一直在路上呢~~