• 逆向 ctype.h 函数库 isalnum、iscntrl、islower、isxdigit、tolower 函数


    0x01 isalnum 函数

    • 函数原型int isalnum(int c);
    • 函数功能:检查所传的字符是否是字母和数字
    • 动态链接库ucrtbase.dll
    • CC++ 实现
    #define _CRT_SECURE_NO_WARNINGS
    #include <Windows.h>
    #include <iostream>
    using namespace std;
    
    unsigned int __stdcall re_isalnum(void);
    
    int main(int argc, char** argv)
    {
    	re_isalnum();
    	return true;
    }
    
    unsigned int __stdcall re_isalnum(void)
    {
    	char str[] = "AAAAAAAAAA";
    	int var1 = 'd';
    	int var2 = '2';
    	int var3 = '	';
    	int var4 = ' ';
    
    	if (isalnum(var1))
    	{
    		printf("var1 = |%c| 是字母数字
    ", var1);
    	}
    	else
    	{
    		printf("var1 = |%c| 不是字母数字
    ", var1);
    	}
    	if (isalnum(var2))
    	{
    		printf("var2 = |%c| 是字母数字
    ", var2);
    	}
    	else
    	{
    		printf("var2 = |%c| 不是字母数字
    ", var2);
    	}
    	if (isalnum(var3))
    	{
    		printf("var3 = |%c| 是字母数字
    ", var3);
    	}
    	else
    	{
    		printf("var3 = |%c| 不是字母数字
    ", var3);
    	}
    	if (isalnum(var4))
    	{
    		printf("var4 = |%c| 是字母数字
    ", var4);
    	}
    	else
    	{
    		printf("var4 = |%c| 不是字母数字
    ", var4);
    	}
    
    	return true;
    }
    
    • 上述程序非常的简单,首先创建了 4 个变量分别为 var1、var2、var3、var4,然后分别判断这 4 个变量是否为数字和字母,并且根据 isalnum 函数返回的结果将它们打印出来。打印结果如下图所示:
      在这里插入图片描述

    注:程序中的 str 字符串只是为了定位程序,从而避开 mian 函数开始前复杂的初始化过程

    • 逆向分析:首先使用 OD 载入被测试的程序,之后搜索字符串 str,使得程序直接定位到 isalnum 函数附近处,并且在开头下断点,为的是方便反复调试。如下图所示已经到达了 main 函数处,并且看到了 isalnum 函数,之后进入第一个 isalnum 函数看看
      在这里插入图片描述
    • isalnum 函数的内部有三个小函数,可见这个函数功能不强,但是内部倒是复杂的很
      在这里插入图片描述
    • 由于已经调试过一遍,所以对这几个函数的功能比较了解。第一个函数主要是用于核对该动态链接库地址的一个常量是否等于 0,等于 0 返回 0,不等于 0 返回 1
      在这里插入图片描述

    setne 命令 是将 ZF 标志位去反后储存在 al 当中

    • 调用完第一个子函数 ucrtbase.63479727 后返回 0,由于返回的是 0,所以跳转会实现,也就跳过了 ucrtbase._isalnum_l 函数,直接执行最后一个子函数
      在这里插入图片描述
    • 在进去 ucrtbase.634b9400 函数前,压入了两个参数,分别是 6410764 就是传入需要检测的字符 dascii 码为 64)。进去这个函数看看,发现其中又调用了一个孙函数,而且压入的参数是一样的,没办法继续跟进这个孙函数看看
      在这里插入图片描述
    • 这个就是孙函数,流程还是十分简单的。首先取出第一个参数 64,也就是传入需要检测的字符,之后判断 64 是否在 -10xFF 之间,为什么要做这个判断呢,因为 ascii 码的范围就在 -10xFF 之间,所以在开头就进行数据过滤,防止程序出现 BUG。如果超出了这个范围就调用 ucrtbase._CrtDbgReportW 函数,这个函数的功能是弹出一个异常对话框表示程序异常,当然了在调试器调试的情况下才会有
      在这里插入图片描述
    • 如果没有超出这个范围的话,就调用 ucrtbase.634b25d7 这个函数,且压入的参数如上图所示,继续跟进这个函数
    • 如下图所示:首先和前面一样判断传入检测的字符是否在 -10xFF 之间,如果不在这个范围内的话就调用异常处理函数
      在这里插入图片描述
      在这里插入图片描述
    • 然后压入传入这个函数的第一个参数 0,之后调用 ucrtbase.63479750 函数,因为调用这个函数并没有传入检测字符的 ascii64,而且传入的参数是固定的,所以这个函数和 isalnum 函数的功能没有太大的关系,所以直接省略该函数的分析 在这里插入图片描述
    • 继续向下调试,会调用 ucrtbase.634797e3 函数
      在这里插入图片描述
    • ucrtbase.634797e3 函数如下图所示,主要功能是取 ecx + 0x4 的地址,ecx 可能是一个对象或者是一个结构体
      在这里插入图片描述
    • 之后经过两次寻址之后,将寻址后的值压入栈中作为第一个参数,并且调用 ucrtbase.63479705 函数,这个函数就是 isalnum 当中的一个核心函数,负责检测传入的字符是否为数字或者字母。
      在这里插入图片描述
    • F7 进入这个函数,下图就是函数的运行流程,主要是算法在这里插入图片描述
    • 那么是用什么算法取检测字符是否为数字或者字母的呢,其实很简单,就是运用的 and 命令来判断,并且有一个定值为 0x107,如果一个字符是字母或者数字,通过数组寻址的值和 and 107 做运算,那么结果就会大于 0,反之则会等于 0
    • 而这个数组就隐藏在 eax 当中,在内存中查看一下这个数组:
      在这里插入图片描述
    • 寻址方式就是 movzx eax,word ptr ds:[eax+ecx*2] 这条命令。下面来验证一下,查询一下 Ascii 表,把所有字母和数字的 16 进制都给表示出来,结合数组寻址可以计算出地址如下图所示:
    ecx -> ecx*2 -> eax+ecx*2
    30-39 -> 60-72 -> 6475BCC8-6475BCDA
    41-5a -> 82-B4 -> 6475BCEA-6475BD1C
    61-7a -> C2-F4 -> 6475BD2A-6475BD5C
    
    • 也就是图中的这个区域:只要是超过这个区域的值和 107and 计算都会等于 0
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
    • 实际调试看看:首先进行数组寻址,值储存在 eax 当中,且值为 0x82
      在这里插入图片描述
      在这里插入图片描述
    • 0x107and 计算后为 2,大于 0,所以字符为数字或者字母
      在这里插入图片描述
      在这里插入图片描述

    0x02 iscntrl 函数

    • 函数原型int iscntrl(int c);
    • 函数功能:检查所传的字符是否是控制字符。根据标准 ASCII 字符集,控制字符的 ASCII 编码介于 0x00 (NUL)0x1f (US) 之间,以及 0x7f (DEL),某些平台的特定编译器实现还可以在扩展字符集 (0x7f 以上) 中定义额外的控制字符。
    • CC++ 实现
    #define _CRT_SECURE_NO_WARNINGS
    #include <Windows.h>
    #include <iostream>
    using namespace std;
    
    unsigned int __stdcall cx_iscntrl(void);
    
    int main(int argc, char** argv)
    {
    	char str[] = "AAAAAAAAAA";
    	cx_iscntrl();
    	return true;
    }
    
    unsigned int __stdcall cx_iscntrl(void)
    {
    	int i = 0, j = 0;
    	char str1[] = "all a about 	 programming";
    	char str2[] = "Runoob 
     tutorials";
    
    	/* 输出字符串直到控制字符 a */
    	while (!iscntrl(str1[i]))
    	{
    		putchar(str1[i]);
    		i++;
    	}
    
    	/* 输出字符串直到控制字符 
     */
    	while (!iscntrl(str2[j]))
    	{
    		putchar(str2[j]);
    		j++;
    	}
    	return true;
    }
    
    • 上述程序是什么意思呢,首先创建了两个字符串,并且分别循环打印字符串中的非控制字符和控制字符,运行结果如下图所示:
      在这里插入图片描述
    • 逆向分析:因为基本上 ctype.h 函数库的所有函数的算法都基本一样,在上面的 isalnum 函数中已经详细的分析过了,所以下面就将简略的分析了。首先根据搜索字符串的方式定位到程序的 main 函数,理由就不多述了。由于该程序使 VS2019 编译的,所以第一个函数的功能是初始化堆栈的,第二个函数才是需要分析的子函数,F7 进入这个 NpCxyFW.0040123A 函数
      在这里插入图片描述
    • 在该函数的开头同样的进行了堆栈初始化的操作。值得注意的是有如下这几个命令,这几个命令其实是用来保存 GS Cookie 的,并且异或 ebp 加密后储存到 local.1 变量的位置,在函数的结尾处会把这个变量取出来解密并且和 .data 段中的 Cookie 作比较,目的是为了防止栈溢出,是保护栈的一种措施,俗称 GS
    ->	mov eax,dword ptr ds:[0x409004]
    	xor eax,ebp
    	mov dword ptr ss:[ebp-0x4],eax
    
    • 继续向下调试后会发现会将两个字符串分别赋值到 local.15local.22 局部变量中,之后初始化两个局部变量 lcoal.3local.6,且都赋值为 0
      在这里插入图片描述
    • 然后会看到一个循环语句,Ollydbg 已经将循环标注出来了,可以看出 local.3 是循环控制变量,再循环结尾处加 1,循环 break 出来的条件是 eax 不为 0,而 eaxiscntrl 函数的返回值,表示非控制字符的话就会跳出循环。之后通过 movsx ecx,byte ptr ss:[ebp+eax-0x3C] 这条语句取出第一个字符串的第一个字符,ebp-3Clocal.15 的地址,eax 为数组寻址,之后调用第一个 iscntrl 函数
      在这里插入图片描述
    • iscntrl 内部结构如下图所示:上面 iscntrl 已经介绍过了,其中第一个子函数是动态链接库中的常量验证操作,验证成功直接跳转,之后进入最后一个子函数 ucrtbase.0F2F9400
      在这里插入图片描述
    • 第一个子函数:
      在这里插入图片描述
    • ucrtbase.0F2F9400 函数内部继续调用了一个 curtbase._chvalidator 函数,且参数和传进来的参数是一样的
      在这里插入图片描述
    • 进去 curtbase._chvalidator 函数看看,首先判断字符是否为在标准的 Ascii 码范围内,如果不在的话就转入异常处理。接着会调用 curtbase_chvalidator_l 函数
      在这里插入图片描述
    • curtbase_chvalidator_l 函数内部首先依然会对传入的字符进行范围判断,判断是否在 Ascii 码的范围内,否的话转入异常处理,和上面一样。ucrtbase.0F2B97E3 这个函数是取数组地址的,在之后的判断字符是否为控制字符会有用
      在这里插入图片描述
    • 直接进入到 ucrtbase.0F2B9705 这个函数,这个函数就是处理字符的核心函数,和 isalnum 函数的算法是一模一样的,且 and 计算的固定值变为了 0x20
      在这里插入图片描述
    • 这个就是 eax 中储存的数组的首地址
      在这里插入图片描述

    0x03 islower 函数

    • 函数原型int islower(int c);
    • 函数功能:检查所传的字符是否是小写字母
    • CC++ 实现
    #define _CRT_SECURE_NO_WARNINGS
    #include <Windows.h>
    #include <iostream>
    using namespace std;
    
    unsigned int __stdcall cx_islower(void);
    
    int main(int argc, char** argv)
    {
    	char str[] = "AAAAAAAAAA";
    	cx_islower();
    	return true;
    }
    
    unsigned int __stdcall cx_islower(void)
    {
    	int var1 = 'Q';
    	int var2 = 'q';
    	int var3 = '3';
    
    	if (islower(var1))
    	{
    		printf("var1 = |%c| 是小写字母
    ", var1);
    	}
    	else
    	{
    		printf("var1 = |%c| 不是小写字母
    ", var1);
    	}
    	if (islower(var2))
    	{
    		printf("var2 = |%c| 是小写字母
    ", var2);
    	}
    	else
    	{
    		printf("var2 = |%c| 不是小写字母
    ", var2);
    	}
    	if (islower(var3))
    	{
    		printf("var3 = |%c| 是小写字母
    ", var3);
    	}
    	else
    	{
    		printf("var3 = |%c| 不是小写字母
    ", var3);
    	}
    	return true;
    }
    
    • 这个程序的首先初始化 3 个局部变量,之后分别对它们进行判断,判断是否为小写字母,并且根据 islower 函数的返回值打印出结果。运行结果如下图所示:
      在这里插入图片描述
    • 逆向分析:因为流程和上面基本一样,所以直接贴出核心函数,此时 and 计算的定值为 02
      在这里插入图片描述
    • islower 函数的功能是判断是否为小写字母,在 Ascii 码中小写字母的范围为 0x610x7A,根据 movzx eax,word ptr ds:[eax+ecx*2] 数组寻址命令,可以计算出范围是 0xFF5BD2A ~ 0xFF5BD5C
      在这里插入图片描述

    0x04 isxdigit 函数

    • 函数原型int isxdigit(int c);
    • 函数功能:检查所传的字符是否是十六进制数字
    • CC++ 实现
    #define _CRT_SECURE_NO_WARNINGS
    #include <Windows.h>
    #include <iostream>
    using namespace std;
    
    unsigned int __stdcall cx_isxdigit(void);
    
    int main(int argc, char** argv)
    {
    	char str[] = "AAAAAAAAAA";
    	cx_isxdigit();
    	return true;
    }
    
    unsigned int __stdcall cx_isxdigit(void)
    {
    	char var1[] = "tuts";
    	char var2[] = "0xE";
    
    	if (isxdigit(var1[0]))
    	{
    		printf("var1 = |%s| 是十六进制数字
    ", var1);
    	}
    	else
    	{
    		printf("var1 = |%s| 不是十六进制数字
    ", var1);
    	}
    
    	if (isxdigit(var2[0]))
    	{
    		printf("var2 = |%s| 是十六进制数字
    ", var2);
    	}
    	else
    	{
    		printf("var2 = |%s| 不是十六进制数字
    ", var2);
    	}
    	return true;
    }
    
    • 运行结果如下图所示:
      在这里插入图片描述
    • 逆向分析:核心算法函数如下图所示,and 计算的固定值为 0x80
      在这里插入图片描述
    • Ascii 码中,16 进制的范围为 0x30 ~ 0x39 && 0x41 ~ 0x46 && 0x61 ~ 0x66,数组首地址储存在 eax 当中,根据 movzx eax,word ptr ds:[eax+ecx*2] 这条数组寻址命令,可以计算出范围在 0xF73BCC8 ~ 0xF73BCDA && 0xF73BCEA ~ 0xF73BCF4 && 0xF73BD2A ~ 0xF73BD34。数组内存数据如下图所示:
      在这里插入图片描述

    0x05 tolower 函数

    • 函数原型int tolower(int c);
    • 函数功能: 把给定的字母转换为小写字母
    • CC++ 实现
    #define _CRT_SECURE_NO_WARNINGS
    #include <Windows.h>
    #include <iostream>
    using namespace std;
    
    unsigned int __stdcall cx_tolower(void);
    
    int main(int argc, char** argv)
    {
    	char str[] = "AAAAAAAAAA";
    	cx_tolower();
    	return true;
    }
    
    unsigned int __stdcall cx_tolower(void)
    {
    	int i = 0;
    	char c;
    	char str[] = "RUNOOB";
    
    	while (str[i])
    	{
    		putchar(tolower(str[i]));
    		i++;
    	}
    	return true;
    }
    
    • 上述程序的功能是将 str 字符串全部字符都转换成小写字符,运行结果如下图所示:
      在这里插入图片描述
    • 逆向分析:根据字符串直接定位到 main 函数的起始部分,然后进行完堆栈的初始化操作后,将需要测试的字符串复制到 local.3 局部变量中
      在这里插入图片描述
    • 紧接着的是一个循环(Ollydbg 中已经标识出来了),local.3 变量初始化为 0,用于循环计数。之后使用 movsx ecx,byte ptr ss:[ebp+eax-0x28] 命令进行数组寻址,循环取出字符串中的每一个字符,在这里取出的是第一个字符,接着调用 tolower 函数
      在这里插入图片描述
    • F7 进入 tolower 函数,发现其会调用 3 个子函数。第一个子函数是进行动态链接库中常量的验证工作,和 tolower 函数的核心功能没什么太大的联系。验证过后直接跳转到 0x0F8C5E67 这个地址
    • 下面就是实现小写字符转换的核心语句,首先程序会判断传入的字符是否在 0x41 ~ 0x5A 这个范围内,其实就是判断是否为大写字母,如果不是大写字母就返回传入的字符;反之如果是大写字母的话,就加上 0x20 使之变为小写字母,之后返回转换完的小写字母,函数结束
      在这里插入图片描述

    总结

    • 总的来说 ctype.h 函数库的实现难度比较高,尤其是这么多函数通过一个数组来判断字符,其算法是十分精妙的,可见开发这个库的程序员功力满深的,唯一美中不足的是函数的调用问题,函数调用有重复,而且对同一字符进行了 3 次重复的验证,降低了效率…

    逆向 ctype.h 库的 isalnum、iscntrl、islower、isxdigit、tolower 函数到此结束,如有错误,欢迎指正

  • 相关阅读:
    iOS开发UI篇—xib的简单使用
    iOS开发UI篇—字典转模型
    iOS开发UI篇—九宫格坐标计算
    iOS开发UI篇—懒加载
    2020121301-01
    2020120501-01
    2020113001-梦断代码-3
    2020112801-01
    2020112401
    2020112201-1
  • 原文地址:https://www.cnblogs.com/csnd/p/11800503.html
Copyright © 2020-2023  润新知