-
-
[原创]KCTF2022 签到题 险象环生
-
发表于: 2022-5-11 03:01 2584
-
题目比较简单但考验基本功
分析目的
将题目下载下来,是一个压缩包,包含一个exe文件和一个包含公开的用户名(461D7AD538F179EE)以及其相对应的序列号(40403286341212)的文本文档,文档中阐明了要求找出以KCTF为用户名对应的序列号。
前期准备
对exe文件使用exeinfo进行初步分析,得到如下结果:
显然这是一个32位的windows可执行程序,没有发现加壳。同时其提醒了程序中存在crc的算法。
开始分析
使用IDA载入程序进行分析,使用shift+f12的IDA快捷键寻找程序中的字符串,发现了Wrong Serial!以及Success!等字符串,对这两个字符串进行交叉引用分析从而快速定位程序中算法关键部分(按x查看字符串交叉引用)。
发现Success字符串并无引用,而Wrong Serial字符串有两处引用
这里选择第一个引用,跳转到0x4014a6这个地址。
可以看到IDA中代码标红了,这使得IDA无法使用F5快捷键生成程序的伪代码。对于这种情况,可以在标红的代码处按p键让IDA尝试重新构建函数,但操作后IDA给出了一个信息
这里的意思是在0x4015ec处有IDA无法识别的数据,这导致了函数构建失败。
对于这种情况,可以按c键让IDA尝试将这些数据转换为代码,因为IDA可能会因为一些花指令一类的混淆导致其没有将其识别为代码。
但操作后发现指令无法转换成代码,此时应该考虑以下情况:
1.是否在前面的代码中对这段数据进行了处理,代码实际上被加密,运行到这段代码之前代码会被还原
2.运行到这段代码前发生了栈溢出,通过栈溢出劫持了eip导致这段代码不是需要执行的
此时可以对前面执行的函数进行分析,查看是否有对这段数据地址进行修改的地方,也可以寻找一下是不是发生了溢出。根据经验判断的话,一般前者的情况比较多。
那么如何缩小寻找解密函数的范围呢?显然,在我们输入用户名和序列号提交之后是最有可能对代码进行解密的。我们可以定位程序从控件中获取用户输入的api进行定位。
那么可以确定解密函数应该就在附近,从上往下寻找函数调用,依次发现0x4034AD和0x4017E0两个函数调用,其中0x4034AD这个函数可以F5转换为伪代码
那么初步看来此函数不像是解密的函数,因为其只有一个参数,且参数类型为int类型,如果是解密函数的话,应该会有一个指向需要解密数据块的指针参数。而0x4017E0这个函数则符合这个要求
函数的第一个参数是指针,分析一下这个指针从何而来
可以看到指针作为第一个参数,其是由edx寄存器保存的,且数据来源于栈中。因为静态分析是看不到栈里面的内容的,所以需要进行动态调试。
使用IDA自带的调试器是可以直接进行调试的,注意调试程序的存放路径中不要有中文。在调试之前我们就可以在edx寄存器被赋值的指令处按F2打上断点,这样程序运行到这里就会停下来,便于我们分析数据
那么程序跑起来之后可以输入出题人提供的正确的用户名以及序列号,这样可以保证其运行的代码路径是正确的。之后提交后便会断在断点处
此时可以发现栈里面的数据是几个16进制数,但是我们知道它其实是一个指针,所以可以理解这个参数实际上是一个指向0x4015d2的指针。
这个指针的位置就在下方而且离无法被解析的数据块很近,此时可以发现0x4015d2处的指令似乎很不对劲,其指令是aaa,结合0x4017E0函数里面的内容是对此处的数据进行异或运算,我们可以断定0x4017E0函数就是我们需要的解密函数。当此函数运行过去之后可以发现原来的无法被识别的数据块发生了改变,我们可以使用F8快捷键一路运行下去,直到运行到Success处为止,此时再对代码按p重新识别函数,再使用F5快捷键便可以生成伪代码,(如果仍然无法识别函数,可以在odbg或者x32dbg中运行并dump,或者在IDA提示无法识别的代码位置将其代码给替换成nop代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | int __cdecl sub_401340(HWND hDlg) { int result; / / eax unsigned int Value; / / [esp + 20h ] [ebp - 3B8h ] int v3; / / [esp + 2Ch ] [ebp - 3ACh ] int v4; / / [esp + 34h ] [ebp - 3A4h ] signed int v5; / / [esp + 38h ] [ebp - 3A0h ] UINT v6; / / [esp + 3Ch ] [ebp - 39Ch ] HWND hWnd; / / [esp + 4Ch ] [ebp - 38Ch ] HWND hWnda; / / [esp + 4Ch ] [ebp - 38Ch ] char Buffer ; / / [esp + 54h ] [ebp - 384h ] BYREF char v10[ 199 ]; / / [esp + 55h ] [ebp - 383h ] BYREF CHAR v11; / / [esp + 11Ch ] [ebp - 2BCh ] BYREF char v12[ 199 ]; / / [esp + 11Dh ] [ebp - 2BBh ] BYREF char v13[ 36 ]; / / [esp + 1E4h ] [ebp - 1F4h ] BYREF int v14[ 50 ]; / / [esp + 208h ] [ebp - 1D0h ] BYREF CHAR String; / / [esp + 2D0h ] [ebp - 108h ] BYREF char v16[ 199 ]; / / [esp + 2D1h ] [ebp - 107h ] BYREF char v17[ 16 ]; / / [esp + 398h ] [ebp - 40h ] BYREF char Destination; / / [esp + 3A8h ] [ebp - 30h ] BYREF int v19; / / [esp + 3A9h ] [ebp - 2Fh ] int v20; / / [esp + 3ADh ] [ebp - 2Bh ] int v21; / / [esp + 3B1h ] [ebp - 27h ] int v22; / / [esp + 3B5h ] [ebp - 23h ] __int16 v23; / / [esp + 3B9h ] [ebp - 1Fh ] char v24; / / [esp + 3BBh ] [ebp - 1Dh ] CPPEH_RECORD ms_exc; / / [esp + 3C0h ] [ebp - 18h ] v11 = 0 ; memset(v12, 0 , sizeof(v12)); LOBYTE(v14[ 0 ]) = 0 ; memset(v14 + 1 , 0 , 0xC7u ); Buffer = 0 ; memset(v10, 0 , sizeof(v10)); String = 0 ; memset(v16, 0 , sizeof(v16)); strcpy(v17, "www.pediy.com" ); Destination = 0 ; v19 = 0 ; v20 = 0 ; v21 = 0 ; v22 = 0 ; v23 = 0 ; v24 = 0 ; strcpy(v13, "23456781ABCDEFGHJKLMNPQRSTUVWXYZ" ); v5 = GetDlgItemTextA(hDlg, 1001 , &String, 201 ); v6 = GetDlgItemTextA(hDlg, 1000 , &v11, 201 ); if ( v5 > 14 || v6 = = 0 ) { SetDlgItemTextA(hDlg, 1001 , "Wrong Serial!" ); hWnd = GetDlgItem(hDlg, 1014 ); EnableWindow(hWnd, 0 ); result = 0 ; } else { v14[ 0 ] = * &v16[ 9 ]; v3 = sub_4034AD(v14); if ( dword_413000 ) sub_4017E0(&loc_4015D2, &loc_401767 - &loc_4015D2, v3); ms_exc.registration.TryLevel = 0 ; Value = sub_401260(&v11, v6); strncpy(&Destination, &String, v5 - 4 ); _ultoa(Value, & Buffer , 10 ); dword_413000 = 0 ; v4 = strcmp(& Buffer , &Destination); if ( v4 ) v4 = v4 < 0 ? - 1 : 1 ; if ( v4 ) SetDlgItemTextA(hDlg, 1001 , "Wrong Serial!" ); else SetDlgItemTextA(hDlg, 1001 , "Success!" ); hWnda = GetDlgItem(hDlg, 1014 ); EnableWindow(hWnda, 0 ); ms_exc.registration.TryLevel = - 2 ; result = 0 ; } return result; } |
我们可以将现在的程序dump下来,以便对比汇编代码进行分析。
那么此时程序结构已经相当明了了:
程序在45-46行处获取了用户输入的用户名和序列号,随后将其存入了String和v11两个变量中去,而v5和v6则是用户名和序列号的长度(参见GetDlgItemTextA的函数原型)。
随后对长度进行判断,若是用户名为控或者序列号长度大于14,那么直接显示错误。通过了第一轮校验之后,便是一个名称为v14的数组引用了一个名称为v16的数组的第10位。
但是v16在伪代码中只在36行处被分配堆时设置为了0,所以想要弄清楚v16的内容是什么,就需要动态调试至此处查看其值
查看其值之后发现原来是我们之前输入的序列号,那么v14的值便被赋值为了序列号的后四位。接着就是0x4034AD函数对v13进行了处理,其函数内容如下
看起来有些摸不着头脑,但return crt_strtox::parse_integer 暴露了这个函数的功能,parse_integer的意思是将参数传入的字符串转变为数字。这里这个函数的作用主要是为下面解密函数0x4017E0做准备
解密函数0x4017E0的前两个参数看起来难以理解,但我们知道它的作用是解密后面的数据块,且它的第三个参数是被转换为数字的序列号的后4位。由于题目提供了一个正确的序列号,此序列号可以正确的解密数据块,那么后四位的值应该是确定的,即为1212.
接下来便是0x401260函数,其接收用户名和用户名的长度作为参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | int __cdecl sub_401260(char * a1, int a2) { int v2; / / ecx unsigned int v3; / / eax unsigned int v4; / / eax unsigned int v5; / / eax unsigned int v6; / / eax unsigned int v7; / / eax unsigned int v8; / / eax unsigned int v9; / / eax unsigned int v10; / / eax int v11; / / edx unsigned int i; / / ecx char v14; / / al int v16[ 256 ]; / / [esp + 0h ] [ebp - 404h ] v2 = 0 ; do { v3 = v2 >> 1 ; if ( (v2 & 1 ) ! = 0 ) v3 ^ = 0xEDB88320 ; if ( (v3 & 1 ) ! = 0 ) v4 = (v3 >> 1 ) ^ 0xEDB88320 ; else v4 = v3 >> 1 ; if ( (v4 & 1 ) ! = 0 ) v5 = (v4 >> 1 ) ^ 0xEDB88320 ; else v5 = v4 >> 1 ; if ( (v5 & 1 ) ! = 0 ) v6 = (v5 >> 1 ) ^ 0xEDB88320 ; else v6 = v5 >> 1 ; if ( (v6 & 1 ) ! = 0 ) v7 = (v6 >> 1 ) ^ 0xEDB88320 ; else v7 = v6 >> 1 ; if ( (v7 & 1 ) ! = 0 ) v8 = (v7 >> 1 ) ^ 0xEDB88320 ; else v8 = v7 >> 1 ; if ( (v8 & 1 ) ! = 0 ) v9 = (v8 >> 1 ) ^ 0xEDB88320 ; else v9 = v8 >> 1 ; if ( (v9 & 1 ) ! = 0 ) v10 = (v9 >> 1 ) ^ 0xEDB88320 ; else v10 = v9 >> 1 ; v16[v2 + + ] = v10; } while ( v2 < 256 ); v11 = a2; for ( i = - 1 ; v11; - - v11 ) { v14 = * a1 + + ; i = v16[(i ^ v14)] ^ (i >> 8 ); } return ~i; } |
此函数进行了很多运算,但其出现了移位,异或等操作,而且还有一些可疑的数字,这是算法的特征,通过之前得到的提示,又或者百度这些数字
得到的结果是这个函数对用户名进行了crc32的操作,操作的结果被存在Value变量里面。
接下来strncpy函数将我们输入的序列号裁剪了一下,舍弃掉了后四位,将前10位存在了Destination变量中,随后_ultoa函数将被crc32的用户名转换为字符串之后存在Buffer变量中,最后比对Buffer变量和Destination,如果相等便正确。
算法梳理
已知我们的用户名是KCTF,经过上面的分析如何得到对应的序列号呢?
1.用户名KCTF需要进行CRC32运算
2.crc运算后的结果需要进行ultoa函数的处理
1 2 3 4 5 6 7 8 9 10 | #include <stdlib.h> #include <stdio.h> int main() { char result[ 30 ]; ultoa( 0x5ee54f4c , result, 10 ); printf(result); return 0 ; } |
3.ultoa函数得到的结果需要加上1212从而解密代码块
4.最后得到结果便是用户名:KCTF 序列码:15920863481212
写在最后:有一段时间没怎么逆向了,正好借助这次题目把状态找回来,最后附上分析的附件,IDA版本是7.5
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- [原创]KCTF2022 签到题 险象环生 2585
- [原创]一次对tx御安全的脱壳 21023
- [原创]对安卓反调试和校验检测的一些实践与结论 25258
- [求助]关于GG修改器lua脚本解密的疑惑 7027