zoukankan      html  css  js  c++  java
  • 走读OpenSSL代码从一张奇怪的证书说起(十)

    上节,我们利用函数运行树对错误进行了初步定位

    读者可能会问: VC 的调试功能很强大,为什么不利用单步跟踪来定位?

    这是因为,对现有代码单步跟踪时,函数帧栈会发生意想不到的变化

    乍一听很不可思议,那就让我们在函数 ASN1_item_ex_d2i 调用 asn1_template_ex_d2i 的地方
    (case ASN1_ITYPE_SEQUENCE 分支)下断点, F5 启动,程序在中断处的调用栈为
    > openssl-0.9.8e.exe!ASN1_item_ex_d2i
      openssl-0.9.8e.exe!ASN1_item_d2i
      openssl-0.9.8e.exe!d2i_X509
      openssl-0.9.8e.exe!PEM_X509_INFO_read_bio
    接着再按 F10, 本来我们认为调试光标会指向下一行,但是光标仍停留在原处,调用栈反而变成
    > openssl-0.9.8e.exe!ASN1_item_ex_d2i
      openssl-0.9.8e.exe!asn1_template_noexp_d2i
      openssl-0.9.8e.exe!asn1_template_ex_d2i
      openssl-0.9.8e.exe!ASN1_item_ex_d2i
      openssl-0.9.8e.exe!ASN1_item_d2i
      openssl-0.9.8e.exe!d2i_X509
    顿时让你陷入不知所措 -- 至于为什么会这样,请读者思考

    另外,由于函数之间嵌套调用关系复杂,同一函数经常进入多次(参见上节中的函数运行树)
    很容易让人产生似曾相识的感觉:函数相同但上下文不同,难以对整体的代码运行有完整的理解和把握

    碰到这种情况,我们该怎么办,能否另辟蹊径?

    函数运行树就是笔者想到的一个尝试
    它可以解决上面碰到的问题:将函数的执行路径以树的形式呈现出来,克服了“只见树木,不见森林”的不足

    为什么要这样做?因为再复杂的业务逻辑,也可以通过硬啃代码来熟悉,但这只是时间的问题,算不上什么高明
    我们更关心的是,能否在这种代码走读的 routine 中有所提高

    问题紧接而来:有没有可能得到函数运行树?
    答案是明确的:当然

    我们来考察以函数 ASN1_item_ex_d2i(调用路径: d2i_X509->ASN1_item_d2i->ASN1_item_ex_d2i)为根的运行树
    下面是得到此运行树的简要说明,由于此过程比较 ad hoc, 就不再列出每一步的详细过程而只给出思路
    当然 Perl 在这之中仍扮演了重要的角色

    1、准备工作 -- 代码实现:获取正在运行函数的调用栈
    我们选择不重新造轮子,而是站在别人的肩膀上 -- 再次感谢伟大的 Internet!
    访问 http://www.codeproject.com/Articles/11132/Walking-the-callstack, 下载源码
    将代码改为 C 语言风格,核心函数改为 int ShowCallStack(char* str),加入 VC 工程

    2、在待显示调用栈的函数开头插入对 ShowCallStack 的函数调用,我们关心的函数有:
    crypto\asn1\tasn_dec.c
        ASN1_item_ex_d2i
        asn1_template_ex_d2i
        asn1_template_noexp_d2i
        asn1_d2i_ex_primitive
        asn1_ex_c2i
    crypto\asn1\tasn_new.c
        ASN1_item_new
        ASN1_item_ex_new
        asn1_item_ex_combine_new
        ASN1_template_new
        ASN1_primitive_new
    crypto\asn1\x_name.c
        x509_name_ex_d2i
    为区分同一函数的不同调用,待显示调用栈函数的入参信息作为 ShowCallStack 的入参

    3、F5 启动调试,结束后将 VC 输出窗口的调用栈显示结果,保存在文件 vc_callstack.txt 中
    其中的典型输出格式如下:

    asn1_item_ex_combine_new(sname=X509_CINF#itype=1#utype=16#type=NEW)
    ASN1_template_new
    asn1_item_ex_combine_new
    ASN1_item_ex_new
    ASN1_item_ex_d2i
    ASN1_item_d2i
    d2i_X509

    ASN1_template_new(field_name=version#flags=OPT_EXP_CONT_#item=ASN1_INTEGER)
    asn1_item_ex_combine_new
    ASN1_template_new
    asn1_item_ex_combine_new
    ASN1_item_ex_new
    ASN1_item_ex_d2i
    ASN1_item_d2i
    d2i_X509

    4、重排调用栈显示格式
    例如,将调用栈
        fun3
        fun2
        fun1
    重新排列成一行内显示的正常格式
    fun1  fun2  fun3

    使用下列 Perl 脚本即可完成

    # 说明:$/=qq/\n\n表示以连续两个换行符【空行】作为记录分隔标记,一次读一个调用栈
    #       split 分隔调用栈为列表(\n 为分隔标记),并逆序在一行内显示列表内容
    perl -ne "BEGIN{$/=qq/\n\n/;}; print qq/\n/;@a=split(qq/\n/,$_);@a=reverse @a;foreach (@a){print qq/$_  /;}" vc_callstack.txt > normal_call.txt

    5、生成的 normal_call.txt, 其函数排列类似如下(A,B,C...表示函数名)
        A
        A B
        A B C
        A B C D
        A B C D E
        A B C F
        A B C G
        A B H
        A B I
        A J
        A J K
        A L
        A M
    用 Perl 脚本,以列为单位,从上向下扫描,将遇到的与上面重复的函数去掉
    并转换成 XML 文件,最终得到以 A 为根的函数运行树
        A
          B
            C
              D
                E
              F
              G
            H
            I
          J
            K
          L
          M

    6、需要强调,在以上步骤,还会碰到某些问题,比如
    对函数 ShowCallStack 的微调,包括:调用栈不显示 ShowCallStack 本身、不显示函数 d2i_X509 以下的调用函数
    ASN1_item_ex_d2i(sname=X509#itype=1#utype=16) 中 # 改为空格
    对函数 ASN1_TYPE_new 的特殊处理,等等
    当然这些问题最终都 work around,细节就不再表述了

    最后,我们来回答上节留下的问题: asn1_ex_c2i 在哪里出错了?
    如下,根据当前运行的上下文,将调用函数 c2i_ASN1_INTEGER

    int asn1_ex_c2i(ASN1_VALUE **pval, const unsigned char *cont, int len,
          int utype, char *free_cont, const ASN1_ITEM *it)
    {
        switch(utype)
        {
          ......
          case V_ASN1_INTEGER:
          case V_ASN1_NEG_INTEGER:
          case V_ASN1_ENUMERATED:
          case V_ASN1_NEG_ENUMERATED:
              tint = (ASN1_INTEGER **)pval;
              if (!c2i_ASN1_INTEGER(tint, &cont, len))
                goto err;
              /* Fixup type to match the expected form */
              (*tint)->type = utype | ((*tint)->type & V_ASN1_NEG);
              break;
          ......
        }
    }

    F11 跟进函数 c2i_ASN1_INTEGER(crypto\asn1\a_int.c), 并继续单步跟踪,最终发现
    当运行到第 244 行时,由于判断条件满足,将执行 if 块里面的语句
        if ((*p == 0) && (len != 1)) // 序列号以 0x00 开头,且序列号长度 != 1
            {
            p++; // p 指向当前证书的序列号
            len--; // len 表示序列号长度
            }
        memcpy(s,p,(int)len); // if 条件满足,将导致序列号少复制一字节

    上面复制的缓冲区 s 构成 X509_CINF 的成员 ASN1_INTEGER *serialNumber
    由于内部表示少了一个字节,在后面的 i2d 环节现了原形,导致验证不通过

    马上验证一下,很简单:将 if 块里面的两行语句注释掉
    编译、链接、运行,历经千辛万苦,屏幕终于输出了 OK

  • 相关阅读:
    去掉python的警告
    LeetCode--687. 最长同值路径
    Python中获取字典中最值对应的键
    python -- 解决If using all scalar values, you must pass an index问题
    keras自定义padding大小
    评价指标的局限性、ROC曲线、余弦距离、A/B测试、模型评估的方法、超参数调优、过拟合与欠拟合
    一言难尽的js变量提升
    vue-cli 脚手架 安装
    十分钟入门 Less
    Echarts的资源文件
  • 原文地址:https://www.cnblogs.com/efzju/p/2674434.html
Copyright © 2011-2022 走看看