吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 6009|回复: 34
收起左侧

[.NET逆向] VisualNDepend逆向工程

  [复制链接]
0x指纹 发表于 2024-8-29 17:04

背景介绍

搜.Net Assembly Diff工具时候,在StackOverflow一个回答中了解到NDepend工具,可以试用14天,试了下diff效果还不错,别的功能也很丰富,下载到的版本是2024.1.1.9735。

.NET Assembly Diff / Compare Tool - What's available? [closed]
https://stackoverflow.com/questions/1280252/net-assembly-diff-compare-tool-whats-available

激活流程

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

1

1

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

展开分析

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

2

2

授权文件

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

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

3

3

4

4

内容解密

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

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

5

5

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

6

6

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

7

7

签名验证

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

8

8

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

9

9

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

10

10

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

11

11

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

12

12

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

13

13

信息校验

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

14

14

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>

可以看到程序对几个日期有一些比较,我们在构造授权文件时候,几个日期的大小可以按照服务器真正返回的内容来。

15

15

生成授权

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

整个过程并不复杂,简单提一下使用python实现的点。
1.C#实现AES加解密,是传入password和salt生成Rfc2898DeriveBytes实例再生成的key,加解密时的salt是不同的,可以调试获取。

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处理。

#解压
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简单实现如下

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[0x10:0x20]
    ciphertext = text[0x20:]
    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%*£$[8f3Kv'{^ç\\".encode("utf-8"),text[:0x10],1000,0x10)
    iv = text[0x10:0x20]
    ciphertext = text[0x20:]
    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[0x10:0x20]
    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。

16

16

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

17

17

18

18

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

无限试用

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

免费评分

参与人数 13威望 +2 吾爱币 +116 热心值 +13 收起 理由
yp17792351859 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
笙若 + 1 + 1 谢谢@Thanks!
jgs + 1 + 1 谢谢@Thanks!
gouzi123 + 1 + 1 谢谢@Thanks!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
offerking + 1 + 1 热心回复!
zhoumeto + 1 + 1 谢谢@Thanks!
smile1110 + 3 + 1 用心讨论,共获提升!
MFC + 1 + 1 谢谢@Thanks!
huanghui9969 + 1 + 1 谢谢@Thanks!
爱飞的猫 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
小朋友呢 + 2 + 1 用心讨论,共获提升!
lingyun011 + 1 + 1 热心回复!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

daohaodejsw 发表于 2024-9-11 07:34
谢谢,代表我也谢谢你!

[Python] 纯文本查看 复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
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@#&#231;:!Ah*~".encode("utf-8")
REQUEST_DERIVE_KEY = "j%*&#163;$[8f3Kv'{^&#231;\\".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[16:32]
    ciphertext = text[32:]
    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[16:32]
    ciphertext = text[32:]
    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[16:32]
    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[16:32]
    ciphertext = all_data[32:]
 
    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)
co2qy 发表于 2024-8-29 20:35
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
学习了,感谢分享
5151diy 发表于 2024-8-30 13:33
谢谢楼主讲解的详细,可以对照软件实践操作
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-4-1 10:43

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表