【逆向初级】独树一帜

独树一帜

题干

题目地址:https://ctf.pediy.com/itembank.htm
在这里插入图片描述
是一个验证器,也可以叫做注册机。
题意是让我们拿到用户 :CTFHUB的Code

0x1 查壳

在这里插入图片描述

0x2 OD调试

我第一想法就是下消息断点,断在按钮类上
点击 “check” 后断下,但是消息断点的知识掌握的还不是很好,我没能追溯到消息处理后跳回的地址。

所以换了思路
EditText ,所以想到了之前使用过的方法
分析:

  1. 编辑框中的内容存在内存中
  2. 点击check后访问内存取出值

在这里插入图片描述
在这里插入图片描述
通过取值操作对内存的访问,定位函数的位置
在这里插入图片描述
果然成功的追到了函数,同时看到了win32API GetDialogTextA()函数,这里也给自己扩展了知识点,以后想断获取编辑框内容的断点可使用API断点 GetDialogTextA

在这里插入图片描述
我首先对函数头下断点,发现这个函数很大可能是一个窗口消息的回调函数,里面是个switch,对消息进行分类操作。

在这里插入图片描述
call .00401000 这个函数前 push了两个参数
压入参数分别为:

  1. Name
  2. KeyCode
    随着 cmp eax,0x1,然后jnz switch的default分支,不再继续执行
    所以可以猜测如果验证成功那么 eax中存放的应该是 0x1

这时候我开始耍小聪明,然后以为是只要弹出信息框就过了,在jnz的时候把ZFLAG改一下即可,或者把eax改为0x1,后来仔细去看题目,是让我提交一个Code

0x3 获取Code分析

首先要分析注册机的原理,我们输入一个注册码,程序生成一个正确的注册码,两者进行对比,从而判断是否正确。

那么获取Code的方式就有2种:

  1. 解析生成Code的算法
  2. 通过动态调试去找到生成后的Code直接拿来用

0x4 分析汇编

00401000  /$  53            push ebx                                 ;  独树一帜.00406A30
00401001  |.  8B5C24 0C     mov ebx,dword ptr ss:[esp+0xC]           ;  独树一帜.0040125C
00401005  |.  55            push ebp
00401006  |.  56            push esi                                 ;  独树一帜.00406930
00401007  |.  8B7424 10     mov esi,dword ptr ss:[esp+0x10]          ;  独树一帜.00406930
0040100B  |.  8A0B          mov cl,byte ptr ds:[ebx]
0040100D  |.  33ED          xor ebp,ebp
0040100F  |.  57            push edi
00401010  |.  8A06          mov al,byte ptr ds:[esi]
00401012  |.  3AC1          cmp al,cl
00401014  |.  0F85 69010000 jnz 独树一帜.00401183
压入 0x1
赋值 ebx 为 "keycode"
压入 ebp 不知道是啥
压入 esi 中 getDialogTextA()函数地址
赋值 esi 为 账号
赋值 cl 为 [ebx]的一个字节  'cl 为keycode第一个字符'
异或 ebp,ebp
压入 edi 0x0
赋值 al 为 [esi]的第一个字节 'al 为用户名的第一个字符'
比较 al 和 cl是否相等
如果相等则继续,否则跳转到 00401183 '结束函数'
//-----------------------------------------------这里分析出keycode首字符为C
0040101A  |.  8BFE          mov edi,esi                              ;  独树一帜.00406930
0040101C  |.  83C9 FF       or ecx,-0x1
0040101F  |.  33C0          xor eax,eax
00401021  |.  F2:AE         repne scas byte ptr es:[edi]
00401023  |.  F7D1          not ecx                                  ;  user32.75860043
00401025  |.  49            dec ecx                                  ;  user32.75860043
00401026  |.  83F9 05       cmp ecx,0x5
00401029  |. /0F82 54010000 jb 独树一帜.00401183

知识点:
repne scas 在汇编中用于遍历字符串
ecx 通过or ecx,-0x1 初始化为 FFFFFFFF
在edi中存放字符串,在AL中存放字符
每次循环 ecx - 1
not ecx 取反
dec ecx 减一 # 因为字符串遍历到最后一个截至符后需要再减一才是真正的字符串长度

赋值 edi 为 esi '用户名'
获取到用户名长度到 ecx
比较ecx和0x5
//-----------------------------------------------这里可以分析出用户名长度至少为5
0040102F  |.  807B 01 2D    cmp byte ptr ds:[ebx+0x1],0x2D
00401033  |.  0F85 4A010000 jnz 独树一帜.00401183
//-----------------------------------------------判断KeyCode第二个字符是否为 0x2D '-'

分析到这里我们可知道以下几点:

  1. CODE 前缀为 ‘C-’
  2. 用户名长度至少为5
00401039  |.  8BFE          mov edi,esi                              ;  独树一帜.00406930
0040103B  |.  83C9 FF       or ecx,-0x1
0040103E  |.  33C0          xor eax,eax
00401040  |.  33D2          xor edx,edx
00401042  |.  F2:AE         repne scas byte ptr es:[edi]
00401044  |.  F7D1          not ecx
00401046  |.  49            dec ecx

这一段代码很眼熟,前面有,就是取出EDI中字符串长度

00401049  |> /0FBE0C32      /movsx ecx,byte ptr ds:[edx+esi]
0040104D  |. |03E9          |add ebp,ecx
0040104F  |. |8BFE          |mov edi,esi                             ;  独树一帜.00406930
00401051  |. |83C9 FF       |or ecx,-0x1
00401054  |. |33C0          |xor eax,eax
00401056  |. |42            |inc edx
00401057  |. |F2:AE         |repne scas byte ptr es:[edi]
00401059  |. |F7D1          |not ecx
0040105B  |. |49            |dec ecx
0040105C  |. |3BD1          |cmp edx,ecx
0040105E  |.^\72 E9         \jb short 独树一帜.00401049

add ebp,0x6064

在这里插入图片描述
这里可看出是一个循环,弹出条件是 edx == ecx,ecx中存放的是用户名的长度
循环的操作是遍历字符串每个字符,字符的ASCII值加到ebp中
第一句是取出edx + esi地址的一个字符,edx初始值为0,随着循环 inc edx递增
跳出循环后 ebp 又加上了一个常数

00401066  |.  55            push ebp
00401067  |.  68 34604000   push 独树一帜.00406034                       ;  ASCII "%lu"
0040106C  |.  68 306B4000   push 独树一帜.00406B30                       ;  ASCII "49796"
00401071  |.  E8 B6030000   call 独树一帜.0040142C

后面觉得越看越不对劲,第一题不会这么难把??
然后我想着从后往前追溯,看看能不能找到生成好的CODE。
果然,不停的F8就可获取到

0040118E  |.  BF 446B4000   mov edi,独树一帜.00406B44                    ;  ASCII "C-B25120-49796"

那么虽然我有了这个正确的Code,如果题目出的难一点,他要随机生成用户的注册码,那么解决方案有2种:

  1. HOOK 生成Code的函数,获取到正确的Code
  2. 分析加密算法

0x5 分析加密算法实现注册机

这里其实就已经超纲了,题目解题在0X4就已经结束。有兴趣可继续往下看。

如何去判断正确的算法流程呢?我们已经有了正确的Code,所以跟着正确的Code应该就可以跑出正确的流程。

00401076  |.  8A16          mov dl,byte ptr ds:[esi]
00401078  |.  8BFE          mov edi,esi                              ;  独树一帜.00406930
0040107A  |.  83C9 FF       or ecx,-0x1
0040107D  |.  33C0          xor eax,eax
0040107F  |.  8815 446B4000 mov byte ptr ds:[0x406B44],dl
00401085  |.  C605 456B4000>mov byte ptr ds:[0x406B45],0x2D
0040108C  |.  F2:AE         repne scas byte ptr es:[edi]
0040108E  |.  F7D1          not ecx
00401090  |.  49            dec ecx
00401091  |.  0FBE4431 FF   movsx eax,byte ptr ds:[ecx+esi-0x1]
00401096  |.  50            push eax
00401097  |.  E8 C4020000   call 独树一帜.00401360

这一段汇编,先是取出用户名长度
然后 movsx eax,byte ptr ds:[ecx+esi-0x1],取出最后一个字符
push eax 把这个字符压栈,执行下面的函数,但是下面的这个函数执行后对程序的堆栈和寄存器没产生影响,所以这个函数是可忽略的。

0040109C  |.  A2 466B4000   mov byte ptr ds:[0x406B46],al
004010A1  |.  BF 306B4000   mov edi,独树一帜.00406B30                    ;  ASCII "25120"
004010A6  |.  83C9 FF       or ecx,-0x1
004010A9  |.  33C0          xor eax,eax
004010AB  |.  F2:AE         repne scas byte ptr es:[edi]
004010AD  |.  F7D1          not ecx
004010AF  |.  2BF9          sub edi,ecx
004010B1  |.  81C5 64600000 add ebp,0x6064

这段代码又是很眼熟的,然后后面又加上了0x6064

在这里插入图片描述
这里看到了C-B,然后往上看赋值语句
在这里插入图片描述
猜想 0x406B44为字符串数组的首地址

通过后面的调试发现 key格式为 C-{用户最后一个字符}{计算而来}-{计算而来}
地址为 00406B30,这个数据为重要的数据
所以对他下断点,定位程序什么时候对这个内存存在写入操作

就这样我定位到了算法内部,然后返回上层函数后,定位到了真正的加密函数中

在这里插入图片描述
但是又发现已经在堆栈中存在这个值,说明在执行这个函数之前,就已经进行了加密。
所以追溯之前调用的函数

00401F2A  |> /8B45 F0       |/mov eax,[local.4]
00401F2D  |. |FF4D F0       ||dec [local.4]
00401F30  |. |85C0          ||test eax,eax
00401F32  |. |7F 06         ||jg short 独树一帜.00401F3A
00401F34  |. |8BC6          ||mov eax,esi
00401F36  |. |0BC7          ||or eax,edi
00401F38  |. |74 3B         ||je short 独树一帜.00401F75
00401F3A  |> |8B45 F4       ||mov eax,[local.3]
00401F3D  |. |99            ||cdq
00401F3E  |. |52            ||push edx
00401F3F  |. |50            ||push eax
00401F40  |. |57            ||push edi
00401F41  |. |56            ||push esi
00401F42  |. |8945 C0       ||mov [local.16],eax
00401F45  |. |8955 C4       ||mov [local.15],edx
00401F48  |. |E8 03150000   ||call 独树一帜.00403450
00401F4D  |. |FF75 C4       ||push [local.15]
00401F50  |. |8BD8          ||mov ebx,eax
00401F52  |. |83C3 30       ||add ebx,0x30
00401F55  |. |FF75 C0       ||push [local.16]
00401F58  |. |57            ||push edi
00401F59  |. |56            ||push esi
00401F5A  |. |E8 81140000   ||call 独树一帜.004033E0
00401F5F  |. |83FB 39       ||cmp ebx,0x39
00401F62  |. |8BF0          ||mov esi,eax
00401F64  |. |8BFA          ||mov edi,edx
00401F66  |. |7E 03         ||jle short 独树一帜.00401F6B
00401F68  |. |035D D4       ||add ebx,[local.11]
00401F6B  |> |8B45 F8       ||mov eax,[local.2]
00401F6E  |. |FF4D F8       ||dec [local.2]
00401F71  |. |8818          ||mov byte ptr ds:[eax],bl
00401F73  |.^\EB B5         |\jmp short 独树一帜.00401F2A
00401F75  |>  8D45 B7       |lea eax,dword ptr ss:[ebp-0x49]
00401F78  |.  2B45 F8       |sub eax,[local.2]
00401F7B  |.  FF45 F8       |inc [local.2]

这里就是我们要的加密函数,其中第一个CALL为关键CALL
在这里插入图片描述
反复分析后发现,前面这段汇编是没有影响的, 取esp + 0x8位置是重要的,然后通过div ecx,有了进一步发现,发现ECX的值是恒定的,从外层函数可看出,0XA是个常数,div ecx,除数放在EAX中,处理后商放在EAX中,余数放在EDX中,然后进入下一次循环,这个商又作为被除的数,然后我也发现余数就是我们的第一个加密参数的逆序输出。

那么问题又回到了追溯这个EAX初始值 0x6261是如何来的?

在之前分析过有个 ebp += 用户名字符的ASCII 然后加一个0x6064
为了验证,我也对eax的来源进行追溯,下了内存访问断点,也正好回到了之前分析的位置。
在这里插入图片描述
然后就是用c++写个demo测试以下想法
在这里插入图片描述
这里是 6220 因为我测试的时候用的用户名和在OD里的不一样,但是如果一样 这个值是相等的。
所以我的猜想是正确的。


这里发现25120 02152是反过来的,我仔细一想… % 0xA 不就是一个十六进制转十进制的过程嘛?所以完全没必要重新计算。

第一个参数出来了,第二个参数也不遥远了,通过同样的内存写入断点,追溯关键CALL,发现最后还是来到了我们分析过的加密算法,只不过eax初始值不是 0x6220了,而是一个其他的数字,我想到之前不是还有个ebp+0x6064的地方嘛?
在这里插入图片描述
果然就是加上一个 6064就可可以了,然后就是转十进制的事情!

如果按照原算法复现,那应该是这样
在这里插入图片描述

但是实际上并不是那么复杂的,最后的代码

#include<iostream>
#include<string>
using namespace std;
int main(){
	string name;
	cout<<"输入用户名:";
	cin>>name;
	int strLen =  name.size();
	int ebp,code1,code2;
	code1 = code2 = 0;
	ebp = 0x0;
	for(int i=0;i < strLen;i++){
		ebp += int(name[i]);
	}
	ebp += 0x6064;
	code1 = ebp;
	code2 =code1 +  0x6064;

	cout<<"keys:"<<"C-"<<name[strLen-1]<<code1<<"-"<<code2<<endl;
	return 0;
} 

在这里插入图片描述

0x6

转发请亲们注明出处,有兴趣大家一起学习~

原创文章 38 获赞 35 访问量 1万+

猜你喜欢

转载自blog.csdn.net/shipsail/article/details/104619535
今日推荐