手动签署 PE 文件
Manually sign a PE file
我正在尝试手动签署现有的 Portable Executable。
我正在按照 this document 中的说明进行操作:
- 将图像 header 加载到内存中。
- 初始化哈希算法上下文。
- 按照可选 Header Windows-Specific 字段中的指定,将图像 header 从其基址散列到校验和地址开始之前。
- 跳过校验和,这是一个 4 字节的字段。
- 按照可选 Header 数据目录中的规定,对从校验和字段末尾到证书 Table 条目开始之前的所有内容进行哈希处理。
- 从证书 Table 条目中获取属性证书 Table 地址和大小。有关详细信息,请参阅 PE/COFF 规范的第 5.7 节。
- 从计算中排除证书 Table 条目,并对从证书 Table 条目末尾到图像 header 末尾的所有内容进行哈希处理,包括部分 Table (headers)。证书 Table 条目的长度为 8 个字节,如可选 Header 数据目录中所指定。
- 创建一个名为 SUM_OF_BYTES_HASHED 的计数器,它不是签名的一部分。将此计数器设置为 SizeOfHeaders 字段,如可选 Header Windows-Specific 字段中指定。
- 构建一个临时的 table 指针,指向图像中的所有部分 header。 COFF 文件 Header 的 NumberOfSections 字段指示 table 应该有多大。不要在 SizeOfRawData 字段为零的 table 中包含任何部分 headers。
- 使用引用的SectionHeader结构中的PointerToRawData字段(偏移量20)作为关键字,按升序排列table的元素。也就是说,根据sections的disk-file偏移量,对sectionheaders进行升序排序。
- 遍历已排序的 table,将相应部分加载到内存中,并对整个部分进行哈希处理。使用 SectionHeader 结构中的 SizeOfRawData 字段来确定要散列的数据量。
- 将该部分的 SizeOfRawData 值添加到 SUM_OF_BYTES_HASHED。
- 对已排序 table 中的所有部分重复步骤 11 和 12。
- 创建一个名为 FILE_SIZE 的值,它不是签名的一部分。将此值设置为从底层文件系统获取的图像文件大小。如果 FILE_SIZE 大于 SUM_OF_BYTES_HASHED,则文件包含必须添加到散列中的额外数据。此数据从 SUM_OF_BYTES_HASHED 文件偏移量开始,其长度为:
(File Size) – ((Size of AttributeCertificateTable) + SUM_OF_BYTES_HASHED) 注:Attribute Certificate Table 的大小在 Certificate Table 的第二个 ULONG 值中指定可选 Header 数据目录中的条目(32 位:偏移量 132、64 位:偏移量 148)。
- 完成哈希算法上下文。注意:此过程使用来自 PE/COFF 规范 8.1 版的偏移值。有关权威偏移值,请参阅最新版本的 PE/COFF 规范。
以下代码尝试从图像中获取 part-to-be-hashed:
// Variables
// full: vector<char> holding the image
// d: vector<char> where to store the data-to-be-hashed
// sections: vector of the sections, ensuring size > 0
// nt/pnt* : pointer inside full that points to the beginning of NT header
// Sort Sections
std::sort(sections.begin(), sections.end(), [](const section& s1, const section& s2) -> bool
{
if (s1.sec->PointerToRawData < s2.sec->PointerToRawData)
return true;
return false;
});
// Up to where?
size_t BytesUpToLastSection = ((char*)(sections[sections.size() - 1].sec) - full.data()) + sizeof(image_section_header);
d.resize(BytesUpToLastSection);
memcpy(d.data(), full.data(), BytesUpToLastSection);
// We remove the certificate table entry (8 bytes)
size_t offset = 0;
if (nt.Is32())
{
offset = offsetof(optional_header_32, DataDirectory[DIR_SECURITY]);
}
else
{
offset = offsetof(optional_header_64, DataDirectory[DIR_SECURITY]);
}
offset += sizeof(nt.FileHeader) + sizeof(nt.Signature);
offset += pnt - full.data();
d.erase(d.begin() + offset, d.begin() + offset + 8);
// We remove the checksum (4 bytes)
if (nt.Is32())
offset = offsetof(optional_header_32,CheckSum);
else
offset = offsetof(optional_header_64,CheckSum);
offset += sizeof(nt.FileHeader) + sizeof(nt.Signature);
offset += pnt - full.data();
d.erase(d.begin() + offset, d.begin() + offset + 4);
// Counter
size_t SUM_OF_BYTES_HASHED = 0;
if (nt.Is32())
SUM_OF_BYTES_HASHED = std::get<optional_header_32>(nt.OptionalHeader).SizeOfHeaders;
else
SUM_OF_BYTES_HASHED = std::get<optional_header_64>(nt.OptionalHeader).SizeOfHeaders;
for (auto& ss : sections)
{
if (ss.sectionData.sz == 0)
continue;
s = d.size();
d.resize(d.size() + ss.sectionData.sz);
memcpy(d.data() + s, ss.sectionData.p, ss.sectionData.sz);
SUM_OF_BYTES_HASHED += ss.sec->SizeOfRawData;
}
size_t FILE_SIZE = full.size();
if (FILE_SIZE > SUM_OF_BYTES_HASHED)
{
// Not entering here, test executable does not have extra data
}
一定是哪里出了问题。签署此数据,然后更新 executable 证书条目并附加 PCKS#7 签名会导致 Windows 无法识别的 executable。右击-> "Invalid Signature".
与signtool.exe
的结果比较时,签名不同。当我尝试使用 CryptVerifyDetachedMessageSignature
验证此签名时,出现错误 0x80091007,这意味着哈希值不正确。
这意味着我没有正确计算 "what to be signed" 缓冲区。我想念什么?
我什至对条目的删除进行了硬编码:
d = full;
d.erase(d.begin() + 296, d.begin() + 296 + 8);
d.erase(d.begin() + 216, d.begin() + 216 + 4);
非常感谢。
我找到了解决方案。
PE 中的签名不是典型的 PKCS#7,它还包含 document.
中描述的特定已验证和未验证属性
这些都满足了,签名就OK了。但是我不能使用 CAdES,因为 Windows 不会接受列表中的任何其他经过身份验证的属性,这是验证 CAdES 所必需的。
我正在尝试手动签署现有的 Portable Executable。
我正在按照 this document 中的说明进行操作:
- 将图像 header 加载到内存中。
- 初始化哈希算法上下文。
- 按照可选 Header Windows-Specific 字段中的指定,将图像 header 从其基址散列到校验和地址开始之前。
- 跳过校验和,这是一个 4 字节的字段。
- 按照可选 Header 数据目录中的规定,对从校验和字段末尾到证书 Table 条目开始之前的所有内容进行哈希处理。
- 从证书 Table 条目中获取属性证书 Table 地址和大小。有关详细信息,请参阅 PE/COFF 规范的第 5.7 节。
- 从计算中排除证书 Table 条目,并对从证书 Table 条目末尾到图像 header 末尾的所有内容进行哈希处理,包括部分 Table (headers)。证书 Table 条目的长度为 8 个字节,如可选 Header 数据目录中所指定。
- 创建一个名为 SUM_OF_BYTES_HASHED 的计数器,它不是签名的一部分。将此计数器设置为 SizeOfHeaders 字段,如可选 Header Windows-Specific 字段中指定。
- 构建一个临时的 table 指针,指向图像中的所有部分 header。 COFF 文件 Header 的 NumberOfSections 字段指示 table 应该有多大。不要在 SizeOfRawData 字段为零的 table 中包含任何部分 headers。
- 使用引用的SectionHeader结构中的PointerToRawData字段(偏移量20)作为关键字,按升序排列table的元素。也就是说,根据sections的disk-file偏移量,对sectionheaders进行升序排序。
- 遍历已排序的 table,将相应部分加载到内存中,并对整个部分进行哈希处理。使用 SectionHeader 结构中的 SizeOfRawData 字段来确定要散列的数据量。
- 将该部分的 SizeOfRawData 值添加到 SUM_OF_BYTES_HASHED。
- 对已排序 table 中的所有部分重复步骤 11 和 12。
- 创建一个名为 FILE_SIZE 的值,它不是签名的一部分。将此值设置为从底层文件系统获取的图像文件大小。如果 FILE_SIZE 大于 SUM_OF_BYTES_HASHED,则文件包含必须添加到散列中的额外数据。此数据从 SUM_OF_BYTES_HASHED 文件偏移量开始,其长度为: (File Size) – ((Size of AttributeCertificateTable) + SUM_OF_BYTES_HASHED) 注:Attribute Certificate Table 的大小在 Certificate Table 的第二个 ULONG 值中指定可选 Header 数据目录中的条目(32 位:偏移量 132、64 位:偏移量 148)。
- 完成哈希算法上下文。注意:此过程使用来自 PE/COFF 规范 8.1 版的偏移值。有关权威偏移值,请参阅最新版本的 PE/COFF 规范。
以下代码尝试从图像中获取 part-to-be-hashed:
// Variables
// full: vector<char> holding the image
// d: vector<char> where to store the data-to-be-hashed
// sections: vector of the sections, ensuring size > 0
// nt/pnt* : pointer inside full that points to the beginning of NT header
// Sort Sections
std::sort(sections.begin(), sections.end(), [](const section& s1, const section& s2) -> bool
{
if (s1.sec->PointerToRawData < s2.sec->PointerToRawData)
return true;
return false;
});
// Up to where?
size_t BytesUpToLastSection = ((char*)(sections[sections.size() - 1].sec) - full.data()) + sizeof(image_section_header);
d.resize(BytesUpToLastSection);
memcpy(d.data(), full.data(), BytesUpToLastSection);
// We remove the certificate table entry (8 bytes)
size_t offset = 0;
if (nt.Is32())
{
offset = offsetof(optional_header_32, DataDirectory[DIR_SECURITY]);
}
else
{
offset = offsetof(optional_header_64, DataDirectory[DIR_SECURITY]);
}
offset += sizeof(nt.FileHeader) + sizeof(nt.Signature);
offset += pnt - full.data();
d.erase(d.begin() + offset, d.begin() + offset + 8);
// We remove the checksum (4 bytes)
if (nt.Is32())
offset = offsetof(optional_header_32,CheckSum);
else
offset = offsetof(optional_header_64,CheckSum);
offset += sizeof(nt.FileHeader) + sizeof(nt.Signature);
offset += pnt - full.data();
d.erase(d.begin() + offset, d.begin() + offset + 4);
// Counter
size_t SUM_OF_BYTES_HASHED = 0;
if (nt.Is32())
SUM_OF_BYTES_HASHED = std::get<optional_header_32>(nt.OptionalHeader).SizeOfHeaders;
else
SUM_OF_BYTES_HASHED = std::get<optional_header_64>(nt.OptionalHeader).SizeOfHeaders;
for (auto& ss : sections)
{
if (ss.sectionData.sz == 0)
continue;
s = d.size();
d.resize(d.size() + ss.sectionData.sz);
memcpy(d.data() + s, ss.sectionData.p, ss.sectionData.sz);
SUM_OF_BYTES_HASHED += ss.sec->SizeOfRawData;
}
size_t FILE_SIZE = full.size();
if (FILE_SIZE > SUM_OF_BYTES_HASHED)
{
// Not entering here, test executable does not have extra data
}
一定是哪里出了问题。签署此数据,然后更新 executable 证书条目并附加 PCKS#7 签名会导致 Windows 无法识别的 executable。右击-> "Invalid Signature".
与signtool.exe
的结果比较时,签名不同。当我尝试使用 CryptVerifyDetachedMessageSignature
验证此签名时,出现错误 0x80091007,这意味着哈希值不正确。
这意味着我没有正确计算 "what to be signed" 缓冲区。我想念什么?
我什至对条目的删除进行了硬编码:
d = full;
d.erase(d.begin() + 296, d.begin() + 296 + 8);
d.erase(d.begin() + 216, d.begin() + 216 + 4);
非常感谢。
我找到了解决方案。
PE 中的签名不是典型的 PKCS#7,它还包含 document.
中描述的特定已验证和未验证属性这些都满足了,签名就OK了。但是我不能使用 CAdES,因为 Windows 不会接受列表中的任何其他经过身份验证的属性,这是验证 CAdES 所必需的。