《C++ 黑客编程揭秘与防范(第2版)》——6.7 打造一个密码显示器

本文涉及的产品
公网NAT网关,每月750个小时 15CU
简介:

本节书摘来自异步社区出版社《C++ 黑客编程揭秘与防范(第2版)》一书中的第6章,第6.7节,作者:冀云,更多章节内容可以访问云栖社区“异步社区”公众号查看。

6.7 打造一个密码显示器

C++ 黑客编程揭秘与防范(第2版)
关于系统提供的调试API函数已经学习了不少,而且基本上常用到的函数都已学过。下面用调试API编写一个能够显示密码的程序。读者别以为这里写的程序什么密码都能显示,这是不可能的。下面针对前面的CrackMe来编写一个显示密码的程序。

在编写关于CrackMe的密码显示程序以前需要准备两项工作,第一项工作是知道要在什么地方合理地下断点,第二项工作是从哪里能读取到密码。带着这两个问题重新来思考一下。在这里的程序中,要对两个字符串进行比较,而比较的函数是strcmp(),该函数有两个参数,分别是输入的密码和真正的密码。也就是说,在调用strcmp()函数的位置下断点,通过查看它的参数是可以获取到正确的密码的。在调用strcmp()函数的位置设置INT3断点,也就是将0xCC机器码写入这个地址。用OD看一下调用strcmp()函数的地址,如图6-75所示。

image

图6-75 调用strcmp()函数的地址

从图6-75中可以看出,调用strcmp()函数的地址为00401E9E。有了这个地址,只要找到该函数的两个参数,就可以找到输入的错误的密码及正确的密码。从图6-75中可以看出,正确的密码的起始地址保存在EDX中,错误的密码的起始地址保存在ECX中。只要在00401E9E地址处下断点,并通过线程环境读取EDX和ECX寄存器值就可以得到两个密码的起始地址。

进行准备的工作已经做好了,下面来写一个控制台的程序。先定义两个常量,一个是用来设置断点的地址,另一个是INT3指令的机器码。定义如下:

// 需要设置INT3断点的位置
#define BP_VA  0x00401E9E
// INT3的机器码
const BYTE bInt3 = '\xCC';```
把CrackMe的文件路径及文件名当参数传递给显示密码的程序。显示的程序首先要以调试的方式创建CrackMe,代码如下:

  // 启动信息
  STARTUPINFO si = { 0 };
  si.cb = sizeof(STARTUPINFO);
  GetStartupInfo(&si);

  // 进程信息
  PROCESS_INFORMATION pi = { 0 };

  // 创建被调试进程
  BOOL bRet = CreateProcess(pszFileName,
            NULL,
            NULL,
            NULL,
            FALSE,
            DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS,
            NULL,
            NULL,
            &si,
            π);

  if ( bRet == FALSE )
  {
    printf("CreateProcess Error rn");
    return -1;
  }`
然后进入调试循环,要处理两个调试事件,一个是CREATEPROCESS_DEBUG_EVENT,另一个是EXCEPTION_DEBUG_EVENT下的EXCEPTION_BREAKPOINT。处理CREATE PROCESS_DEBUG_EVENT的代码如下:

// 创建进程时的调试事件
case CREATE_PROCESS_DEBUG_EVENT:
  {
    // 读取欲设置INT3断点处的机器码
    // 方便后面恢复
    ReadProcessMemory(pi.hProcess,
             (LPVOID)BP_VA,
             (LPVOID)&bOldByte,
             sizeof(BYTE),
             &dwReadWriteNum);

    // 将INT3的机器码0xCC写入断点处
    WriteProcessMemory(pi.hProcess,
              (LPVOID)BP_VA,
              (LPVOID)&bInt3,
              sizeof(BYTE),
              &dwReadWriteNum);
    break;
}```
在CREATE_PROCESS_DEBUG_EVENT中对调用strcmp()函数的地址处设置INT3断点,再将0xCC写入这里时要把原来的机器码读取出来。读取原机器码使用ReadProcess Memory(),写入INT3的机器码使用WriteProcessMemory()。读取原机器码的作用是当写入的0xCC产生中断以后,需要将原机器码写回,以便程序可以正确继续运行。

再来看一下EXCEPTION_DEBUG_EVENT下的EXCEPTION_BREAKPOINT是如何进行处理的,代码如下:

// 产生异常时的调试事件
case EXCEPTION_DEBUG_EVENT:
{
  // 判断异常类型
  switch ( de.u.Exception.ExceptionRecord.ExceptionCode )
  {
    // INT3类型的异常
  case EXCEPTION_BREAKPOINT:
    {
      // 获取线程环境
      context.ContextFlags = CONTEXT_FULL;
      GetThreadContext(pi.hThread, &context);

      // 判断是否断在设置的断点位置处
      if ( (BP_VA + 1) == context.Eip )
      {
        // 读取正确的密码
        ReadProcessMemory(pi.hProcess,
            (LPVOID)context.Edx,
            (LPVOID)pszPassword,
            MAXBYTE,
            &dwReadWriteNum);
        // 读取错误密码
        ReadProcessMemory(pi.hProcess,
            (LPVOID)context.Ecx,
            (LPVOID)pszErrorPass,
            MAXBYTE,
            &dwReadWriteNum);

        printf("你输入的密码是: %s rn", pszErrorPass);
        printf("正确的密码是: %s rn", pszPassword);

        //指令执行了INT3而被中断
        // INT3的机器指令长度为1字节
        // 因此需要将EIP减一来修正EIP
        // EIP是指令指针寄存器
        // 其中保存着下条要执行指令的地址
        context.Eip --;

        // 修正原来该地址的机器码
        WriteProcessMemory(pi.hProcess,
              (LPVOID)BP_VA,
              (LPVOID)&bOldByte,
              sizeof(BYTE),
              &dwReadWriteNum);
        // 设置当前的线程环境
        SetThreadContext(pi.hThread, &context);
      }
      break;
    }
  }
}`
对于调试事件的处理,应该放到调试循环中。上面的代码给出的是对调试事件的处理,再来看一下调试循环的大体代码:

while ( TRUE )
{
  // 获取调试事件
  WaitForDebugEvent(&de, INFINITE);

  // 判断事件类型
  switch ( de.dwDebugEventCode )
  {
    // 创建进程时的调试事件
    case CREATE_PROCESS_DEBUG_EVENT:
    {
        break;
    }
    // 产生异常时的调试事件
    case EXCEPTION_DEBUG_EVENT:
    {
      // 判断异常类型
      switch ( de.u.Exception.ExceptionRecord.ExceptionCode )
      {
        // INT3类型的异常
        case EXCEPTION_BREAKPOINT:
        {
        }
        break;
      }
    }
  }

  ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE);
}```
只要把调试事件的处理方法放入调试循环中,程序就完整了。接下来编译连接一下,然后把CrackMe直接拖放到这个密码显示程序上。程序会启动CrackMe进程,并等待用户的输入。输入账号及密码后,单击“确定”按钮,程序会显示出正确的密码和用户输入的密码,如图6-76所示。
![image](https://yqfile.alicdn.com/7ead49e3c34f4b19e9a8eef9d1f9bc9b5718b774.png)

图6-76 显示正确密码

根据图6-76显示的结果进行验证,可见获取的密码是正确的。程序到此结束,读者可以把该程序改成通过附加调试进程来显示密码,以巩固所学的知识。
相关实践学习
高可用应用架构
欢迎来到“高可用应用架构”课程,本课程是“弹性计算Clouder系列认证“中的阶段四课程。本课程重点向您阐述了云服务器ECS的高可用部署方案,包含了弹性公网IP和负载均衡的概念及操作,通过本课程的学习您将了解在平时工作中,如何利用负载均衡和多台云服务器组建高可用应用架构,并通过弹性公网IP的方式对外提供稳定的互联网接入,使得您的网站更加稳定的同时可以接受更多人访问,掌握在阿里云上构建企业级大流量网站场景的方法。 学习完本课程后,您将能够: 理解高可用架构的含义并掌握基本实现方法 理解弹性公网IP的概念、功能以及应用场景 理解负载均衡的概念、功能以及应用场景 掌握网站高并发时如何处理的基本思路 完成多台Web服务器的负载均衡,从而实现高可用、高并发流量架构
相关文章
|
3月前
|
数据安全/隐私保护 C++
c++实现http客户端和服务端的开源库以及Base64加密密码
c++实现http客户端和服务端的开源库以及Base64加密密码
|
7月前
|
Java 数据安全/隐私保护 C++
43.【Java 实现验证码获取 C++实现密码加密和删除和QQ登入系统】
43.【Java 实现验证码获取 C++实现密码加密和删除和QQ登入系统】
54 0
|
7月前
|
算法 数据安全/隐私保护 C++
RSA密码算法C++实现
RSA密码算法C++实现
156 0
|
数据安全/隐私保护 C++
C/C++编程题之密码验证合格程序
C/C++编程题之密码验证合格程序
|
安全 算法 数据安全/隐私保护
C/C++编程题之简单密码
密码是我们生活中非常重要的东东,我们的那么一点不能说的秘密就全靠它了。哇哈哈. 接下来渊子要在密码之上再加一套密码,虽然简单但也安全。
|
移动开发 分布式数据库 数据安全/隐私保护
|
数据安全/隐私保护 C++ C语言
【密码学】生成8位26个字母和数字的全排列(密码字典,密钥)c++代码(非递归高效直接)
生成8位26个字母和数字的全排列(密钥)代码(非递归高效直接) 用C语言或C++,生成一个8位的26个字母和数字的全排列的原代码,例如从:00000000到ZZZZZZZZ。
1934 0

热门文章

最新文章