首先百度看看什么是驱动程序,看了几篇有关windows驱动开发的文章,总结一下,就是个sys文件,涉及到服务和设备,需要权限才能运行。
打开题目压缩包,一个带管理员图标的exe,一个sys文件,readme里说要开测试模式,先分别ida分析看看。
exe文件里函数不多,基本上每个我都点了一遍,有创建服务的,启动服务的,获取文件完整路径和单独的文件名的,以及调用printf和scanf的。。。最后来到主程序。
call_printf(aEnterPassWord); call_scanf(aD, v5); if ( (signed __int64)v5[0] <= 96000 && (signed __int64)v5[0] >= 90000 )
主程序里调用
了启动服务的函数,输出了密码提示,并且用scanf接收输入。scanf用的格式字符串很奇怪,在这里:
就是一个%d,也就是一个数字。但是显然下面这个更像是一个序列号字符串才对。
我这里甚至动态调试了几次,才确定scanf确实用的%d,接收一个90000~96000之间的数字。
作为测试,直接运行程序,输入数字,发现不在范围内的就错误弹框,范围内的程序就直接结束,因为我还没开测试模式。
我坚信密码不可能是范围这么小的五位数,并且认定后面还会调用scanf,并且使用下面这个格式化字符串,但是这样不就有两次输入了么?(后来发现真的就是五位数)
继续往下
最终调用的是DeviceIoControl,从栈上看一下传进去的参数
这里badcallbak是一个错误弹框的函数指针,codestr是从内存中复制过来的数组,在调用前,writeptr这个函数向其中+2和+18的地方分别写了两个8字节的指针,value是输入的数字。而inbuffer中第一个qword中,也就是前8字节是输出缓冲区的地址。(ida的栈方向好像是反的,直接看变量的地址发现后面几个才在高地址)
其中 sub_14000256E很奇怪,貌似是取栈顶的值赋值给参数,这里v4对结果根本没影响,所以跳过不管
.text:000000014000256E sub_14000256E proc near ; CODE XREF: MAin+10E↑p
.text:000000014000256E mov rax, [rsp+0]
.text:0000000140002572 mov [rcx], rax
.text:0000000140002575 retn
.text:0000000140002575 sub_14000256E endp
接着分析sys文件,从driverEntry跟进去,很快找到
于是到MAjorFunc函数去看,这个函数要处理的就是DeviceIoControl
这里ida自动分析的结构体出了点问题,删掉一个值后居然直接直接给换成了另一个。不过因为知道输入缓冲区的内容,根据后面的使用情况还是可以猜出这些变量的内容。
这里被我标记为maycall的函数里面调用了KeInitializeApc,KeInsertQueueApc,虽然不知道APC是什么,但显然不能让输入值是之前参数里错误弹窗的函数,于是可以确认要满足
if ( *(_DWORD *)((char *)inbuffer + v11 + 36) == v12 )
sub_14000110D这个函数也很奇怪,按照ida的伪代码,里面直接用了未初始化的指针,本来想具体看看汇编的,但是决定先看看返回的这个数字怎么用,说不定能猜出来。
inbuffer中前32字节是四个数字,再后面就是从内存中复制出来的数组,按照后面的调用看,这里在经过decodeit之后直接把整个数组用qmemcpy复制然后作为maycall的参数,也就是说后面这个数组里存的是验证成功的回调代码,只是需要经过正确解密,而解密是否成功就通过if里取第一个dword来判断。 (may_outbuf也是猜出来的,因为v13是函数地址,被赋值为从inbuffer中传进去的outbuffer起始地址,因此memcpy这里肯定是要改变outbuffer的内容,才能作为解密后的代码。)这里var决定的传入decodeit的参数,第一个参数应该是要解密的代码的开头地址,第二个参数是要解密的数组长度,最后一个参数是输入数字。这里如果var是-4的话刚好就是整段代码全部解密,实际上我开始就假设var是-4(猜错了)。
判断条件里v12也是需要解密的长度,和decodeit第二个参数一样。
接下来进入decodeit解密函数。这个函数比较好懂,把输入首尾相连当成一个圆这样,而指针就在这个圆上转,每次取指针前一个byte和后一个word相加。输入的value决定了指针起点和迭代次数,这里v3也就是返回值没有用,不用管:
__int16 __fastcall decodeit(_BYTE *str, int lenstr, int value) { void **v3; // rax int count; // er10 __int64 lenstr_1; // rbx int offset_end; // esi __int64 offset; // r11 unsigned __int8 lastbyte_; // al __int64 offset_1; // r8 __int16 thisword; // di unsigned __int16 v11; // dx void *retaddr; // [rsp+0h] [rbp+0h] v3 = &retaddr; count = value - 1; lenstr_1 = lenstr; if ( value - 1 >= 0 ) { offset_end = lenstr - 1; do { offset = count % (signed int)lenstr_1; if ( count % (signed int)lenstr_1 ) { lastbyte_ = str[offset - 1]; offset_1 = count % (signed int)lenstr_1; } else { lastbyte_ = str[lenstr_1 - 1]; offset_1 = 0i64; } if ( (_DWORD)offset == offset_end ) thisword = (unsigned __int8)*str + ((unsigned __int8)str[offset_1] << 8);//这里int8应该是错的,要是8为左移8位不就全为0了吗。从汇编里也没有找到相应的左移,应该是直接操作AX,AL这种,所以后来用的时候我改成了int16 //另外这里结尾处特殊处理和直接读word不一样,如果把[0]拼到后面来看感觉这里读法就不是小端序了 else thisword = *(_WORD *)&str[offset_1]; v11 = thisword + lastbyte_; LOWORD(v3) = v11 >> 8; str[offset_1] = HIBYTE(v11); if ( (_DWORD)offset == offset_end ) *str = v11; else str[offset_1 + 1] = v11; --count; } while ( count >= 0 ); } return (signed __int16)v3; }
因为不知道正确解密后的结果和最后指针指向偏移地址,倒过来写一个相对应的加密没有意义(我也不一定能写出来)。只要直接用这个解密算法假设解密成功的判断条件,value的值显然可以爆破,接下来的问题就是var的值。
其实var的值也有限制,显然不能超过inbuffer的范围。我想出了几种方法,直接看汇编太累;动态调试驱动程序好像有点麻烦,我只有ida,甚至没装虚拟机,所以不合适;把这段代码复制下来跑一遍看结果,也不能保证是对的;直接连var的值一起爆破,貌似可行。
但是爆破之前,我决定先看看揭密前的代码,缩小范围。
回到exe文件,再这段代码所在的地址按下C
这里MAin函数中调用writeptr写进去的地址就是两个dq,虽然中间还空了一个,但我觉得这地址写进去肯定有用,应该不在需要解密的范围内。
后面这个sub_1400060D4是ida自动分析出来的,里面有jnz,jz,call rax,看起来就像是个正常的函数,不像是需要解密的乱码。
按下F5,虽然后面有栈分析失败的红字,但还是成功了。
于是神奇的事情出现了:
两边函数长得差不多。再来看之前返回var的那个看不懂的函数:
刚好是右边这个函数的返回值。根据左右两个函数的不同之处,猜测右边函数应该返回161,而左边的函数会进入call rax的分支。
跳到这段代码起始地址+161+36-32的位置来看:
刚好是分析出来的函数截止的地方,再加上4字节的0
现在可以断定需要解密的就是后面这一串42h长度的数据。
int main(){ for(__int64 i=90000;i<=96000;i++){ _BYTE code[]={0xE8, 0x59, 0x0D, 0x6D, 0x80, 0x3C, 0xA2, 0x78, 0x15, 0x87, 0x16, 0x16, 0x07, 0x26, 0x68, 0x55, 0x7F, 0x12, 0xF1, 0xEF, 0xF9, 0xA1, 0x9C, 0xE8, 0xEA, 0x9C, 0x90, 0xF4, 0x9F, 0x3A, 0xA8, 0x8C, 0x27, 0x47, 0x79, 0xF6, 0xDC, 0x20, 0x7F, 0x86, 0xED, 0x34, 0x7E, 0xF7, 0x1C, 0x55, 0x6B, 0xF6, 0xEF, 0xF2, 0x2A, 0x7A, 0xF0, 0x44, 0x50, 0x8A, 0x9B, 0xE1, 0xC4, 0xE1, 0x45, 0x90, 0x2B, 0x0E, 0xCF, 0xAF}; int len=0x42; cout<<"i="<<(long)i<<endl; decodeit(code,len,i); if(*(_DWORD *)((char *)code) ==len){ for(int j=0;j<len;j++) printf("%02X ",code[j]); cout<<endl; break; } } return 0; }
跑出结果,试了一下,居然竟然真的就ok了。我以为应该要再来个scanf用那个字符串的。。。。。。。
做完刚好八点半,发现readyu大佬已经提交过。不过这个结果来得太突然,我仍然有种不太真实的感觉。
现在有点时间,想起来这个有趣的过程,觉得还是要记录一下。至于程序的实现,就是这段代码解密后的执行,以及一些还没弄明白的地方,我也没什么动力再去探究,就等出题思路和大佬们的wp出来直接看吧。
[进行中]2019 KCTF总决赛 | 巅峰对决,谁与争锋!(感谢第五空间和安恒信息对活动的支持!)
最后于 1天前 被mb_ibocelll编辑 ,原因: 纠错