From 70de3752a015c79a6c1918151c8329fb14c8747e Mon Sep 17 00:00:00 2001
From: magnum <magnum>
Date: Thu, 15 Sep 2011 23:01:11 +0200
Subject: [PATCH] Performance patch for mskrb5, including a non-OpenSSL RC4

---
 src/Makefile          |    1 +
 src/mskrb5_fmt_plug.c |  233 ++++++++++++++++++++-----------------------------
 src/rc4.c             |   70 +++++++++++++++
 src/rc4.h             |   39 ++++++++
 src/unicode.c         |   15 ++--
 5 files changed, 209 insertions(+), 149 deletions(-)
 create mode 100644 src/rc4.c
 create mode 100644 src/rc4.h

diff --git a/src/Makefile b/src/Makefile
index 8ed6041..c72c6f8 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -58,6 +58,7 @@ JOHN_OBJS = \
 	md5_go.o \
 	md5_eq.o \
 	md5.o \
+	rc4.o \
 	hmacmd5.o \
 	base64.o \
 	md4.o \
diff --git a/src/mskrb5_fmt_plug.c b/src/mskrb5_fmt_plug.c
index e60785a..e3d3f31 100644
--- a/src/mskrb5_fmt_plug.c
+++ b/src/mskrb5_fmt_plug.c
@@ -1,33 +1,36 @@
-/*****************************************************************************
-mskrb5_fmt.c
-
-MS Kerberos 5 "PA ENC TIMESTAMP" by magnum 2011, no rights reserved
-
-This attacks a known-plaintext vulnerability in AS_REQ pre-auth packets. The
-known plaintext is a UTC timestamp in the format 20081120171510Z. Only if this
-indicate a match we decrypt the whole timestamp and calculate our own checksum
-to be really sure.
-
-The plaintext attack combined with re-using key setup was said to result in
-more than 60% speedup. This was confirmed using John the Ripper and variants
-of this code.
-
-http://www.ietf.org/rfc/rfc4757.txt
-http://www.securiteam.com/windowsntfocus/5BP0H0A6KM.html
-
-Input format is 'user:$mskrb5$user$realm$checksum$timestamp'
-
-For compatibility with (possible) future attacks, there are fields for username
-and realm but they are not used in this attack so they can be empty. Example:
-
-user:$mskrb5$$$02E837D06B2AC76891F388D9CC36C67A$2A9785BF5036C45D3843490BF9C228E8C18653E10CE58D7F8EF119D2EF4F92B1803B1451
-
-OMP is supported but it doesn't scale well on more than a few cores as-is.
-
-Based on the Kerberos 5 module by Nasko Oskov, in turn based on
-the KRB4 module by Dug Song.
-
-*****************************************************************************/
+/*
+ * mskrb5_fmt.c
+ *
+ * MS Kerberos 5 "PA ENC TIMESTAMP" by magnum
+ *
+ * This attacks a known-plaintext vulnerability in AS_REQ pre-auth packets. The
+ * known plaintext is a UTC timestamp in the format 20081120171510Z. Only if
+ * this indicate a match we decrypt the whole timestamp and calculate our own
+ * checksum to be really sure.
+ *
+ * The plaintext attack combined with re-using key setup was said to result in
+ * more than 60% speedup. This was confirmed using John the Ripper and variants
+ * of this code.
+ *
+ * http://www.ietf.org/rfc/rfc4757.txt
+ * http://www.securiteam.com/windowsntfocus/5BP0H0A6KM.html
+ *
+ * Input format is 'user:$mskrb5$user$realm$checksum$timestamp'
+ *
+ * For compatibility with (possible) future attacks, there are fields for
+ * username and realm but they are not used in this attack so they can be
+ * empty. Example:
+ *
+ * user:$mskrb5$$$02E837D06B2AC76891F388D9CC36C67A$2A9785BF5036C45D3843490BF9C228E8C18653E10CE58D7F8EF119D2EF4F92B1803B1451
+ *
+ * OMP is supported and scales very well now.
+ *
+ * This  software is hereby  placed in the public domain.  In case that is not
+ * applicable, it is Copyright © 2011 magnum, and it is hereby released to the
+ * general public under the following terms:  Redistribution and use in source
+ * and binary forms, with or without modification, is permitted.
+ *
+ */
 
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -47,28 +50,10 @@ the KRB4 module by Dug Song.
 #include "common.h"
 #include "unicode.h"
 
-#if 0
-#warning INFO: We are using OpenSSL HMAC_MD5
-#include <openssl/hmac.h>
-static void hmac_md5(const unsigned char *key, const unsigned char *data, int datalen, unsigned char *digest)
-{
-	HMAC(EVP_md5(), key, 16, data, datalen, digest, NULL);
-}
-#else
 #include "md5.h"
 #include "hmacmd5.h"
-// Move a hmac_md5_init() from crypt_all to set_key(),
-// and store &ctx in saved_key.
-//
-// Pro: saves another key setup using same key
-// Con: larger saved_key (288 bytes vs 16)
-//
-// This boosted performance with about 8%
-#define SPLIT_HMACMD5
-#endif
-
-#include <openssl/rc4.h>
 #include "md4.h"
+#include "rc4.h"
 
 #define FORMAT_LABEL       "mskrb5"
 #define FORMAT_NAME        "MS Kerberos 5 AS-REQ Pre-Auth"
@@ -76,7 +61,6 @@ static void hmac_md5(const unsigned char *key, const unsigned char *data, int da
 #define BENCHMARK_COMMENT  ""
 #define BENCHMARK_LENGTH   0
 #define PLAINTEXT_LENGTH   125
-#define UTF8_PLAINTEXT_LEN 125
 #define CRYPT_BINARY_SIZE  8
 #define BINARY_SIZE        0
 #define MAX_REALMLEN       20
@@ -89,7 +73,6 @@ static void hmac_md5(const unsigned char *key, const unsigned char *data, int da
 #define PLAINTEXT_OFFSET   14
 
 // these may be altered in init() if running OMP
-// and that formula is subject to change
 #define MIN_KEYS_PER_CRYPT	    1
 #define THREAD_RATIO            32
 #ifdef _OPENMP
@@ -110,14 +93,11 @@ static struct fmt_tests tests[] = {
 	{NULL}
 };
 
-static ARCH_WORD_32 saved_plain[MAX_KEYS_PER_CRYPT][(UTF8_PLAINTEXT_LEN+4)/sizeof(ARCH_WORD_32)];
+static ARCH_WORD_32 saved_plain[MAX_KEYS_PER_CRYPT][(PLAINTEXT_LENGTH+4)/sizeof(ARCH_WORD_32)];
+static int saved_len[MAX_KEYS_PER_CRYPT];
 static ARCH_WORD_32 output[MAX_KEYS_PER_CRYPT][CRYPT_BINARY_SIZE/sizeof(ARCH_WORD_32)];
-#ifdef SPLIT_HMACMD5
-static HMACMD5Context saved_key[MAX_KEYS_PER_CRYPT];
-#else
-static unsigned char saved_key[MAX_KEYS_PER_CRYPT][KEY_SIZE];
-#endif
-
+static HMACMD5Context saved_ctx[MAX_KEYS_PER_CRYPT];
+static int keys_prepared;
 static unsigned char *saltblob = NULL;
 #define CHECKSUM  saltblob
 #define TIMESTAMP &saltblob[CHECKSUM_SIZE]
@@ -132,26 +112,25 @@ static void init(struct fmt_main *pFmt)
 	if (n > MAX_KEYS_PER_CRYPT)
 		n = MAX_KEYS_PER_CRYPT;
 	fmt_mskrb5.params.min_keys_per_crypt = n;
-	n = n * n * ((n >> 1) + 1) * THREAD_RATIO;
+	n = n * (n << 1) * THREAD_RATIO;
 	if (n > MAX_KEYS_PER_CRYPT)
 		n = MAX_KEYS_PER_CRYPT;
 	fmt_mskrb5.params.max_keys_per_crypt = n;
 #endif
 	if (options.utf8) {
-		// in utf-8, up to four bytes can compose one character
-		fmt_mskrb5.params.plaintext_length = UTF8_PLAINTEXT_LEN;
-		tests[1].plaintext = "\xC3\xBC";         // German u-umlaut in UTF-8
-		tests[1].ciphertext = "$mskrb5$$$958db4ddb514a6cc8be1b1ccf82b0191$090408357a6f41852d17f3b4bb4634adfd388db1be64d3fe1a1d75ee4338d2a4aea387e5";
-		tests[2].plaintext = "\xC3\x9C\xC3\x9C"; // 2x uppercase of them
-		tests[2].ciphertext = "$mskrb5$$$057cd5cb706b3de18e059912b1f057e3$fe2e561bd4e42767e972835ea99f08582ba526e62a6a2b6f61364e30aca7c6631929d427";
-	}
-	if (options.utf8) {
-		// in utf-8, up to four bytes can compose one character
-		fmt_mskrb5.params.plaintext_length = UTF8_PLAINTEXT_LEN;
-		tests[1].plaintext = "\xC3\xBC";         // German u-umlaut in UTF-8
+		tests[1].plaintext = "\xC3\xBC"; // German u-umlaut in UTF-8
 		tests[1].ciphertext = "$mskrb5$$$958db4ddb514a6cc8be1b1ccf82b0191$090408357a6f41852d17f3b4bb4634adfd388db1be64d3fe1a1d75ee4338d2a4aea387e5";
 		tests[2].plaintext = "\xC3\x9C\xC3\x9C"; // 2x uppercase of them
 		tests[2].ciphertext = "$mskrb5$$$057cd5cb706b3de18e059912b1f057e3$fe2e561bd4e42767e972835ea99f08582ba526e62a6a2b6f61364e30aca7c6631929d427";
+	} else {
+		if (CP_to_Unicode[0xfc] == 0x00fc) {
+			tests[1].plaintext = "\xFC";     // German u-umlaut in many ISO-8859-x
+			tests[1].ciphertext = "$mskrb5$$$958db4ddb514a6cc8be1b1ccf82b0191$090408357a6f41852d17f3b4bb4634adfd388db1be64d3fe1a1d75ee4338d2a4aea387e5";
+		}
+		if (CP_to_Unicode[0xdc] == 0x00dc) {
+			tests[2].plaintext = "\xDC\xDC"; // 2x uppercase of them
+			tests[2].ciphertext = "$mskrb5$$$057cd5cb706b3de18e059912b1f057e3$fe2e561bd4e42767e972835ea99f08582ba526e62a6a2b6f61364e30aca7c6631929d427";
+		}
 	}
 }
 
@@ -162,26 +141,13 @@ static char *hex2bin(char *src, unsigned char *dst, int outsize)
 
 	pe = src + strlen(src);
 	qe = dst + outsize;
-
-	for (p = src, q = dst;
-	    p < pe && q < qe && isxdigit((int) (unsigned char) *p); p += 2) {
-		ch = tolower((int) (unsigned char) p[0]);
-		cl = tolower((int) (unsigned char) p[1]);
-
-		if ((ch >= '0') && (ch <= '9'))
-			ch -= '0';
-		else if ((ch >= 'a') && (ch <= 'f'))
-			ch -= 'a' - 10;
-		else
+	p = src, q = dst;
+	while (p < pe && q < qe && (ch = atoi16[ARCH_INDEX(*p++)]) != 0x7f) {
+		if (ch == 0x7f)
+			return p;
+		cl = atoi16[ARCH_INDEX(*p++)];
+		if (cl == 0x7f)
 			return p;
-
-		if ((cl >= '0') && (cl <= '9'))
-			cl -= '0';
-		else if ((cl >= 'a') && (cl <= 'f'))
-			cl -= 'a' - 10;
-		else
-			return p + 1;
-
 		*q++ = (ch << 4) | cl;
 	}
 	return p;
@@ -292,33 +258,9 @@ static int valid(char *ciphertext, struct fmt_main *pFmt)
 
 static void set_key(char *key, int index)
 {
-	const unsigned char one[] = { 1, 0, 0, 0 };
-	unsigned char K[KEY_SIZE];
-	int len;
-#ifdef SPLIT_HMACMD5
-	unsigned char K1[KEY_SIZE];
-#endif
-
-	len = strlen(key);
-	memcpy(saved_plain[index], key, len + 1);
-
-	// K = MD4(UTF-16LE(password)), ordinary 16-byte NTLM hash
-	len = E_md4hash((unsigned char *) saved_plain[index], len, K);
-
-	if (len <= 0)
-		((char*)(saved_plain[index]))[-len] = 0;	// match truncation
-
-	// K1 = HMAC-MD5(K, 1)
-	// 1 is encoded as little endian in 4 bytes (0x01000000)
-#ifdef SPLIT_HMACMD5
-	hmac_md5(K, (unsigned char *) &one, 4, K1);
-
-	// We do key setup of the next HMAC_MD5 here. rest in crypt_all()
-	// &saved_key is now &ctx
-	hmac_md5_init_K16(K1, &saved_key[index]);
-#else
-	hmac_md5(K, (unsigned char *) &one, 4, saved_key[index]);
-#endif
+	saved_len[index] = strlen(key);
+	memcpy(saved_plain[index], key, saved_len[index] + 1);
+	keys_prepared = 0;
 }
 
 static char *get_key(int index)
@@ -328,27 +270,48 @@ static char *get_key(int index)
 
 static void crypt_all(int count)
 {
-	unsigned char K3[KEY_SIZE], cleartext[PLAINTEXT_OFFSET + CRYPT_BINARY_SIZE];
-	RC4_KEY key;
+	const unsigned char one[] = { 1, 0, 0, 0 };
 	int i;
 
+	if (!keys_prepared)	{
+#ifdef _OPENMP
+#pragma omp parallel for
+#endif
+		for (i = 0; i < count; i++) {
+			int len;
+			unsigned char K[KEY_SIZE];
+			unsigned char K1[KEY_SIZE];
+			// K = MD4(UTF-16LE(password)), ordinary 16-byte NTLM hash
+			len = E_md4hash((unsigned char *) saved_plain[i], saved_len[i], K);
+
+			if (len <= 0)
+				((char*)(saved_plain[i]))[-len] = 0;	// match truncation
+
+			// K1 = HMAC-MD5(K, 1)
+			// 1 is encoded as little endian in 4 bytes (0x01000000)
+			hmac_md5(K, (unsigned char *) &one, 4, K1);
+
+			// We do key setup of the next HMAC_MD5 here. rest in inner loop
+			hmac_md5_init_K16(K1, &saved_ctx[i]);
+		}
+		keys_prepared = 1;
+	}
+
 #ifdef _OPENMP
-#pragma omp parallel for default(none) private(i, K3, cleartext, key) shared(count, output, saved_key, saltblob)
+#pragma omp parallel for
 #endif
 	for (i = 0; i < count; i++) {
-		// K1 is stored in saved_key[index]
+		unsigned char K3[KEY_SIZE], cleartext[PLAINTEXT_OFFSET + CRYPT_BINARY_SIZE];
+		HMACMD5Context ctx;
+		RC4_KEY key;
+		// key set up with K1 is stored in saved_ctx[index]
 		// CHECKSUM and TIMESTAMP are just defines, they are actually
 		// concatenated to saltblob[]
 
 		// K3 = HMAC-MD5(K1, CHECKSUM)
-#ifdef SPLIT_HMACMD5
-		HMACMD5Context ctx; // can't be moved above the OMP pragma
-		memcpy(&ctx, &saved_key[i], sizeof(ctx));
+		memcpy(&ctx, &saved_ctx[i], sizeof(ctx));
 		hmac_md5_update(CHECKSUM, CHECKSUM_SIZE, &ctx);
 		hmac_md5_final(K3, &ctx);
-#else
-		hmac_md5(saved_key[i], CHECKSUM, CHECKSUM_SIZE, K3);
-#endif
 
 		// RC4(K3, TIMESTAMP) decrypt part of the timestamp
 		RC4_set_key(&key, KEY_SIZE, K3);
@@ -396,37 +359,27 @@ static int cmp_one(void *binary, int index)
 
 static int cmp_exact(char *source, int index)
 {
-#ifdef SPLIT_HMACMD5
 	HMACMD5Context ctx;
-#endif
 	unsigned char K2[KEY_SIZE], K3[KEY_SIZE];
 	RC4_KEY key;
 	unsigned char cleartext[TIMESTAMP_SIZE];
 
-	// K1 is stored in saved_key[index]
+	// K1 is stored in saved_ctx[index]
 	// CHECKSUM and TIMESTAMP are just defines, they are actually
 	// concatenated to saltblob[]
 
 	// K3 = HMAC-MD5(K1, CHECKSUM)
-#ifdef SPLIT_HMACMD5
-	memcpy(&ctx, &saved_key[index], sizeof(ctx));
-	hmac_md5_update(CHECKSUM, CHECKSUM_SIZE, &saved_key[index]);
-	hmac_md5_final(K3, &saved_key[index]);
-#else
-	hmac_md5(saved_key[index], CHECKSUM, CHECKSUM_SIZE, K3);
-#endif
+	memcpy(&ctx, &saved_ctx[index], sizeof(ctx));
+	hmac_md5_update(CHECKSUM, CHECKSUM_SIZE, &saved_ctx[index]);
+	hmac_md5_final(K3, &saved_ctx[index]);
 
 	// Decrypt the timestamp with the derived key K3
 	RC4_set_key(&key, KEY_SIZE, K3);
 	RC4(&key, TIMESTAMP_SIZE, TIMESTAMP, cleartext);
 
 	// create checksum K2 = HMAC-MD5(K1, cleartext)
-#ifdef SPLIT_HMACMD5
 	hmac_md5_update(cleartext, TIMESTAMP_SIZE, &ctx);
 	hmac_md5_final(K2, &ctx);
-#else
-	hmac_md5(saved_key[index], cleartext, TIMESTAMP_SIZE, K2);
-#endif
 
 	// Compare our checksum with the input checksum
 	return (!memcmp(K2, CHECKSUM, CHECKSUM_SIZE));
diff --git a/src/rc4.c b/src/rc4.c
new file mode 100644
index 0000000..9682717
--- /dev/null
+++ b/src/rc4.c
@@ -0,0 +1,70 @@
+/*
+ * Our own RC4 based on the "original" as posted to sci.crypt in 1994 and
+ * tweaked for  performance  on  x86-64.   OpenSSL is probably faster for
+ * decrypting larger amounts of data but we are more interested in a very
+ * fast key setup.  On Intel and AMD x64, I have seen up to 50% speedups.
+ *
+ * The speed  improvement  (if you see one) is due to OpenSSL's  (or your
+ * distributor's) choice of type for RC4_INT. Some systems perform bad if
+ * this is defined as char. Others perform bad if it's not. If needed, we
+ * could move JOHN_RC4_INT to arch.h
+ *
+ * Syntax is same as OpenSSL;
+ * just #include "rc4.h"  instead of  <openssl/rc4.h>
+ *
+ * Put together by magnum in 2011. No Rights Reserved.
+ */
+
+#include "rc4.h"
+
+#define swap_byte(a, b) { RC4_INT swapByte = (*a); (*a) = (*b); (*b) = swapByte; }
+
+#define swap_state(n) { \
+	index2 = (key_data_ptr[index1] + state[(n)] + index2) % 256; \
+	swap_byte(&state[(n)], &state[index2]); \
+	if (++index1 == key_data_len) index1 = 0; \
+}
+
+void RC4_set_key(RC4_KEY *key, RC4_INT key_data_len, const unsigned char *key_data_ptr)
+{
+	RC4_INT index1;
+	RC4_INT index2;
+	RC4_INT *state;
+	RC4_INT counter;
+
+	state = &key->state[0];
+	for(counter = 0; counter < 256; counter++)
+		state[counter] = counter;
+	key->x = 0;
+	key->y = 0;
+	index1 = 0;
+	index2 = 0;
+	for(counter = 0; counter < 256; counter += 4) {
+		swap_state(counter);
+		swap_state(counter + 1);
+		swap_state(counter + 2);
+		swap_state(counter + 3);
+	}
+}
+
+void RC4(RC4_KEY *key, RC4_INT buffer_len, const unsigned char *buffer_ptr, unsigned char *out)
+{
+	RC4_INT x;
+	RC4_INT y;
+	RC4_INT *state;
+	RC4_INT counter;
+
+	x = key->x;
+	y = key->y;
+
+	state = &key->state[0];
+	for(counter = 0; counter < buffer_len; counter ++)
+		{
+			x = (x + 1);
+			y = (state[x] + y) % 256;
+			swap_byte(&state[x], &state[y]);
+			*out++ = buffer_ptr[counter] ^ state[(state[x] + state[y]) % 256];
+		}
+	key->x = x;
+	key->y = y;
+}
diff --git a/src/rc4.h b/src/rc4.h
new file mode 100644
index 0000000..89cc560
--- /dev/null
+++ b/src/rc4.h
@@ -0,0 +1,39 @@
+/*
+ * Our own RC4 based on the "original" as posted to sci.crypt in 1994 and
+ * tweaked for  performance  on  x86-64.   OpenSSL is probably faster for
+ * decrypting larger amounts of data but we are more interested in a very
+ * fast key setup.  On Intel and AMD x64, I have seen up to 50% speedups.
+ *
+ * The speed  improvement  (if you see one) is due to OpenSSL's  (or your
+ * distributor's) choice of type for RC4_INT. Some systems perform bad if
+ * this is defined as char. Others perform bad if it's not. If needed, we
+ * could move JOHN_RC4_INT to arch.h
+ *
+ * Syntax is same as OpenSSL;
+ * just #include "rc4.h"  instead of  <openssl/rc4.h>
+ *
+ * Put together by magnum in 2011. No Rights Reserved.
+ */
+
+#ifndef HEADER_RC4_H
+#define HEADER_RC4_H
+
+#define RC4_KEY JOHN_RC4_KEY
+#define RC4_INT JOHN_RC4_INT
+#define RC4_set_key JOHN_RC4_set_key
+#define RC4 JOHN_RC4
+
+#include "arch.h"
+#define JOHN_RC4_INT unsigned ARCH_WORD
+
+typedef struct rc4_key
+{
+	RC4_INT state[256];
+	RC4_INT x;
+	RC4_INT y;
+} RC4_KEY;
+
+extern void RC4(RC4_KEY *key, RC4_INT len, const unsigned char *indata, unsigned char *outdata);
+extern void RC4_set_key(RC4_KEY *key, RC4_INT len, const unsigned char *data);
+
+#endif /* HEADER_RC4_H */
diff --git a/src/unicode.c b/src/unicode.c
index d0b911a..10c529b 100644
--- a/src/unicode.c
+++ b/src/unicode.c
@@ -376,18 +376,19 @@ inline unsigned int strlen16(const UTF16 * str)
  * Written by Alain Espinosa <alainesp@gmail.com> in 2008
  * and placed in the public domain.
  *
+ * This is now thread-safe
  */
 int E_md4hash(const UTF8 * passwd, unsigned int len, unsigned char *p16)
 {
 	int trunclen;
-	static UTF16 wpwd[PLAINTEXT_BUFFER_SIZE + 1];
+	UTF16 wpwd[PLAINTEXT_BUFFER_SIZE + 1];
 
 	// Quick work around. We may want to port the else{} into BE code,
 	// and do it without impacting LE speed. The boost is not huge though.
 #if ARCH_ALLOWS_UNALIGNED
 	if (len > 27) {
 #endif
-		static MD4_CTX ctx;
+		MD4_CTX ctx;
 
 		/* Password is converted to UTF-16LE */
 		trunclen = enc_to_utf16(wpwd, PLAINTEXT_BUFFER_SIZE, passwd, len);
@@ -401,14 +402,15 @@ int E_md4hash(const UTF8 * passwd, unsigned int len, unsigned char *p16)
 		MD4_Final(p16, &ctx);
 #if ARCH_ALLOWS_UNALIGNED
 	} else {
-		static unsigned int nt_buffer[16];
-		static unsigned int lastlen = 15;
+		unsigned int nt_buffer[16];
 		unsigned int a = INIT_A;
 		unsigned int b = INIT_B;
 		unsigned int c = INIT_C;
 		unsigned int d = INIT_D;
 		unsigned int i = 0, md4_size = 0;
 
+		memset(nt_buffer, 0, sizeof(nt_buffer));
+
 		/* Password is converted to UTF-16LE */
 		trunclen = enc_to_utf16(wpwd, 27, passwd, len);
 		// We need to check this because it's not just a matter of truncating
@@ -426,11 +428,6 @@ int E_md4hash(const UTF8 * passwd, unsigned int len, unsigned char *p16)
 		else
 			nt_buffer[i] = 0x80;
 
-		/* Key cleaning */
-		for (i++; i <= lastlen; i++)
-			nt_buffer[i] = 0;
-
-		lastlen = md4_size >> 1;
 		nt_buffer[14] = md4_size << 4;
 
 		/* Round 1 */
-- 
1.7.4.1

