MS-CHAPv2和MPPE——身份认证、密钥协商和数据加密

内容纲要

一、WinServer2003服务器启用VPN服务

1、在管理工具中打开路由和远程访问

截图

2、右键配置并启用路由和远程访问

截图

3、在弹出的安装向导中点击下一步开始配置

截图

4、选择自定义配置

截图

5、勾选VPN访问

截图

6、点击完成

截图

7、确定开始服务

截图

8、等待初始化结束

截图

9、在计算机管理的本地用户和组中右键添加本地用户以用于VPN链接

截图

10、设置用户名密码

截图

11、右键新添加的用户,打开属性

截图

12、在拨入选项卡下允许远程访问权限

截图

二、WinXP客户端链接VPN

1、在网络连接面板左侧点击创建一个新的连接启动新建连接向导

截图

2、选择连接到我的工作场所的网络

截图

3、随便设置一个名字

截图

4、填入步骤一中设置的服务器的ip地址

截图

5、在弹出的连接对话框中点击属性按钮,在打开的属性面板里选择网络选项卡,更改VPN类型为PPTP VPN

截图

6、设置用户名密码开始连接的同时启动wireshark抓包

截图

7、捕获到完整流量数据包

截图

三、计算响应值

1、提取16字节挑战值

截图

在wireshark中可以十分方便地从挑战包中获得16字节的挑战值

2、提取24字节响应值

截图

在wireshark中发现响应报文的有效载荷是49字节的,而非24字节,查看rfc文档,可以发现这49字节是16字节对等挑战值+8字节空字符+24字节响应值+1字节保留Flag构成

截图

3、程序模拟响应值计算

import hashlib
from Crypto.Cipher import DES

def NtPasswordHash(password):
    return hashlib.new("md4",password.encode("utf-16le")).digest()

def padding(key:bytes):
    key=key.rjust(8,b"\x00")
    for i in reversed(range(1,9)):
        key=int.to_bytes(int.from_bytes(key[:i],"big")<<1,i,"big")+key[i:]
    return key

def ChallengeHash(PeerChallenge,Challenge,UserName):
    return hashlib.sha1(PeerChallenge+Challenge+username.encode()).digest()[:8]

def DesEncrypt(Clear,key:bytes):
    return DES.new(padding(key),DES.MODE_ECB).encrypt(Clear)

def ChallengeResponse(Challenge,PasswordHash):
    Response=b""
    ZPasswordHash=PasswordHash.ljust(21,b"\x00")
    for i in range(3):
        Response+=DesEncrypt(Challenge,ZPasswordHash[7*i:7*(i+1)])
    return Response

if __name__=="__main__":
    ChallengeDump="b6eaed34f4f1e068cedfdb58bf7da53c"
    ResponsePayloadDump="1e69bad543d752a6797662d9f19df0e70000000000000000eb5c83aa451c26ff219f4fc0caff7ff12a54aad9867ef68100"

    AuthChallenge=bytes.fromhex(ChallengeDump)
    ResponsePayload=bytes.fromhex(ResponsePayloadDump)

    PeerChallenge=ResponsePayload[:16]
    TrueResponse=ResponsePayload[24:48]
    print(TrueResponse.hex())

    username="PPTP"
    password="123456"

    PasswordHash=NtPasswordHash(password)
    Challenge=ChallengeHash(PeerChallenge,AuthChallenge,username)
    Response=ChallengeResponse(Challenge,PasswordHash)
    print(Response.hex())

四、MPPE解密算法

1、MPPE协议

1.1 MPPE选项配置报文

MPPE选项配置报文位于PPP CCP报文Option字段中,总长48位(6字节),前8位表示协议类型,MPPE的值为18,中间8位定义了配置报文长度,MPPE的值为6字节,最后32位是支持的参数列表,其中有含义的6位用字母HMSLDC表示。

H表示stateless,为真则每个报文换一次密钥,否则256个报文换一次

M表示中等强度加密,为真则用56位密钥加密

S表示高强度加密,为真则使用128位密钥加密

L表示低强度加密,为真则使用4位密钥加密

C表示MPPC压缩,为真则启用MPPC压缩

D总是为0

截图

1.2 MPPE密文

MPPE密文由PPP Compressed datagram 报文承载,其PPP报文格式如下

截图

前2字节为PPP协议类型,PPP-comp的值为0x00fd,如果PPP LCP中协商了PFC(Protocol Field Compression),则可能被压缩为一字节的0xfd。

中间4位为设置位,A为是否立即更换密钥,如果1.1节中MPPE配置报文中H位的值为真,则该位一直为1,BC是MPPC的配置位,D为真表示该数据被MPPE加密了

后12位为序号,标识这是第几个包,到0xFFF之后重新从0开始,并且即使H=0也要更新密钥

之后的载荷便是真正MPPE加密了的数据

2、MPPE密钥生成算法

  1. 计算PasswordHash的MD4哈希,取前16位为 PasswordHashHash

  2. 计算PasswordHashHash、NtResponse(24字节直接挑战响应)和Magic1的SHA1哈希,同样取前16位作为MasterKey

  3. 根据是否为服务端和发送端选择Magic2或Magic3和MasterKey、SHSPad1、SHSPad2一起进行SHA1哈希,根据密钥强度取前8位或16位得到StartKey

  4. 用StartKey+SHSPad1+StartKey+SHSPad2进行SHA1哈希,根据密钥强度取前8位或16位得到SessionKey

  5. 使用SessionKey初始化RC4流密码用以加密数据生成新的SessionKey

    RFC3079这里写的是用SessionKey生成RC4流密钥,后面的sample也是直接用SessionKey生成的流密钥直接加密数据,但是实际上这里的SessionKey要用密钥更新算法生成新的SessionKey,新的SessionKey生成的流密钥才能用来加密数据。简直是迷惑行为

截图

3、MPPE密钥更新算法

  1. 用StartKey+SHSPad1+SessionKey+SHSPad2进行SHA1哈希,根据密钥强度取前8位或16位得到InterimKey

    虽然RFC3078在这里说第一次调用的时候StartKey==SessionKey,但是实际上这说的是MPPE密钥生成算法中的第4步,而不是在MPPE密钥更新算法中。有让人误会密钥生成算法生成的SessionKey就是第一个包的加密密钥的嫌疑。这是在和RFC3079合伙骗人吗?

  2. 使用RC4加密InterimKey得到新的SessionKey

  3. 使用新的SessionKey生成RC4流密钥用以加密数据

截图

截图

4、MPPE密码算法实现

from ResponseGen import NtPasswordHash,ChallengeResponse
import hashlib
from Crypto.Cipher import ARC4

SHSpad1=b"\x00"*40
SHSpad2=b"\xf2"*40
Magic1=b"\x54\x68\x69\x73\x20\x69\x73\x20\x74\x68\x65\x20\x4D\x50\x50\x45\x20\x4D\x61\x73\x74\x65\x72\x20\x4B\x65\x79"
Magic2=b"\x4F\x6E\x20\x74\x68\x65\x20\x63\x6C\x69\x65\x6E\x74\x20\x73\x69\x64\x65\x2C\x20\x74\x68\x69\x73\x20\x69\x73\x20\x74\x68\x65\x20\x73\x65\x6E\x64\x20\x6B\x65\x79\x3B\x20\x6F\x6E\x20\x74\x68\x65\x20\x73\x65\x72\x76\x65\x72\x20\x73\x69\x64\x65\x2C\x20\x69\x74\x20\x69\x73\x20\x74\x68\x65\x20\x72\x65\x63\x65\x69\x76\x65\x20\x6B\x65\x79\x2E"
Magic3=b"\x4F\x6E\x20\x74\x68\x65\x20\x63\x6C\x69\x65\x6E\x74\x20\x73\x69\x64\x65\x2C\x20\x74\x68\x69\x73\x20\x69\x73\x20\x74\x68\x65\x20\x72\x65\x63\x65\x69\x76\x65\x20\x6B\x65\x79\x3B\x20\x6F\x6E\x20\x74\x68\x65\x20\x73\x65\x72\x76\x65\x72\x20\x73\x69\x64\x65\x2C\x20\x69\x74\x20\x69\x73\x20\x74\x68\x65\x20\x73\x65\x6E\x64\x20\x6B\x65\x79\x2E"

def GetMasterKey(PasswordHashHash,NtResponse):
    return hashlib.sha1(PasswordHashHash+NtResponse+Magic1).digest()[:16]

def GetAsymmetricKey(MasterKey,SessionKeyLength,IsSend,IsServer):
    s = Magic2 if IsSend ^ IsServer else Magic3
    return hashlib.sha1(MasterKey+SHSpad1+s+SHSpad2).digest()[:SessionKeyLength]

def GetNewKeyFromSHA(StartKey,SessionKey,SessionKeyLength):
    return hashlib.sha1(StartKey[:SessionKeyLength]+SHSpad1+SessionKey[:SessionKeyLength]+SHSpad2).digest()[:SessionKeyLength]

def GetSessionKey(password,NtResponse,IsSend,Type="M",Debug=False):
    SessionKeyLength=8 if Type!="S" else 16
    PasswordHashHash=hashlib.new("md4",NtPasswordHash(password)[:16]).digest()[:]
    MasterKey=GetMasterKey(PasswordHashHash,NtResponse)
    StartKey=GetAsymmetricKey(MasterKey,SessionKeyLength,IsSend,True)
    SessionKeyPad={"L":b"\xd1\x26\x9e","M":b"\xd1","":b""}
    SessionKeyIndex={"L":3,"M":1,"S":0}
    SessionKey=SessionKeyPad[Type]+GetNewKeyFromSHA(StartKey,StartKey,SessionKeyLength)[SessionKeyIndex[Type]:]
    if Debug:
        print(PasswordHashHash.hex())
        print(MasterKey.hex())
        print(StartKey.hex())
        print(SessionKey.hex())
    return StartKey,SessionKey

def UpsateSessionKey(StartKey,SessionKey,Type="M"):
    SessionKeyLength=8 if Type!="S" else 16
    InterimKey=GetNewKeyFromSHA(StartKey,SessionKey,SessionKeyLength)
    return MPPE_Encrypt(InterimKey,InterimKey)

def MPPE_Decrypt(Encrypted:bytes,SessionKey):
    return ARC4.new(SessionKey).decrypt(Encrypted)

def MPPE_Encrypt(plainText:bytes,SessionKey):
    return ARC4.new(SessionKey).encrypt(plainText)

if __name__ == "__main__":
    UserName = "User"
    Password = "clientPass"
    NtResponse =bytes.fromhex("82 30 9E CD 8D 70 8B 5E A0 8F AA 39 81 CD 83 54 42 33 11 4A 3D 85 D6 DF")
    StartKey,SessionKey=GetSessionKey(Password,NtResponse,True,Type="L",Debug=True)
    print(MPPE_Encrypt("test message".encode(),SessionKey).hex())
41c00c584bd2d91c4017a2a12fa59f3f
fdece3717a8c838cb388e527ae3cdd31
8b7cdc149b993a1b
d1269ec49fa62e3e
929137917e5803d668d75898

截图

4、MPPE算法解密测试

from MPPE import *
username = "PPTP"
password = "123456"
NtResponse = bytes.fromhex("c517a6b0c33f026f3288e6d5287afc6ec7cdb2236ad2d652")
print(NtResponse.hex())
StartKey, SessionKey = GetSessionKey(password, NtResponse, False, Type="S", Debug=True)
enc = bytes.fromhex("90013e367452f9852e1835508e773c0d4e9e6f0138e330b294b37c3dbdb89be777fca662a048c1abfdeaeb586ab0df25ef62c28c045f7c177465fcdc611efdb8191b3b542732e741cc31ac3f18d14fe0fa84a0b40984addfd8ffb9a520359ba8bc76a3db31dde2f0a501648adc5c99d460584b24fd6b45e799d9a99eb58fab7e73a11bdb2fe832f0dbef70683ab5a17be689915271526026a8878cdcc54d030c33584be34c16ed00584a6f824ff5c25bef2f89819ab76abffba242db600d23447cd2e08d63ab9ce64b9b7ef22d88e3ba62f588980f65c941a3905ad8936f7673cdc29fdd61b50f12fd6ebdb3dcb31fc9029e46d90120c5d2c9b75e74ad12992543c88ac500192bff08e94fbc6f201dbe58d90c91bef499d81eef2e19ad3463db9ad6222233b8ffda8a2f0d84697b97080b977ee4f1d9b1d21dc823928161adb6d16a45950403cee9bfec587f")
n = enc[1]
for i in range(n+1):
    SessionKey = UpdateSessionKey(StartKey, SessionKey, "S")
    print(SessionKey.hex())
print(MPPE_Decrypt(enc[2:], SessionKey).hex())
dd97ea03890e253f93af68aabfee2334
8edcd8150dde97c96f45a7c9a1161cd6
9b76b9115d4a29c5f92e4ccf479c29c3
37ca6772a73d1db7111d4e4e3381eea9
0d129f23039d92bd80f13a827d90f7ee
17db84eb52375eb2490f7d6d7f5120a6
00214500014808de000080114a8ec0a82591ffffffff0044004301344e6c01080600e1a4944e06000000c0a8259100000000000000000000000000534500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000638253633501083d07080053450000000c0f71313037392d3762653131616330653c084d53465420352e303706062c2b01f90f2b03dc0100ff00000000000000
标签:,
  1. 请教一下大佬,MPPE密钥生成算法的第5步中,SessionKey更新一次才能用于加密数据这一部分,您是在哪里找到的呢?我也是参考RFC 3078和3079,尝试MS-CHAP v1的MPPE加解密没有成功。

    • 因为https://datatracker.ietf.org/doc/html/rfc3078#section-7.2里面说了,每次加密传输前都要更新一次
      虽然对于第一次传输来说这个session key已经是新的了,但是按照RFC的说法再更新一次好像也没什么问题2333

不说点什么喵?

12 − 4 =

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据