0x指纹 发表于 2024-8-29 17:04

VisualNDepend逆向工程

# 背景介绍
搜.Net Assembly Diff工具时候,在StackOverflow一个回答中了解到NDepend工具,可以试用14天,试了下diff效果还不错,别的功能也很丰富,下载到的版本是2024.1.1.9735。
```
.NET Assembly Diff / Compare Tool - What's available?
https://stackoverflow.com/questions/1280252/net-assembly-diff-compare-tool-whats-available
```

# 激活流程
将程序断网后点击Start Evaluation会进入NDepend manual server access流程,自动生成了Date Request text,到网站可以获取到Data Response text,输入后即可激活。


程序联网的话,应该是后台直接发起请求验证结果完成激活,对程序断网可以方便分析,并且程序会随时联网发送消息,比如patch造成或修改授权造成的运行异常报告,还是给断了好。

# 展开分析
14天后,打开dnspy开始看下,大部分函数名、变量名、字符串都被混淆了,程序流程还是好的。注意有弹窗,试着断在MessageBox.Show,运行断下来了,看下调用堆栈回溯,可以以此展开调试分析。


# 授权文件
经过一番杂乱但并不复杂的调试分析,知道程序解密验证服务器返回的数据后,将进行加密保存为C:\ProgramData\NDepend\ActivationEval文件,程序每次启动都会解析此文件进行验证。

断在NDpend.Core.dll的oJX.xJg函数中,调试可知前半部分是读ActivationEval文件内容,在kSx.cSZ函数中解密文件。



# 内容解密
跟进kSx.cSZ函数,看到函数调用链中出现RijndaelManaged、Rfc2898DeriveBytes、CpherMode.CBC、PaddingMode.PKCS7等字样,可知主体上是PKCS7填充AES-CBC解密的C#调用。

先进入CSq函数,m4f是key和iv的长度,程序会用256和128都试一下,前者会解密失败,再用128进行解密。



进入jSp,先对文件内容进行base64解密,随后分为[:0x10]、和三部分。第一部分用来作为password,和salt字符串传入Rfc2898DeriveBytes初始化,随后生成AES解密的key,第二部分是AES解密的iv,第三部分是密文。


AES解密完后会进入YSA函数,可以看到又进行了一遍base解密,随后解压操作,即可得到授权数据的明文。


# 签名验证
可以看到末尾段是有一段签名信息的,根据判断会使用rsa-sha1验签。


根据调用堆栈,回溯到HIc函数,再进入X2t.k2x函数,根据局部变量内容,我们可以看到从HadrwareID提取出了要验签的内容,加载了RSAKeyValue并进行了哈希验证,最后进入l4h.M4D函数开始验签。


调试进入c4S函数中,根据局部变量的信息,可以判断Y4y是rsaCryptoPublic.VerifyData(hashToSignBytes,signature)函数。


RsaCryptoPublic实例的初始化是c4X函数。


调试看到从RSAKeyValue中初始化了Modulus和Exponent,是模数和公钥指数。


模数是两个大素数的乘积,到factordb.com上分解下,未果,那只能patch过掉了。


# 信息校验
查看调用堆栈回溯到G1Z4函数,进入EIx函数,便是信息校验部分。分为两部分,一部分是本机HardwareID信息是否和解密的授权数据匹配,另一部分是检查授权相关日期相关信息。


HardwareID部分信息我们可以解密软件生成的data request text数据获取,授权信息日期中日期相关部分内容如下,可以猜测信息有注册时间、到期时间、多少天后弹出激活、多少天后弹出请求更多试用、过期后多少内还能再请求更多试用。
```
<DateRegister>23 Aug 2024</DateRegister>
<DateExpire>07 Sep 2024</DateExpire>
<MoreEvalAlreadyAsked>False</MoreEvalAlreadyAsked>
<EvalNbDaysLeftToShowActivationForm>6</EvalNbDaysLeftToShowActivationForm>
<EvalNbDaysLeftToShowAskForMoreEvalButton>4</EvalNbDaysLeftToShowAskForMoreEvalButton>
<CanReEvalNbDaysAfterEvalExpiration>240</CanReEvalNbDaysAfterEvalExpiration>
<EvalRegisteredWithProductVersion>2024.1.1.9735</EvalRegisteredWithProductVersion>
```
可以看到程序对几个日期有一些比较,我们在构造授权文件时候,几个日期的大小可以按照服务器真正返回的内容来。


# 生成授权
思路是先解密软件手动激活时候弹窗里面的data request text,得到HardwareID部分信息,构造出授权信息明文,修改过期时间,再加密生成ActivationEval文件放在C:\ProgramData\NDepend\目录下即可。

整个过程并不复杂,简单提一下使用python实现的点。
1.C#实现AES加解密,是传入password和salt生成Rfc2898DeriveBytes实例再生成的key,加解密时的salt是不同的,可以调试获取。
```pyhthon
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend

def derive_key(password, salt, iterations, key_length):
    kdf = PBKDF2HMAC(
      algorithm=hashes.SHA1(),
      length=key_length,
      salt=salt,
      iterations=iterations,
      backend=default_backend()
    )
    return kdf.derive(password)
```

2.C#的解压缩数据格式是Raw Deflate,不能直接用python的zlib处理。
```python
#解压
plaintext = zlib.decompress(compress_data, -zlib.MAX_WBITS)

#压缩
compressor = zlib.compressobj(wbits=-zlib.MAX_WBITS)
compress_data = compressor.compress(plaintext.encode("utf-8"))+compressor.flush()       
```

授权生成的pthon简单实现如下
```python
import random
from Crypto.Cipher import AES
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
import base64
import zlib

def pkcs7padding(text):
    bs = 16
    length = len(text)
    bytes_length = len(text)
    padding_size = length if (bytes_length == length) else bytes_length
    padding = bs - padding_size % bs
    padding_text = padding.to_bytes(1,'little') * padding
    return text + padding_text


def data_response_decrypt(key,iv,ciphertext):
    base64text = AES.new(key, AES.MODE_CBC,iv).decrypt(ciphertext)
    compress_data = base64.b64decode(base64text)
    plaintext = zlib.decompress(compress_data, -zlib.MAX_WBITS)
    return plaintext

def data_response_encrypt(key,iv,plaintext):
    compressor = zlib.compressobj(wbits=-zlib.MAX_WBITS)
    compress_data = compressor.compress(plaintext.encode("utf-8"))+compressor.flush()
    base64_cipher = base64.b64encode(compress_data)
    return AES.new(key, AES.MODE_CBC,iv).encrypt(pkcs7padding(base64_cipher))

def derive_key(password, salt, iterations, key_length):
    kdf = PBKDF2HMAC(
      algorithm=hashes.SHA1(),
      length=key_length,
      salt=salt,
      iterations=iterations,
      backend=default_backend()
    )
    return kdf.derive(password)
def decrypt_server_data(cipher):
    text = base64.b64decode(cipher)
    key = derive_key("N|[%^^m@#ç:!Ah*~".encode("utf-8"),text[:0x10],1000,0x10)
    iv = text
    ciphertext = text
    plaintext = data_response_decrypt(key, iv, ciphertext)
    return plaintext.decode("utf-8")
def decrypt_data_request(cipher):
    text = base64.b64decode(cipher)
    key = derive_key("j%*£$,1000,0x10)
    iv = text
    ciphertext = text
    plaintext = data_response_decrypt(key, iv, ciphertext)
    return plaintext.decode("utf-8")

def generateActivationEval():
    random_bytes = bytes(random.getrandbits(8) for _ in range(32))
    key = derive_key("N|[%^^m@#ç:!Ah*~".encode("utf-8"),random_bytes[:0x10],1000,0x10)
    iv = random_bytes
    with open("licenseData.txt","r") as f:
      plaintext = f.read()
    all = random_bytes + data_response_encrypt(key, iv, plaintext)
    with open("ActivationEval","wb") as f:
      f.write(base64.b64encode(all))

# data_request = decrypt_data_request("xxxxxxxx")
# print(data_request)

# data_response = decrypt_server_data("xxxxxxxx")
# print(data_response)

generateActivationEval()
```

# 篡改检测
除了生成授权文件,前面提到在factordb.com上分解rsa验签的模数,没成功,没有私钥无法签名,只能patch下过掉验签。可以在c4S函数中rsaCryptoPublic.VerifyData(hashToSignBytes,signature)函数调用那里patch直接返回true。


随后可以进入软件界面,但是发现diff功能无法正常使用,经过一番杂乱的调试分析,先后找到多个暗桩,其中yTf和P39两个是检测我们过rsa验签patch的NDepend.Core.dll,继续patch直接过掉。



随后发现diff功能还是不能正常使用,有暗桩没找到,真是明枪易躲暗箭难防。

# 无限试用
不想花过多时间耗在庞杂的软件功能中调试以求寻出所有暗桩,就试着换下思路。经尝试,删除C:\ProgramData\NDepend下的ActivationEval文件,再随意修改NDepend_2024.1.1.9735\Lib\DownloadInfo.xml中的邮箱地址,可获取新的14天试用,猜测和服务端没有仔细校验软件生成的data request信息有关。

daohaodejsw 发表于 2024-9-11 07:34

谢谢,代表我也谢谢你!



import base64
import random
import string
import xml.etree.ElementTree as ET
import zlib
from Crypto.Cipher import AES
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

# Constants
SERVER_DERIVE_KEY = "N|[%^^m@#ç:!Ah*~".encode("utf-8")
REQUEST_DERIVE_KEY = "j%*£$[8f3Kv'{^ç\\".encode("utf-8")
ITERATIONS = 1000
KEY_LENGTH = 16
BS = 16
XML_FILE_PATH = r"C:\Users\user\Downloads\NDepend_2024.1.1.9735\Lib\DownloadInfo.xml"
ACTIVATION_EVAL_PATH = r"C:\ProgramData\NDepend\ActivationEval"

def pkcs7_padding(text):
    length = len(text)
    bytes_length = len(text)
    padding_size = length if (bytes_length == length) else bytes_length
    padding = BS - padding_size % BS
    padding_text = padding.to_bytes(1, 'little') * padding
    return text + padding_text

def data_response_decrypt(key, iv, ciphertext):
    base64_text = AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext)
    compressed_data = base64.b64decode(base64_text)
    plaintext = zlib.decompress(compressed_data, -zlib.MAX_WBITS)
    return plaintext

def data_response_encrypt(key, iv, plaintext):
    compressor = zlib.compressobj(wbits=-zlib.MAX_WBITS)
    compressed_data = compressor.compress(plaintext.encode("utf-8")) + compressor.flush()
    base64_cipher = base64.b64encode(compressed_data)
    return AES.new(key, AES.MODE_CBC, iv).encrypt(pkcs7_padding(base64_cipher))

def derive_key(password, salt, iterations, key_length):
    kdf = PBKDF2HMAC(
      algorithm=hashes.SHA1(),
      length=key_length,
      salt=salt,
      iterations=iterations,
      backend=default_backend()
    )
    return kdf.derive(password)

def decrypt_server_data(cipher):
    text = base64.b64decode(cipher)
    key = derive_key(SERVER_DERIVE_KEY, text[:16], ITERATIONS, KEY_LENGTH)
    iv = text
    ciphertext = text
    plaintext = data_response_decrypt(key, iv, ciphertext)
    return plaintext.decode("utf-8")

def decrypt_data_request(cipher):
    text = base64.b64decode(cipher)
    key = derive_key(REQUEST_DERIVE_KEY, text[:16], ITERATIONS, KEY_LENGTH)
    iv = text
    ciphertext = text
    plaintext = data_response_decrypt(key, iv, ciphertext)
    return plaintext.decode("utf-8")

def generate_activation_eval():
    random_bytes = bytes(random.getrandbits(8) for _ in range(32))
    key = derive_key(SERVER_DERIVE_KEY, random_bytes[:16], ITERATIONS, KEY_LENGTH)
    iv = random_bytes
    with open(XML_FILE_PATH, "r") as f:
      plaintext = f.read()
    all_data = random_bytes + data_response_encrypt(key, iv, plaintext)
    with open(ACTIVATION_EVAL_PATH, "wb") as f:
      f.write(base64.b64encode(all_data))

def decrypt_activation_eval(activation_eval_path):
    with open(activation_eval_path, "rb") as f:
      all_data = base64.b64decode(f.read())

    random_bytes = all_data[:32]
    key = derive_key(SERVER_DERIVE_KEY, random_bytes[:16], ITERATIONS, KEY_LENGTH)
    iv = random_bytes
    ciphertext = all_data

    plaintext = data_response_decrypt(key, iv, ciphertext)
    return plaintext.decode("utf-8")

def generate_random_string(length=16):
    letters_and_digits = string.ascii_letters + string.digits
    return ''.join(random.choice(letters_and_digits) for _ in range(length))

def randomize_email_and_tracking(xml_path):
    tree = ET.parse(xml_path)
    root = tree.getroot()

    email_element = root.find('.//EmailAddress')
    tracking_data_element = root.find('.//TrackingData')

    if email_element is not None:
      email_element.text = f"user{random.randint(10000000, 90000000)}@mail.com"

    if tracking_data_element is not None:
      tracking_data_element.text = generate_random_string(16)

    tree.write(xml_path, encoding='utf-8', xml_declaration=True)
    generate_activation_eval()

if __name__ == "__main__":
    randomize_email_and_tracking(XML_FILE_PATH)
    print(f"XML file updated: {XML_FILE_PATH}")
    plaintext = decrypt_activation_eval(ACTIVATION_EVAL_PATH)
    print(plaintext)

Edcison 发表于 2024-8-29 18:25

谢谢大佬分享

co2qy 发表于 2024-8-29 20:35

谢谢lz教学

sungod 发表于 2024-8-29 20:55

厉害了啊

Mysky6 发表于 2024-8-29 21:08


谢谢大佬分享

agion 发表于 2024-8-30 01:47

学习一下

ijack2001 发表于 2024-8-30 08:50

谢谢大佬分享

adevour 发表于 2024-8-30 09:09

确实不错

15090878185 发表于 2024-8-30 11:16

谢谢大佬分享

runfucy 发表于 2024-8-30 11:35

学习了,感谢分享
页: [1] 2 3 4
查看完整版本: VisualNDepend逆向工程