0x01 isalnum 函数

  • 函数原型int isalnum(int c);
  • 函数功能:检查所传的字符是否是字母和数字
  • 动态链接库ucrtbase.dll
  • C\C++ 实现
#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 = '\t';
int var4 = ' '; if (isalnum(var1))
{
printf("var1 = |%c| 是字母数字\n", var1);
}
else
{
printf("var1 = |%c| 不是字母数字\n", var1);
}
if (isalnum(var2))
{
printf("var2 = |%c| 是字母数字\n", var2);
}
else
{
printf("var2 = |%c| 不是字母数字\n", var2);
}
if (isalnum(var3))
{
printf("var3 = |%c| 是字母数字\n", var3);
}
else
{
printf("var3 = |%c| 不是字母数字\n", var3);
}
if (isalnum(var4))
{
printf("var4 = |%c| 是字母数字\n", var4);
}
else
{
printf("var4 = |%c| 不是字母数字\n", 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 以上) 中定义额外的控制字符。
  • C\C++ 实现
#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 \t programming";
char str2[] = "Runoob \n tutorials"; /* 输出字符串直到控制字符 \a */
while (!iscntrl(str1[i]))
{
putchar(str1[i]);
i++;
} /* 输出字符串直到控制字符 \n */
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);
  • 函数功能:检查所传的字符是否是小写字母
  • C\C++ 实现
#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| 是小写字母\n", var1);
}
else
{
printf("var1 = |%c| 不是小写字母\n", var1);
}
if (islower(var2))
{
printf("var2 = |%c| 是小写字母\n", var2);
}
else
{
printf("var2 = |%c| 不是小写字母\n", var2);
}
if (islower(var3))
{
printf("var3 = |%c| 是小写字母\n", var3);
}
else
{
printf("var3 = |%c| 不是小写字母\n", 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);
  • 函数功能:检查所传的字符是否是十六进制数字
  • C\C++ 实现
#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| 是十六进制数字\n", var1);
}
else
{
printf("var1 = |%s| 不是十六进制数字\n", var1);
} if (isxdigit(var2[0]))
{
printf("var2 = |%s| 是十六进制数字\n", var2);
}
else
{
printf("var2 = |%s| 不是十六进制数字\n", 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);
  • 函数功能: 把给定的字母转换为小写字母
  • C\C++ 实现
#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 函数到此结束,如有错误,欢迎指正

逆向 ctype.h 函数库 isalnum、iscntrl、islower、isxdigit、tolower 函数的更多相关文章

  1. 逆向 string.h 函数库 memset、strcpy、strcmp 函数

    memset 函数 函数原型:void *memset(void *str, int c, size_t n) 主要功能:复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符 ...

  2. 【C++实现python字符串函数库】二:字符串匹配函数startswith与endswith

    [C++实现python字符串函数库]字符串匹配函数startswith与endswith 这两个函数用于匹配字符串的开头或末尾,判断是否包含另一个字符串,它们返回bool值.startswith() ...

  3. 逆向 string.h 函数库 strlen、memchr、strcat 函数

    strlen 函数 主要功能:返回字符串的长度 C/C++ 实现: #include <iostream> #include <stdio.h> #include <st ...

  4. c 头文件<ctype.h>(一)

    头文件<ctype.h>中声明了一些测试字符的函数. 每个函数的参数均为int类型,参数的值必须是EOF或可用unsigned char类型表示的字符,函数返回值为int类型. 如果参数c ...

  5. gcc 头文件是用户应用程序和函数库之间的桥梁和纽带 功能的真正逻辑实现是以硬件层为基础

    gcc GCC, the GNU Compiler Collection - GNU Project - Free Software Foundation (FSF) http://gcc.gnu.o ...

  6. 标准C函数库的使用方法

    本篇介绍若干经常使用的标准C函数的使用方法,主要介绍stdio(标准输入输出).math(数字函数库).time(时间函数库).stdlib(标准函数库)string(标准字符串函数)等. 最后更新  ...

  7. (转)函数库调用 VS 系统调用

    Linux下对文件操作有两种方式:系统调用(system call)和库函数调用(Library functions).可以参考<Linux程序设计>(英文原版为<Beginning ...

  8. python函数库及函数标准库

    一.系统库提供的内部函数 字符函数库: 1)str.islower() :字符串是否全部是小写 2)str.isspace() :字符串是否为空 3)help(str):查询字符串函数库 4)str. ...

  9. 为开发者准备的 Android 函数库(2016 年版)

    转载:http://www.androidchina.net/5922.html第三方函数库(译者注:包括第三方提供的 SDK,开源函数库)以惊人的方式助力着 Android 开发,借助这些其他开发人 ...

随机推荐

  1. 如何在 ASP.NET Core 中写出更干净的 Controller

    你可以遵循一些最佳实践来写出更干净的 Controller,一般我们称这种方法写出来的 Controller 为瘦Controller,瘦 Controller 的好处在于拥有更少的代码,更加单一的职 ...

  2. Celery:进一步探索

    一.创建Celery专用模块 对于大型项目,一般需要创建一个专用模块,便于管理. 1.1 模块结构 proj/__init__.py /celery.py /tasks.py proj/celery. ...

  3. 从一部电影史上的趣事了解 Spring 中的循环依赖问题

    title: 从一部电影史上的趣事了解 Spring 中的循环依赖问题 date: 2021-03-10 updated: 2021-03-10 categories: Spring tags: Sp ...

  4. vue 弹窗禁止底层滚动

    原因:底层视图高度超出百分百,加入弹窗后再苹果浏览器隐藏上下栏的情况下遮罩层没有完全遮住底层. 处理:打开弹窗后禁止底层滚动调用stop事件,关闭则开启底层滚动调用move事件. let mo=fun ...

  5. Co-prime HDU - 4135

    题目链接:https://vjudge.net/problem/HDU-4135#author=0 题意:求在区间[a,b]中有多少个数与n互质. 思路:先看数据范围很大,所以不能枚举.因为互质难求, ...

  6. java例题_11 求不重复数

    1 /*11 [程序 11 求不重复数字] 2 题目:有 1.2.3.4 这四个数字,能组成多少个互不相同且无重复数字的三位数?都是多少? 3 程序分析:可填在百位.十位.个位的数字都是 1.2.3. ...

  7. Java学习之随机数的用法

    •前言 随机数的产生在一些代码中很常用,也是我们必须要掌握的. 而 Java 中产生随机数的方法主要有三种: new Random() Math.random() currentTimeMillis( ...

  8. DAOS 分布式异步对象存储|存储模型

    概述 DAOS Pool 是分布在 Target 集合上的存储资源预留.分配给每个 Target 上的 Pool 的实际空间称为 Pool Shard. 分配给 Pool 的总空间在创建时确定,后期可 ...

  9. PAT (Advanced Level) Practice 1023 Have Fun with Numbers (20 分) 凌宸1642

    PAT (Advanced Level) Practice 1023 Have Fun with Numbers (20 分) 凌宸1642 题目描述: Notice that the number ...

  10. python基础(五):列表的使用(上)

    什么是列表 列表是一系列元素,按特定顺序排列组成.列表总的元素之间没有任何关系,既可以时字符串,也可以是数字,还可以是布尔值. 由此可以看出,列表通常包含多个元素,因此再给列表命名的时候,最好使用复数 ...