AnalyserCM1 WriteUp
这题本来是投给青少年CTF平台题目审核未通过所以发出来给大家玩一玩文章末尾附带源码附件
拿到题之后直接上调试器
在调试器里运行会提示异常然后崩溃
设置调试器异常的时候暂停:
我们再运行一次
在这里断下来了,是这个call出了问题,我们在内存窗口Ctrl+G跳转到rsp+40的位置
注意观察那条call的指令是qword ptr ds:[rsp+40h]
所以是8字节,我们看看能否跳到这个地址里去
当然,这么高的地址肯定是不可能的。所以我们需要追溯rsp+40的来源
我们按下快捷键H进入高亮模式,点击那个40,因为rsp的出现频率是一定要比40这个常数要高,所以高亮那个40会更容易观察
可以看到是rsp+40这个地址里的值是rax的值写入的
我们继续追rax, 同样的手法,高亮模式
可以看到,是通过左移运算得到的值,现在cl的值为1(现在的CL值可信是因为目前为止cl在进行左移运算后没有被写入过)
Rax <<= 1 就相当于 rax * 2 或 rax + rax
拿出计算器,将它右移,我们看看结果是有没有用
直接到调试器反汇编窗口Ctrl+G跳转到这个地址(因为call指令是要执行指令的,所以推测这个地址如果有效,则必须是代码)
转过来一看,果然,但为什么呢?那个cl的值又是哪里来的
我们继续追
注意看这条指令,从一个地址读取了一个字节的数据扩展写到eax寄存器,高亮模式追是谁给这个地址写入的值, 按照这样的思路一直追
找到了比较关键的位置,一个判断,如果这个判断条件成立(jne ZF=0)则赋值为0 反之为1,如果为0, 则call的地址将不会被乘2
而判断的关键在于test eax, eax 将两个操作数进行与运算并根据结果给ZF位赋值, 而eax值的来源又是上面那个地址里读取出来的。继续跟
发现到这里戛然而止了,其实不然,我们可以观察到还有一个比较
而这个比较是直接越过了是给cl 赋值1还是0, 而我们又看到 cmp qword ptr ss:[rsp+48], 0x十六个F也就是-1
而上面的lea r8, qword ptr ds:[rsp + 48h]则是将rsp+48h的出来的地址给了r8寄存器,这里就相当于C语言中的取地址&var1
由此可以推断,call将改变rsp+48h里的数据,我们在这个call下断点,然后重新调试,断下后F7跟进去看
我们可以看到,把参数在栈中备份了一份,紧接着是一个call
这个call我们无需担心,因为如果有参数就会给rcx rdx r8 r9d等寄存器赋值,我们进入这个call,看它的ret
发现就是简简单单的0xC3 函数内部没有平衡栈,外面也没有平衡栈,说明根本就没参数, 而第二个call就不一样了
跟进去看看
等下,突然想到个什么东西
....
极有可能是反调试 syscall系统调用,这条指令会根据eax的值(ssdt编号)来执行不同的函数
反调试函数就那几个 NtSetInformationThread NtQueryInformationProcess当然还有检查PEB结构的某些成员等手段。
这里我们先不考虑那么多因为能用来反调试的系统调用基本就这两个(当然NtClose也能玩骚操作)
这里我们根本不需要对照ssdt来判断是哪个函数,因为最外面
这里我们可以看到 rcx为16个F,也就是-1, 这代表的是当前进程的伪句柄 所以直接就可以锁定是NtQueryInformationProcess了
函数原型:
NTSTATUS NtQueryInformationProcess(HANDLE ProcessHandle,//进程句柄
PROCESSINFOCLASS ProcessInformationClass,//进程信息类型,传不同的值表示查进程不同的信息
PVOIDProcessInformation,//输出参数 存储进行信息的缓冲区 ULONG ProcessInformationLength,//缓冲区大小
PULONG ReturnLength//实际大小)
而edx为7,这个7也就是PROCESSINFOCLASS::ProcessDebugPort
过这道题的反调试需要对Windows系统有深入了解
如果有调试器,则第三个参数的值会被写入为无符号0xFFFFFFFFFFFFFFFF也就是有符号的-1
所以,我们先在内存窗口转到r8寄存器里的地址,跑过syscall后可以看到变成8个FF了
全部改为00,然后运行
Ok,终于把反调试过掉了
这时,我们也可以选择直接在外面的call动手脚
因为我们很确定这个call是反调试,程序主要逻辑并不在这里,所以我们可以直接改
因为R8里的地址里的数据本身就是0,所以我们只需要rax返回0,因为通过函数原型我们看到返回值是NTSTATUS
而0的值正好对应着STATUS_SUCCESS, 也就是调用成功(在Windows内核编程经常用到这个东西)
至此,反调试结束,我们搜索字符串定位一下关键点
对了,改完汇编指令记得保存
保存之后我们分析保存好的exe
搜索字符串定(Shift+D)位关键点
双击进去
断到这里,往下走
到这个call F8单步步过的时候,阻塞住了,也就是让我们输入flag
随便编造一个flag, qsnctf{aaaaabbbbb}
然后一路F8走
这里,计算字符串”qsnctf{”的长度 结果保存在rsp+30
然后下面这里
循环比较我们的字符串,qsnctf{跟我们的flag比,疑似是判断是否有qsnctf{这个前缀
同样是格式校验,判断输入的字符串最后一位是不是}
跑过这个call发现这个call的返回值是个地址,而这个地址里存储的正是我们输入的内容去掉flag格式。
注意看,去掉flag格式后的长度,在跟0x19(25)比较,而rax正好是我们输入的aaaaabbbbb长度为0xA(10)
所以可以推断这是一个长度校验,我们把flag重新编排一下
qsnctf{0123456787654321ABCDEFGHI}
先在当前位置下断点再重新调试
Ok这个时候条件就满足了,继续往下走
到这个call的时候,rcx也就是第一个参数为我们输入进来的数据(不带flag格式) rdx也就是长度,r8也用到了,目前不知道干啥的,大概率是算法,先进call 走走看
注意此时rax为我们输入的字符串(不带格式)的指针,当前EIP指向的这条指令是rax读取一个字节,然后放入到一个两个字节(short)的内存空间里, 放在了rsp+4,而且
看到这个jae 基本可以确定是循环没错了,往下走
注意看这两条指令,把保存的字符拿出来,进行左移2位然后再写回去
再看,eax被写入一个值,这个值是从一个内存地址里读取到的,
然后ecx是我们保存进去的值,两个进行异或,也就是说那个地址里存储的值是关键,我们拿出来
0x66 记录一下
异或完的值在ecx里, 又给了eax,然后再存回原处
然后又拿出来,进行右移1再给回去
整个流程就是input[i] <<= 2; input[i] ^= 0x66; input[i] >>= 1;
我们再往下看,看看还进行了什么操作
没了,算法就这么简单
跑出这个call
我们发现下面一个call紧接着就是判断成功与否,我们看看它会拿着我们输入内容加密后的数据跟什么做比较
跑到这里,我们看参数窗口
我们发现,rcx是我们输入的数据
rdx则是另一组数据
我们发现,格式极为相似, 都是short类型
R8则可能是长度,因为我们输入进来的数据占一个字节,然后扩充成两个字节存进去的 0x32(50) / 2 = 0x19(25),就是我们的长度,没得洗了
我们把算法倒推一下,我们把第二个参数的数据拿50个字节
VS新建项目
出问题了,此处问题可想而知,因为左移1位就等于乘以2
那么右移1位就等于除以2,如果除不尽,则会丢掉余数
所以,我们需要判断如果当前数据除以2除不尽,则+1
因为取除以2余数,不是0就是1所以更好办了
我们得反着来,因为作者出题的时候也考虑到了这个问题,他们把他们是先让数据取余数,直接加上余数
所以我们得反过来,有余数要为0, 反之为1
下载链接:链接:https://pan.baidu.com/s/12fkhUFChMfqzM19pJrQR7w?pwd=8s1t提取码:8s1t