2023年3月

使用vCenter 对ESXi 主机进行补丁升级

背景说明:公司内部有许多ESXi主机需要进行补丁升级,记录一下通过vCenter对ESXi主机进行补丁升级的过程,也可以使用esxcli命令行方式。

vsphere版本:vCenter 6.7 和 ESXi 6.7

实操过程

1、查看ESXi主机版本(可以在ESXI主机或者VCenter中查看主机当前版本号)

2、登录 VMware补丁发布地址
产品补丁程序 - VMware Customer Connect
,选择产品为“ESXi (Embedded and Installable) ”,并将版本选择为“6.7.0”然后点击下方的“搜索”。然后下载最新补丁。

3、在vCenter平台上层下载好的补丁包

3.1、点击菜单,点击Update Manager。

3.2、点击更新,点击文件上载(导入)

3.3、点击浏览,选择对应补丁包,


3.4、导入补丁包

如下图,我们可以看到导入的补丁程序。

4、点击基准,新建基准

创建基准,自定义名称,选择内容(本次是补丁升级,所以选择修补程序)

这里会自动添加所匹配到的修补程序


如下图,新基准的内容下就是我们刚才导入的补丁包的内容。

5、主机页,选择ESXi主机>>更新>>附加>>附加基准或基准组

注意:
更新前,将需要进行补丁升级的ESXi主机上的虚拟机关机或迁移,并进入维护模式。

选择刚才新建的基准(esxi_buding)

6、点击修复

修复前会预检查,一般会提示一些问题。我这里它发现HA问题,并自动把HA禁用掉了。

7、接下来就是等待修复完成就行,这个过程不出意外不需要人工干预。

到物理服务器处看了一下,系统已经在自动进行补丁升级了。

8、升级完成。vcenter自动把esxi主机纳管进去,这里可以看到esxi主机已经是最新的版本号。

hi,我是熵减,见字如面。

对于技术团队来说,开发软件产品,是一项长期的工作。

就如同马拉松一样,要完成这样的游戏,需要的多个迭代周期和很多冲刺的不断地积累。

在游戏的过程中,需要持续的、有节奏的向着目标前进,并在此过程要不断地做出调整和改变。

然而,在现实中,今天有不少的团队,是无法如此有效的开展工作的。

其背后的
原因是:团队将冲刺(Sprints)的数量和周期作为了目标。

将短期的冲刺做为团队的目标,就是将开发产品的工作,看为了一项短跑游戏。它的本质上就变成了一种浪费,因为团队会最终陷入一些毫无意义的工作任务之中。

尤其是在今天,在互联网增长神话消失后,唯快不破的方式,已经不能给产品或业务带来太多的意义。

所以,及时的调整团队工作的节奏,对团队和业务的长期发展是更为有价值的。

短跑的“敏捷”

在冲刺(Sprints)的数量和周期成为团队的目标后,整个团队的动作就会悄然发生改变。

现实中,大多数团队都采用一些类似敏捷的方法,在这种方法中,开发工作会在迭代中开展。团队会计划一些 Sprints ,会评估如何完成这些任务,并在完成后进行任务过程的回顾。而对于Sprints的背后的要交付的产品价值,要解决的实际问题,研发团队是很少讨论,也不太愿意去关注的。

同时,为了增加冲刺(Sprints)的数量,或减少完成冲刺(Sprints)的时间投入,团队会做出很多没必要的拆分,从大的数据上来看:所有的事情都在不断的进行,Sprints的完成量和完成时间投入也非常的合理。

但其实呢,产研团队可能在玩一个短跑冲刺的游戏,来迎合管理层的考核目标。在短跑游戏中,团队往往就会偏离了业务价值的交付的目的。

短跑冲刺的伤害

其实,这种空洞的短跑冲刺游戏,会给团队造成多方面的伤害:

首先,团队的稀缺的研发资源被浪费在了很多无意义的冲刺(Sprints)之上。从表面上来看,团队是完成了很多的任务,交付任务的效率也很高,但真正交付的业务价值是多少,是很少有人去关心的。

第二,团队的冲刺(Sprints)回顾,成为了一个形式化的步骤。回顾,更多的只停留在第一层的任务完成的过程上的复盘,而未能在向前推进一步,对于冲刺(Sprints)的业务价值,方向上的问题,团队总是视而不见的。

第三,工程师们逐渐丧失了价值感。工程师们逐渐成为了任务完成的一个终端,而无法真正参与到问题的解决之中,长时间的参与感缺失,就会对工作失去了价值感和意义感,稳定性也就成为了一个问题。

总之,对于技术团队,最稀缺的资源就是工程团队的时间资源,如果我们如此任性将时间浪费在空洞的短跑冲刺游戏上,是非常可惜的。

回归意义

要解决空洞的短跑游戏问题,其办法也是非常简单的:那就是
让冲刺(Sprints)有意义。

让团队有全局的视角,找到一个健康的节奏,将有效的时间资源,尽可能的投入到有业务价值的功能交付之上。

具体如下:

首先,在管理上,不只是单一维度的去考核团队的冲刺数量和交付时间周期的指标。

第二,要让产研团队一起去设定每一个“冲刺”的业务价值,让每个人(包括他们自己)都理解朝着这个价值前进的重要性。

第三,团队要能根据实际的情况,动态的调整“冲刺”的重要性和优先级,任务范围的大小、交付周期的计划等,不在追求虚假的指标。

第四,在团队的“冲刺”回顾中,要向上有深挖假设的有效性,譬如,问题是否定位准确,方案设计是否有效,交付的业务价值是否符合预期等。而不只是任务完成过程中的研发效率那一小块东西。

数字产品研发和交付,是一个如同马拉松一样的长跑游戏。

在此过程中,要找到合适的短期目标,控制好整体的节奏,不断做出有意义的回顾和调整,让团队看见真实的意义和价值,是持续发展的关键。

写在最后

软件产品的开发过程,不是一项短跑冲刺的游戏,而是一项持续的马拉松项目。

如果将研发过程简化为只有冲刺数量和交付周期的短期任务,而忽略了团队内人员内在需求,资源的浪费就是必然的事情。

回顾到敏捷和冲刺(Sprints)的本质之上,要让团队将更多的精力和能量,用在交付更有业务价值的迭代之上,才是更有意义的。

好的目标,会带来好的结果,所以小心你的考核目标。


阅读,思考,练习,分享,日日不断之功。

嗯,写完了。

新的一天,加油哦 (ง •̀_•́)ง

直观的界面、出色的计算功能和图表工具,使Excel成为最流行的个人计算机数据处理软件。在独立的数据包含的信息量太少,而过多的数据又难以理清头绪时,制作成表格是数据管理的最有效手段之一。这样不仅可以方便整理数据,还可以方便我们查找和应用数据。后期我们还可以对具有相似表格框架,相同性质的数据进行合并汇总工作。在本文中,您将学习如何使用
Spire.XLS for C++
创建 Excel 文档
,以及如何
将数据写入 Excel 工作表

  • 在 C++ 中将文本或数字值写入单元格
  • 在 C++ 中将数组写入指定的单元格范围

安装Spire.XLS for C++

有两种方法可以将 Spire.XLS for C++ 集成到您的应用程序中。一种方法是通过
NuGet
安装它,另一种方法是从我们的网站
下载包
并将库复制到您的程序中。通过 NuGet 安装更简单,更推荐使用。您可以通过访问以下链接找到更多详细信息。

在 C++ 应用程序中集成 Spire.XLS for C++

在 C++ 中将文本或数字值写入单元格

Spire.XLS for C++ 提供了
Workbook
类和
Worksheet
类,分别表示 Excel 文档和工作表。 用户可以使用
Worksheet->GetRange(int row, int column)
方法访问特定的单元格。然后,使用
CellRange->SetText()

CellRange->SetNumberValue(
) 方法为单元格分配一个文本值或数字值。以下是详细步骤:

  • 创建一个
    Workbook
    对象。
  • 使用
    Workbook->GetWorksheets()->Get()
    方法获取第一个工作表。
  • 使用
    Worksheet->GetRange(int row, int column)
    方法获取特定单元格。
  • 使用
    CellRange->SetText()

    CellRange->SetNumberValue()
    方法将文本值或数字值添加到指定的单元格。
  • 使用
    Workbook->SaveToFile()
    方法将工作簿保存到 Excel 文件。

完整代码

C++

#include "Spire.Xls.o.h";using namespaceSpire::Xls;intmain() {//指定输出文件路径和名称
    std::wstring outputPath = L"输出\\";
std::wstring outputFile
= outputPath + L"将单个值写入单元格.xlsx";//创建一个Workbook对象 Workbook* workbook = newWorkbook();//获取第一个工作表 Worksheet* sheet = workbook->GetWorksheets()->Get(0);//将文本和数字写入指定的单元格 sheet->GetRange(1, 1)->SetText(L"名字");
sheet
->GetRange(1, 2)->SetText(L"年龄");
sheet
->GetRange(1, 3)->SetText(L"部门");
sheet
->GetRange(1, 4)->SetText(L"入职日期");
sheet
->GetRange(1, 1)->SetText(L"名字");
sheet
->GetRange(2, 1)->SetText(L"谢殊");
sheet
->GetRange(2, 2)->SetNumberValue(29);
sheet
->GetRange(2, 3)->SetText(L"市场部");
sheet
->GetRange(2, 4)->SetText(L"2018-02-26");
sheet
->GetRange(3, 1)->SetText(L"李强");
sheet
->GetRange(3, 2)->SetNumberValue(30);
sheet
->GetRange(3, 3)->SetText(L"人力资源部");
sheet
->GetRange(3, 4)->SetText(L"2017-07-13");
sheet
->GetRange(4, 1)->SetText(L"高阳");
sheet
->GetRange(4, 2)->SetNumberValue(35);
sheet
->GetRange(4, 3)->SetText(L"策划部");
sheet
->GetRange(4, 4)->SetText(L"2015-04-01");//自动调整列宽 sheet->GetAllocatedRange()->AutoFitColumns();//将样式应用于第一行 CellStyle* style = workbook->GetStyles()->Add(L"newStyle");
style
->GetFont()->SetIsBold(true);
sheet
->GetRange(1, 1, 1, 4)->SetStyle(style);//保存文件 workbook->SaveToFile(outputFile.c_str(), ExcelVersion::Version2016);
workbook
->Dispose();
}

效果图

在 C++ 中将数组写入指定的单元格范围

Spire.XLS for C++ 提供了
Worksheet->InsertArray()
方法,它允许程序员将向量写入工作表的指定单元格范围。在将数组写入工作表之前,您需要将它们转换为向量。将数组写入工作表的步骤如下:

  • 创建一个
    Workbook
    对象。
  • 使用
    Workbook->GetWorksheets()->Get()
    方法获取第一个工作表。
  • 创建一个数组并将其转换为一个向量或多个向量。
  • 使用
    Worksheet->InsertArray()
    方法将向量插入工作表。
  • 使用
    Workbook->SaveToFile()
    方法将工作簿保存到 Excel 文件。

完整代码

C++

#include "Spire.Xls.o.h";using namespaceSpire::Xls;using namespacestd;intmain() {//指定输出文件路径和名称
    wstring outputPath = L"输出\\";
wstring outputFile
= outputPath + L"将数组写入指定的单元格范围.xlsx";//创建一个Workbook对象 Workbook* workbook = newWorkbook();//获取第一个工作表 Worksheet* sheet = workbook->GetWorksheets()->Get(0);//创建一维数组 wstring oneDimensionalArray[6] = { L"一月", L"二月", L"三月", L"四月", L"五月", L"六月"};//将数组转换为向量 vector<LPCWSTR>vec;for (size_t i = 0; i < sizeof(oneDimensionalArray) / sizeof(oneDimensionalArray[0]); i++)
{
vec.push_back(oneDimensionalArray[i].c_str());
}
//将向量插入工作表 sheet->InsertArray(vec, 1, 1, false);//创建一个二维数组 wstring twoDimensionalArray[4][5] ={
{L
"姓名", L"年龄", L"性别", L"部门.", L"联系方式."},
{L
"李刚", L"25", L"", L"广告部", L"835256"},
{L
"刘兴桐", L"24", L"", L"运营策划部", L"835583"},
{L
"陈海波", L"26", L"", L"销售部", L"834176"}
};
//获取行号和列号 int rowNum = sizeof(twoDimensionalArray) / sizeof(twoDimensionalArray[0]);int columnNum = sizeof(twoDimensionalArray[0]) / sizeof(twoDimensionalArray[0][0]);//将二维数组拆分为多个一维向量 for (size_t i = 0; i < rowNum; i++)
{
vector
<LPCWSTR>vec_temp;for (size_t j = 0; j < columnNum; j++)
{
vec_temp.push_back(twoDimensionalArray[i][j].c_str());
}
//将向量插入工作表 sheet->InsertArray(vec_temp, 4 + i, 1, false);
}
//自动调整列宽 sheet->GetAllocatedRange()->AutoFitColumns();//将样式应用于第一行 CellStyle* style = workbook->GetStyles()->Add(L"newStyle");
style
->GetFont()->SetIsBold(true);
sheet
->GetRange(1, 1, 1, 6)->SetStyle(style);
sheet
->GetRange(4, 1, 4, 5)->SetStyle(style);//保存文件 workbook->SaveToFile(outputFile.c_str(), ExcelVersion::Version2016);
workbook
->Dispose();
}

效果图

—本文完—

一、说明

在JDBC中,executeBatch这个方法可以将多条dml语句批量执行,效率比单条执行executeUpdate高很多,这是什么原理呢?在mysql和oracle中又是如何实现批量执行的呢?本文将给大家介绍这背后的原理。

二、实验介绍

本实验将通过以下三步进行
a. 记录jdbc在mysql中批量执行和单条执行的耗时
b. 记录jdbc在oracle中批量执行和单条执行的耗时
c. 记录oracle plsql批量执行和单条执行的耗时
相关java和数据库版本如下:Java17,Mysql8,Oracle11G

三、正式实验

在mysql和oracle中分别创建一张表

create table t (  -- mysql中创建表的语句
    id    int,
    name1 varchar(100),
    name2 varchar(100),
    name3 varchar(100),
    name4 varchar(100)
);
create table t (  -- oracle中创建表的语句
    id    number,
    name1 varchar2(100),
    name2 varchar2(100),
    name3 varchar2(100),
    name4 varchar2(100)
);

在实验前需要打开数据库的审计
mysql开启审计:

set global general_log = 1;

oracle开启审计:

alter system set audit_trail=db, extended;  
audit insert table by scott;  -- 实验采用scott用户批量执行insert的方式

java代码如下:

import java.sql.*;

public class JdbcBatchTest {

    /**
     * @param dbType 数据库类型,oracle或mysql
     * @param totalCnt 插入的总行数
     * @param batchCnt 每批次插入的行数,0表示单条插入
     */
    public static void exec(String dbType, int totalCnt, int batchCnt) throws SQLException, ClassNotFoundException {
        String user = "scott";
        String password = "xxxx";
        String driver;
        String url;
        if (dbType.equals("mysql")) {
            driver = "com.mysql.cj.jdbc.Driver";
            url = "jdbc:mysql://ip/hello?useServerPrepStmts=true&rewriteBatchedStatements=true";
        } else {
            driver = "oracle.jdbc.OracleDriver";
            url = "jdbc:oracle:thin:@ip:orcl";
        }

        long l1 = System.currentTimeMillis();
        Class.forName(driver);
        Connection connection = DriverManager.getConnection(url, user, password);
        connection.setAutoCommit(false);
        String sql = "insert into t values (?, ?, ?, ?, ?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        for (int i = 1; i <= totalCnt; i++) {
            preparedStatement.setInt(1, i);
            preparedStatement.setString(2, "red" + i);
            preparedStatement.setString(3, "yel" + i);
            preparedStatement.setString(4, "bal" + i);
            preparedStatement.setString(5, "pin" + i);

            if (batchCnt > 0) {
                // 批量执行
                preparedStatement.addBatch();
                if (i % batchCnt == 0) {
                    preparedStatement.executeBatch();
                } else if (i == totalCnt) {
                    preparedStatement.executeBatch();
                }
            } else {
                // 单条执行
                preparedStatement.executeUpdate();
            }
        }
        connection.commit();
        connection.close();
        long l2 = System.currentTimeMillis();
        System.out.println("总条数:" + totalCnt + (batchCnt>0? (",每批插入:"+batchCnt) : ",单条插入") + ",一共耗时:"+ (l2-l1) + " 毫秒");
    }

    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        exec("mysql", 10000, 50);
    }
}

代码中几个注意的点,

  1. mysql的url需要加入useServerPrepStmts=true&rewriteBatchedStatements=true参数。
  2. batchCnt表示每次批量执行的sql条数,0表示单条执行。

首先测试mysql

exec("mysql", 10000, batchCnt);

代入不同的batchCnt值看执行时长
batchCnt=50 总条数:10000,每批插入:50,一共耗时:4369 毫秒
batchCnt=100 总条数:10000,每批插入:100,一共耗时:2598 毫秒
batchCnt=200 总条数:10000,每批插入:200,一共耗时:2211 毫秒
batchCnt=1000 总条数:10000,每批插入:1000,一共耗时:2099 毫秒
batchCnt=10000 总条数:10000,每批插入:10000,一共耗时:2418 毫秒
batchCnt=0 总条数:10000,单条插入,一共耗时:59620 毫秒

查看general log
batchCnt=50
微信截图_20230310143127.png

batchCnt=0
微信截图_20230310145753.png

可以得出几个结论:

  1. 批量执行的效率相比单条执行大大提升。
  2. mysql的批量执行其实是改写了sql,将多条insert合并成了insert xx values(),()...的方式去执行。
  3. 将batchCnt由50改到100的时候,时间基本上缩短了一半,但是再扩大这个值的时候,时间缩短并不明显,执行的时间甚至还会升高。

分析原因:
当执行一条sql语句的时候,客户端发送sql文本到数据库服务器,数据库执行sql再将结果返回给客户端。总耗时 = 数据库执行时间 + 网络传输时间。使用批量执行减少往返的次数,即降低了网络传输时间,总时间因此降低。但是当batchCnt变大,网络传输时间并不是最主要耗时的时候,总时间降低就不会那么明显。特别是当batchCnt=10000,即一次性把1万条语句全部执行完,时间反而变多了,这可能是由于程序和数据库在准备这些入参时需要申请更大的内存,所以耗时更多(我猜的)。
再来说一句,batchCnt这个值是不是能无限大呢,假设我需要插入的是1亿条,那么我能一次性批量插入1亿条吗?当然不行,我们不考虑undo的空间问题,首先你电脑就没有这么大的内存一次性把这1亿条sql的入参全部保存下来,其次mysql还有个参数max_allowed_packet限制单条语句的长度,最大为1G字节。当语句过长的时候就会报"Packet for query is too large (1,773,901 > 1,599,488). You can change this value on the server by setting the 'max_allowed_packet' variable"。

接下来测试oracle

exec("oracle", 10000, batchCnt);

代入不同的batchCnt值看执行时长
batchCnt=50 总条数:10000,每批插入:50,一共耗时:2055 毫秒
batchCnt=100 总条数:10000,每批插入:100,一共耗时:1324 毫秒
batchCnt=200 总条数:10000,每批插入:200,一共耗时:856 毫秒
batchCnt=1000 总条数:10000,每批插入:1000,一共耗时:785 毫秒
batchCnt=10000 总条数:10000,每批插入:10000,一共耗时:804 毫秒
batchCnt=0 总条数:10000,单条插入,一共耗时:60830 毫秒
可以看到oracle中执行的效果跟mysql中基本一致,批量执行的效率相比单条执行都大大提升。问题就来了,oracle中并没有这种insert xx values(),()..语法呀,那它是怎么做到批量执行的呢?

查看当执行batchCnt=50的审计视图dba_audit_trail
微信截图_20230310152235.png
从审计的结果中可以看到,batchCnt=50的时候,审计记录只有200条(扣除登入和登出),也就是sql只执行了200次。sql_text没有发生改写,仍然是"insert into t values (:1 , :2 , :3 , :4 , :5 )",而且sql_bind只记录了批量执行的最后一个参数,即50的倍数。从awr报告中也能看出的确是只执行了200次(限于篇幅,awr截图省略)。那么oracle是怎么做到只执行200次但插入1万条记录的呢?我们来看看oracle中使用存储过程的批量插入。

四、存储过程

准备数据:
首先将t表清空 truncate table t;
用java往t表灌10万数据 exec("oracle", 100000, 1000);
创建t1表 create table t1 as select * from t where 1 = 0;

以下两个procudure的目的相同,都是将t表的数据灌到t1表中。nobatch是单次执行,usebatch是批量执行。

create or replace procedure nobatch is
begin
  for x in (select * from t)
  loop
    insert into t1 (id, name1, name2, name3, name4)
    values (x.id, x.name1, x.name2, x.name3, x.name4);
  end loop;
  commit;
end nobatch;
/
create or replace procedure usebatch (p_array_size in pls_integer)
is
  type array is table of t%rowtype;
  l_data array;
  cursor c is select * from t;
begin
  open c;
  loop
    fetch c bulk collect into l_data limit p_array_size;
    forall i in 1..l_data.count insert into t1 values l_data(i);
    exit when c%notfound;
  end loop;
  commit;
  close c;
end usebatch;
/

执行上述存储过程

SQL> exec nobatch;  
Elapsed: 00:00:32.92

SQL> exec usebatch(50);
Elapsed: 00:00:00.77

SQL> exec usebatch(100);
Elapsed: 00:00:00.47

SQL> exec usebatch(1000);
Elapsed: 00:00:00.19

SQL> exec usebatch(100000);
Elapsed: 00:00:00.26

存储过程批量执行效率也远远高于单条执行。查看usebatch(50)执行时的审计日志,sql_bind也只记录了批量执行的最后一个参数,即50的倍数。跟前面jdbc使用executeBatch批量执行时的记录内容一样。由此可知jdbc的executeBatch跟存储过程的批量执行应该是采用的同样的方法。
微信截图_20230310160438.png

存储过程的这个关键点就是forall。查阅相关文档。
The FORALL statement runs one DML statement multiple times, with different values in the VALUES and WHERE clauses.
The different values come from existing, populated collections or host arrays. The FORALL statement is usually much faster than an equivalent FOR LOOP statement.
The FORALL syntax allows us to bind the contents of a collection to a single DML statement, allowing the DML to be run for each row in the collection without requiring a context switch each time.
翻译过来就是forall很快,原因就是不需要每次执行的时候等待参数。

五、总结

  1. mysql的批量执行就是改写sql。
  2. oracle的批量执行就是用的forall。
  3. 选择一个合适批量值。

参考:
https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/FORALL-statement.html#GUID-C45B8241-F9DF-4C93-8577-C840A25963DB
https://oracle-base.com/articles/9i/bulk-binds-and-record-processing-9i
https://www.akadia.com/services/ora_bulk_insert.html

众所周知,在做消息认证或者签名时,仅使用hash函数安全性是不高的,容易遭受字典和暴力破解(
https://www.cmd5.com/
)。所以通常会使用带密钥或加盐的哈希算法作为消息认证或者口令存储,正如标题所说,我们在检索互联网上关于加盐的实现时,内容往往都是在明文
后面
加上随机值:

image-20230307141942058

那做消息认证的密钥或者盐可不可以加在明文前面呢?

这就引出本文的攻击方式。

MD5 算法计算逻辑

为了清楚这个攻击方式原理,需要先了解下md5的计算逻辑。

md5算法本质上是一种压缩算法,将长度小于
2^64
bit的任意字符压缩成128bit固定长度字符。同AES之类的分组加密算法一样,md5也需要进行分组计算。

图来自先知社区

如图所示,md5的计算需要经过两个步骤:

  1. 分组&填充
  2. 具体计算

1.分组&填充

首先会对明文按照512bit的长度进行分组,最后一个分组可能会发生长度不足512bit,或者刚刚512bit。

无论最后一个分组的长度是否刚好等于512bit,按照填充规则都需要进行填充,具体细节:

  • 假设明文刚好能被512整除,需要新增一个分组,在末尾
    8*8=64bit
    按照小端存储放入原始明文的长度,分组中间剩余的bit 按照
    10000000
    的方式进行填充,形成一个总长512bit的新分组。
  • 假设整除512bit余数大于0
    • 且余数大于
      512-8*8 =448 bit
      则需要继续填充
      10000
      (0x80000.....)至下一个分组的448bit,剩余的bit按照小端存储填充原始明文长度
    • 余数小于448bit,末尾填充原始明文长度,中间剩余部分填充填充
      10000
      (0x80000.....)

以上两个步骤用代码表示即为:

        m_l = len(message)  # 原始消息长度
        length = struct.pack('<Q', (m_l) * 8)  # 长度转化为小端 unsigned long long 8B
        blank_padding = b""
        message += b'\x80'  # 10000000
        # 此分组不足以填充长度时
        if 56 < len(message) < 64:
            blank_padding += b'\x00' * (56 + 64 - len(message))  # 填充至下一个分组
        # 分组能填充长度
        else:
            blank_padding = b'\x00' * (56 - len(message) % 64)  # 本分组填充
        if len(blank_padding) > 0:
            # 填充10000
            message += blank_padding
        # 填充长度
        message += length

2. 具体计算

其实具体的计算过程,我们不用关注,把这个过程当做一个黑盒(关注细节的可以关注文末github地址)就可以:

image-20230309666666335844

在上一步分组的基础上,第一组的分组的明文会和128bit的初始序列(幻数)做为输入进行压缩计算,初始序列是一组固定的值:

image-20230309174432910

计算后会产生新的序列,做为下一组的“初始序列”和下一组的明文再次进行压缩计算,接下来的分组重复这种“上一组的输出作为下一组的输入”,最后一组的128bit输出即为最终的md5值。

哈希长度拓展攻击

了解了md5的计算逻辑,再回到这张图,上一次的的输出作为下一次的输入这种方式可能会导致一个问题。假设存在明文分组
abc
,明文分组产生md5的过程可以简化为:

  • 分组a:h.a = md5(iv,a), md5计算需要两个参数,iv 为初始序列,h.a 为压缩计算结果
  • 分组b:h.ab = md5(h.a,b)
  • 分组c或者最终md5: h.abc = md5(h.ab,c)

图来自先知社区

在这个过程当中,如果密钥被放在a分组当中,bc为原消息认证的明文,那攻击者可以在不知道密钥的情况下,扩展明文长度,如增加明文d,计算abcd的hash,只需要知道基于abc的hash值,即可生成新的hash。

即:h.abcd = md5(h.abc,d)

这个过程即为hash长度扩展攻击。

接下来我们根据实际的例子来实操下

实操

假设存在一个商城订单支付场景,订单的确认是通过前端参数给出,存在一个逻辑漏洞可以通过前端参数来控制商品价格,从而实现“零元购”或者越权购买。

image-20230307165648656

正常情况下进行购买,因为默认此用户只有300积分,所以会购买失败:

image-20230307171050046

但如果进行参数价格
good_price
修改,会因为签名校验不通过:

image-20230307171601458

所以需要进行签名破解,先看下后端验证的逻辑:

<?php
	$total_score = 300;
	$flag = 'xxxxxxxxxxxx';
	$secret_key = "??????????????????????????????????????";  // 前端未知
	$post_data =urldecode(file_get_contents("php://input"));
	$user_sign = $_GET['signature'];
	$sign = md5($secret_key.$post_data);
	if ($user_sign === $sign) {
		$price = $_POST['good_price'];
		if ($price > $total_score){
			echo '对不起,您的积分余额不足,交易失败!';
		}else{
			echo "恭喜,购买成功!$flag";
		}
	}else{
		echo '签名数据被篡改!';
	}

?>

后端存在一个签名逻辑,会验证用户的post参数加上密钥的md5值,如果用户修改了post参数,但因为不知道密钥也就没发生成合法的md5,所以验证会不通过。

如果没有了解过哈希长度扩展攻击,这个代码是没啥问题的,所以知识面决定攻击面。

而且这个地方密钥被放在了明文前面拼接,针对哈希长度扩展攻击,利用起来还挺简单的,可以使用现成的工具,比如
hashpump
,按照提示输入内容即可:

image-20230310154439762

最后的明文中十六进制部分需要url编码,但因为hashpump需要编译,在win平台编译比较麻烦,所以我自己实现了一个md5版本的利用工具(
https://github.com/shellfeel/hash-ext-attack)

image-20230310161015225

最后把得到的结果粘贴到burp,成功购买。

image-20230310164429827

总结

文章分析了下md5的计算逻辑,以及哈希长度扩展的攻击原理,对于此类攻击的修复,其实很简单只需要把密钥由加在明文前面改为明文后面,或者使用标准的hmac算法,hmac算法里面会用密钥和明文做移位异或操作,从而增强hash的安全性,本文是以md5为例,其实对于有着类似M-D结果的hash算法都是可以这样利用的,比如sha-0,sha-1,sha-2 等。

image-20230301201336027

参考

公众号

欢迎大家关注我的公众号,这里有干货满满的硬核安全知识,和我一起学起来吧!

欢迎关注