收获

  • 对比两个程序中函数的相似性时,BinDiff 的使用方法

  • 遇到未给定或难以确定的数据时,可以通过确定数据的范围,直接进行爆破

  • 学到了一种新的转换小端序的方法


(2023年4月16日)【GDOUCTF 2023】L!S!


思路

解压得到两个文件:

GDOUCTF2023-L!S!1.png

文件名是个提示,ls-original 是原始的文件,ls-patched 是修改后的文件

分别在 64 位 IDA 中打开这两个文件,发现他们的主函数内容几乎是一摸一样的
(这里不贴图了,主函数特别长,有 1100 多行代码)

结合文件的名字,这两个文件可能只有一点细微的差异,其他内容都是一样的

识别二进制文件中的差异,可以使用 IDA 的 BinDiff 插件

首先下载 BinDiff,详情见本站的《二进制文件相似性》一文 (文章链接不可用,请直接搜索文章名)

识别结果如下,按相似度低到高排序:

GDOUCTF2023-L!S!2.png

发现只有 extract_dirs_from files 这个函数的相似度是 0.84,其他的都是 1
所以突破口肯定就在函数 extract_dirs_from files 中了

跟进一下
ls-original 中的函数:

GDOUCTF2023-L!S!3.png

ls-patched 中的函数:

GDOUCTF2023-L!S!4.png

两边同时开对比看一下:

GDOUCTF2023-L!S!5.png

左边多定义了三个变量,继续往下翻,发现差别主要就是在 lmao[] 的地方:

GDOUCTF2023-L!S!6.png

黄色框中是相同的地方,主要是多出了红色框中的内容

将不同的部分提取出来:

LABEL_7:
        if ( v9 && !v9[1] )
        {
          *&lmao[8] = 0x3F7D132A2A252822LL;
          *lmao = 0x7D2E370A180F1604LL;
          *&lmao[24] = 0x31207C7C381320LL;
          *&lmao[16] = 0x392A7F3F39132D13LL;
          v18 = lmao;
          do
            *v18++ ^= **v7;
          while ( &lmao[31] != v18 );
          puts(lmao);
        }
        goto LABEL_9;
      }

这里 v18 是一个指向 lmao 首地址的指针,而 lmao 的值是由 4 组 8 字节的数据拼接而成(小端序存放
(注意:拼接得到 lmao 的值时,要先对每一组小端序数据进行还原)

while ( &lmao[31] != v18 ) 控制 do while 循环一直将 lmao 中的所有元素全部与 *v7 的值进行异或 ,然后将异或结果输出

但是 *v7 的值在程序中无法得知,所以只能对 *v7 进行爆破
*v7 的取值只有 256 种可能,从 0 ~ 255


脚本

解法一

def little_endian(num, width_num):  # 将小端序转换为正序  
    global buffer  
    hex_str = hex(num)  # 将int数据转换为十六进制的字符串  
    while len(hex_str) != width_num + 2:  
        hex_str = "0x" + "0" * (width_num - len(hex_str[2:])) + hex_str[2:]  # 位数不足width的用0凑齐  
    index = width_num  
    while index >= 2:  
        tmp = int((hex_str[index: index + 2]), 16)  # 每两位string转换为十六进制int型数据  
        buffer.append(tmp)  # 将int型作为char存入buffer  
        index -= 2  
    return buffer  
  
  
buffer = []  # 存放结果的列表  
  
lmao1 = 0x7D2E370A180F1604  
lmao2 = 0x3F7D132A2A252822  
lmao3 = 0x392A7F3F39132D13  
lmao4 = 0x31207C7C381320  
  
little_endian(lmao1, 16)  
little_endian(lmao2, 16)  
little_endian(lmao3, 16)  
little_endian(lmao4, 14)  
  
print(buffer)  
# [4, 22, 15, 24, 10, 55, 46, 125, 34, 40, 37, 42, 42, 19, 125, 63, 19, 45, 19, 57, 63, 127, 42, 57, 32, 19, 56, 124, 124, 32, 49]  
  
for key in range(256):  # 直接爆破key  
    flag = ""  
    print("key = ", key)  
    for k in range(len(buffer)):  
        tmp = buffer[k] ^ key  # 逐个与key异或  
        flag += chr(tmp)  # 对应的字符存入flag  
    print(flag)  
    if 'HZCTF' in flag or 'NSSCTF' in flag:  # 只输出包含'HZCTF'或'NSSCTF'的结果  
        break

解法二

发现官方 Writeup 有一种更方便地将一组小端序数据合并成一个正序数据的方法,记录一下
但也有一个缺点,无法自己随意控制数据的长度,只能统一为相同长度

import struct  
  
stack_bytes = [  
    0x7d2e370a180f1604,  
    0x3f7d132a2a252822,  
    0x392a7f3f39132d13,  
    0x31207c7c381320  
]  
  
# 将stack_bytes中的数据按照小端字节序打包为二进制数据  
xored_bytes = struct.pack("<4Q", *stack_bytes)  
# 其中,< 表示小端序,Q代表一个无符号长整型  
# 每个无符号长整型整数占8个字节,所以总共打包出来的字符串长度为32个字节  
# b'\x04\x16\x0f\x18\n7.}"(%**\x13}?\x13-\x139?\x7f*9 \x138|| 1\x00'  
  
for xorkey in range(256):  
    output = bytes(byte ^ xorkey for byte in xored_bytes)  
    if b"HZCTF{" in output:  
        print(output)  
  
# b'HZCTF{b1ndiff_1s_a_us3ful_t00l}L'  
# (因为在lmao4的高位补了一个0,所以多输出了一个L)

结果

NSSCTF{b1ndiff_1s_a_us3ful_t00l}
(最终要求将 HZCTF 改为 NSSCTF)

GDOUCTF2023-L!S!7.png