2019 CISCN 线上赛
国赛初赛总算打完了。刚好是西湖论剑后一天,跟队友在杭州小宾馆里肝题。成绩还算可以吧,就是时间有点不够,做题慢了,不然还能出一道的。第一天1道easyGo,第二天四道题,这谁顶得住啊。安卓题还是不会做,要赶紧学安卓咯!
easyGo
这题是由Go语言编译成的二进制文件。之前没做过go的题目,有点无从下手。参考了一下夜影师傅的一篇文章,用文章中类似的方法能在00000000004CEFB8
找到提示语字符串”Please
input you flag like flag{123} to
judge:“,通过查找引用能在00000000004E1130
附近找到其他字符串的引用。再次查找引用定位到sub_495150
,这里是主要加密逻辑。不过单纯静态做也有点乱,动调之后看到在0000000000495318
对比长度0x2A,在
.text:00000000004953A1 call sub_4023F0
下断点,RDX直接指向了flag
1 | 000000C0000181B0 66 6C 61 67 7B 39 32 30 39 34 64 61 66 2D 33 33 flag{92094daf-33 |
之后仔细看看也能看到base64密文在00000000004CF8B6
,加密表在000000C00006C580
1 | import base64 |
bbvvmm
拿flag需要解出username和password,nc上去拿flag。
username
username用国密SM4加密,其中
key = 'DA98F1DA312AB753A5703A0BFD290DD6'.decode('hex')
cipher = 'RVYtG85NQ9OPHU4uQ8AuFM+MHVVrFMJMR8FuF8WJQ8Y='
很明显是base64,但其实加密表也被替换了,在0000000000406C20
username的加密就是标准的国密加密。踩坑警告:不要用gmssl,用pysm4!
1 | import base64 |
password
虚拟机题
vm结构体:
1 | 00000000 vm struc ; (sizeof=0x4D0, mappedto_10) |
opcode反编译:
1 | B0 19 00 00 00 push 0x19 |
这部分算法很简单,不过直接看opcode会有点晕,建议直接调试,对着主要结构体看不难发现直接异或了0x78开始的6个字节,最后相加要为0。
远程打不通,可能是read读了回车。
1 | from pwn import * |
strang_int
一开始不知道时啥架构的,后面放到linux里面file一下可以看到是DOS/MBR程序,直接用默认x86打开就行了。这题如果对操作系统比较熟悉的话做起来会上手一点。
刚开始的一段代码是十六进制的,第一行的jmp其实跳的就是下一行,因为MBR加载的位置在0x7C00,之后初始化了一些东西,不用太关心。
接下来0x01FE的代码是32进制的。注意到刚开始将ds赋值成了10h,所以接下来的寻址都要加0x100。
000001FE开始的一部分初始化了一部分寄存器,也不用太关心。接下来的逻辑是关键:
1 | seg000:0000022C xor ebx, ebx |
在第一个0x10的循环内,修改了ds:128h开始的一些数据,把ds:0D08h和8E00复制过去。去道0xE08并不能看出什么,但是如果再往下看0x100个字节,到了0xF08能看到刚好连续16个word指向了16个地址。顺着这些地址也发现不了什么,但是把这些地址都加0x100后,刚刚好能对应16个类似函数的代码。这时大概能猜到一点东西:程序里的偏移不太对。
最终0x228+0x21*8这里存放了连续16个地址,映射到0xF08也就是`00000D7C
开始的16个函数。
再来看这一段代码:
1 | seg000:0000025F ; -------------------------------------------------------------------- |
同样的,这里的0B78h和0D48h和65h应当对应地址0x0D78和0xF48和265h。在F48看到一连串相似的结构体,每个的第一个字节都是0x21到0x30中的某一个。
继续看代码,从0xF48+[0xD78]*4读取一个字节,覆盖到0x265上,刚好覆盖了int
指令的操作数那一字节,联系之前的修改函数地址,不难得出,这道题通过修改中断向量表,用中断服务程序实现了一个虚拟机功能,用ecx和eax实现操作数的传递。其中00000D64
开始的5个dword是寄存器,00000D78
是ip。接下来就分析00000D7C
开始的每个handler了。
用下面的代码可以得到opcode对应的汇编代码:
1 | opcode = [[0x21, 0x0, 0x81], [0x27, 0x1, 0x1], [0x24, 0x1, 0x1], [0x23, 0x2, 0x0], [0x22, 0x3, 0x2], [0x21, 0x4, 0x8], [0x28, 0x3, 0x4], [0x27, 0x2, 0x3], [0x28, 0x3, 0x4], [0x27, 0x2, 0x3], [0x28, 0x3, 0x4], [0x27, 0x2, 0x3], [0x27, 0x3, 0x3], [0x23, 0x4, 0x3], [0x24, 0x3, 0x2], [0x27, 0x2, 0x4], [0x24, 0x0, 0x2], [0x21, 0x1, 0x1], [0x25, 0x0, 0x1], [0x22, 0x1, 0x0], [0x21, 0x2, 0x81], [0x26, 0x1, 0x2], [0x21, 0x2, 0x9], [0x26, 0x1, 0x2], [0x21, 0x2, 0x9], [0x2D, 0x2, 0x1], [0x21, 0x0, 0x81], [0x22, 0x1, 0x0], [0x21, 0x2, 0x9], [0x25, 0x1, 0x2], [0x23, 0x3, 0x0], [0x23, 0x4, 0x1], [0x26, 0x3, 0x4], [0x21, 0x4, 0x7E], [0x2D, 0x4, 0x3], [0x21, 0x3, 0x1], [0x25, 0x0, 0x3], [0x25, 0x1, 0x3], [0x26, 0x2, 0x3], [0x21, 0x4, 0x5A], [0x2D, 0x4, 0x2], [0x2F, 0x0, 0x0], [0x30, 0x0, 0x0]] |
1 | 00 mov r0, 0x81 |
加密算法并不难,注意一开头将opcode的前4个字节清零,然后这里放每次循环得到的结果,下一个循环继续异或。
写出解密算法:
1 | cipher = [0x65, 0x55, 0x63, 0x57, 0x01, 0x04, 0x53, 0x06, 0x49, 0x49, 0x49, 0x1F, 0x1F, 0x07, 0x57, 0x51, 0x57, 0x43, 0x5F, 0x57, 0x57, 0x5E, 0x43, 0x57, 0x0A, 0x02, 0x57, 0x43, 0x5E, 0x03, 0x5E, 0x57, 0x00, 0x00, 0x59, 0x0F] |
where_u_are
环境配置
一道wasm逆向题。wasm的汇编代码比x86的恶心多了。。。
用python本地可以开个web服务:
1 | python -m SimpleHTTPServer 8080 |
chrome中输入localhost:8080,点击html进入web服务。按f12,在sources界面可以调试。第一次打开并没有加载好wasm,刷新一次可以看到wasm,可以看到左侧各种函数,中间是主要的汇编代码,右侧有调用栈,断点,变量监视等。断点设好后可以在右侧Scope里看到各种全局变量和局部变量。之后静态分析不动了可以来这里动调确认猜想。
另外跑web的服务的时候不清楚为啥点了确认之后会反复弹窗,点了取消猜出结果,而且只会输出两个浮点数,结果的提示语并不会输出。
刚刚说了,wasm的汇编及其恶心。先用wabt的wasm2c将wasm转成C代码。这里的C代码仍然究极恶心。我们可以用gcc+IDA帮我们优化,然后阅读IDA反编译出来的代码。
首先是wasm2c。编译好wabt后,在bin文件夹内能找到wasm2c,使用指令
1 | wasm2c where_u_are.wasm -o where_u_are.c |
得到wabt反编译的C代码,然后把wasm2c文件夹内的wasm-rt.h
复制到同目录下,使用指令
1 | gcc -c where_u_are.c |
得到只编译不连接的.o文件,这时就可以拖进ida分析了。
算法分析
经过gcc+IDA的优化,代码总算多少能看了。main函数对应wasm的第25个函数即f25。
分析算法之前,先明白wasm的字符串常量全都保存在文件末尾,我们不用太关心它是怎样使用的,只要知道哪里用到了这串字符串即可。用010editor在文件末尾我们看到了几个字符串:"%s" "%lf %lf" "Correct!!" "Wrong!"
回到main函数,可以看到几个类似偏移的数据:0x1170u 0x1173u 0x1186u 0x117Cu。如果我们稍作计算,可以发现这些偏移和这些字符串是一一对应的。可以断定这里就是对应这些字符串,也就不难猜出f98和f97是类似printf和scanf的函数。那么主要逻辑应该在f23和f24里面。分析f23中的循环,先猜测f43是strlen,在循环内的f22中,看到另一个可疑的偏移0x400,在010editor中看到是一个长度32位的字符串,对应0-9a-z,字母部分空缺了几个。回到f23的循环,每个循环内部还有大小为5的循环,结合最后一行(>>j)&1,以及刚刚的字符串,应该能想到,这里将输入的字符按照字符串映射到0到31,然后把每一个bit取出来,这样的到了n*5个bit
来到f24,稍作分析这里的代码。
1 | v9 = 0.0; |
大概就是将刚刚的n*5个bit按奇偶分成两组,然后进行这里的“算法”。最终校验的代码IDA分析的不是很好,不过大概也能看出结果要为25和175。
比赛时没看出是啥算法,就爆破了:
1 | for i in range(1048576): |
赛后发现这其实是二分查找,奇偶两组是两个区间[-180, 180]和[-90, 90],每一个bit的0或1决定二分的方向,那只要求出查找175和25过程中的方向即可。
1 | def getbit(start, result, min_, max_): |