0% found this document useful (0 votes)
123 views20 pages

Server Security-To Detect and Prevent Brute Force and Dictionary Attack

This document discusses server security techniques to detect and prevent brute force and dictionary attacks. It provides an overview of dictionary attacks, which systematically try words from a dictionary as passwords, and brute force attacks, which try every possible combination. It then describes how the tool John the Ripper can be used to conduct dictionary attacks and analyze password strength. It also discusses the hashing algorithm bcrypt, which helps secure passwords by using salts and multiple encryption rounds to make passwords very difficult to crack.

Uploaded by

Cherry
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
123 views20 pages

Server Security-To Detect and Prevent Brute Force and Dictionary Attack

This document discusses server security techniques to detect and prevent brute force and dictionary attacks. It provides an overview of dictionary attacks, which systematically try words from a dictionary as passwords, and brute force attacks, which try every possible combination. It then describes how the tool John the Ripper can be used to conduct dictionary attacks and analyze password strength. It also discusses the hashing algorithm bcrypt, which helps secure passwords by using salts and multiple encryption rounds to make passwords very difficult to crack.

Uploaded by

Cherry
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 20

SERVER SECURITY-TO DETECT AND PREVENT

BRUTE FORCE AND DICTIONARY ATTACK

A PROJECT REPORT

Submitted by

Anshil Seth 18BCI0173


Raunak Thapliyal 18BCI0163

Course Title: Cryptography fundamentals

Course Code: CSE – 1011

Under the guidance of

Prof. Ramani S.

VIT University, Vellore.

1|Page
ABSTRACT

DICTIONARY ATTACK-A dictionary attack is a method of breaking into


a password-protected computer or server by systematically entering every
word in a dictionary as a password. A dictionary attack can also be used in an
attempt to find the key necessary to decrypt an encrypted message or
document.
Typically a guessing attack which uses precompiled list of options. Rather
than trying every option, only try complete options which are likely to work.

BRUTE FORCE ATTACK- In this attack attacker attempts to determine a


secret by trying every possible combination.The time to complete is greater,
but there is greater coverage of likely cleartext value (all possibilities only if
set to the maximum length and every possible character is considered in every
position)
A weakness of dictionary attacks is that it obviously relies on words supplied
by a user.Dictionary attacks like ‘JOHN THE RIPPER’ can be prevented by
a method called ‘SALTING’. A salt is random data that is used as an
additional input to a one-way function that "hashes" a password or
passphrase.
The primary function of salts is to defend against dictionary attacks or against
its hashed equivalent, a pre-computed rainbow table attack.
For detecting this attack, we can see the average time is taken by the attack
to crack a password, depending on the result we can find whether the ‘John
The Ripper’ uses ‘Dictionary Attacks’ or the general ‘brute force’ method .
In this project, programs will be created for detecting the above-mentioned
attacks and preventing it by using application of hashing algorithms.

2|Page
KEYWORDS:

 Dictionary Attack
 Brute force Attack
 John the ripper
 Hash Suit
 MD5 /SHA-1/SHA-2
 bcrypt
 Salting
 Blowfish

INTRODUCTION

To understand how to protect yourself from a password attack, you should


become familiar with the most commonly used types of attacks. With that
information, you can use password cracking tools and techniques to regularly
audit your own organization's passwords and determine whether your defences
need bolstering. The most common type of attack is password guessing.
Attackers can guess passwords locally or remotely using either a manual
or automated approach. Password guessing isn't always as difficult as
you'd expect. Most networks aren't configured to require long and
complex passwords, and an attacker needs to find only one weak
password to gain access to a network.

A dictionary attack is a technique or method used to breach the computer security


of a password-protected machine or server. A dictionary attack attempts to defeat
an authentication mechanism by systematically entering each word in a
3|Page
dictionary as a password or trying to determine the decryption key of an
encrypted message or document. Dictionary attacks are often successful because
many users and businesses use ordinary words as passwords. The most common
method of authenticating a user in a computer system is through a password. This
method may continue for several more decades because it is the most convenient
and practical way of authenticating users. However, this is also the weakest form
of authentication, because users frequently use ordinary words as passwords.
Antagonistic users such as hackers and spammers take advantage of this
weakness by using a dictionary attack. Hackers and spammers attempt to log in
to a computer system by trying all possible passwords until the correct one is
found.
Two countermeasures against dictionary attacks include:

 Delayed Response: A slightly delayed response from the server prevents


a hacker or spammer from checking multiple passwords within a short
period of time.
 Account Locking: Locking an account after several unsuccessful
attempts (for example, automatic locking after three or five unsuccessful
attempts) prevents a hacker or spammer from checking multiple
passwords to log in.

The scope of this project includes an explanation of dictionary attacks, how


to prevent them using slow hashing algorithms like bcrypt, scrypt,
PBKDF2. Dictionary attacks are not effective against systems that make
use of multiple-word passwords and also fail against systems that use
random permutations of lowercase and uppercase letters combined with
numerals.

4|Page
The difference between a Dictionary and a brute-force attack is that a Dictionary
contains a list of probable matches rather than all possible string combinations.
A Dictionary needs to be well optimized otherwise if it includes any string
combinations it risks becoming a brute-force attack and loses its efficiency.
Therefore, Dictionaries often include known popular passwords, words from the
English and other languages, ID numbers, phone numbers, sentences from
books, etc.

Contribution

John the Ripper:

John the Ripper is a free password cracking software tool. Initially


developed for the Unix operating system, it now runs on fifteen different
platforms (eleven of which are architecture-specific versions of Unix, DOS,
Win32, BeOS, and OpenVMS). It is one of the most popular password
testing and breaking programs as it combines a number of password
crackers into one package, autodetects password hash types, and includes a
customizable cracker. It can be run against various encrypted password
formats including several crypt password hash types most commonly found
on various Unix versions (based on DES, MD5, or Blowfish), Kerberos
AFS, and Windows NT/2000/XP/2003 LM hash. Additional modules have
extended its ability to include MD4-based password hashes and passwords
stored in LDAP, MySQL, and others.

One of the modes John can use is the dictionary attack. It takes text string
samples (usually from a file, called a wordlist, containing words found in a
dictionary or real passwords cracked before), encrypting it in the same

5|Page
format as the password being examined (including both the encryption
algorithm and key), and comparing the output to the encrypted string. It can
also perform a variety of alterations to the dictionary words and try these.
Many of these alterations are also used in John's single attack mode, which
modifies an associated plaintext (such as a username with an encrypted
password) and checks the variations against the hashes.

John the Ripper is often used in the enterprise to detect weak passwords that
could put network security at risk, as well as other administrative purposes.
The software can run a wide variety of password-cracking techniques
against the various user accounts on each operating system and can be
scripted to run locally or remotely.

bcrypt:

bcrypt is a password hashing function designed by Niels Provos and David


Mazières, based on the Blowfish cipher, and presented at USENIX in 1999.
bcrypt is a hashing algorithm that is scalable with hardware (via a
configurable number of rounds). Its slowness and multiple rounds ensure that
an attacker must deploy massive funds and hardware to be able to crack your
passwords. Add to that per-password salts (bcrypt REQUIRES salts) and you
can be sure that an attack is virtually unfeasible without either ludicrous
amount of funds or hardware.

Bcrypt is a cross-platform file encryption utility. Encrypted files are portable


across all supported operating systems and processors. Passphrases must be
between 8 and 56 characters and are hashed internally to a 448-bit key.
However, all characters supplied are significant. The stronger your
passphrase, the more secure your data.

6|Page
Bcrypt uses the Eksblowfish algorithm to hash passwords. While the
encryption phase of Eksblowfish and Blowfish are exactly the same, the key
schedule phase of Eksblowfish ensures that any subsequent state depends on
both salt and key (user password), and no state can be precomputed without
the knowledge of both. Because of this key difference, bcrypt is a one-way
hashing algorithm. You cannot retrieve the plain text password without
already knowing the salt, rounds, and key (password).

In addition to encrypting your data, bcrypt will by default overwrite the


original input file with random garbage three times before deleting it in order
to thwart data recovery attempts by persons who may gain access to your
computer. If you're not quite ready for this level of paranoia yet, see the
installation instructions below for how to disable this feature. If you don't
think this is paranoid enough see below.

Bcrypt Algorithm:

bcrypt (cost, salt, pwd)


state ~ EksblowfishSetup (cost, salt, key)
ctext ~ //Enter text to hashed//
repeat (64)
ctext ~ EncryptECB (state, ctext)
return Concatenate (cost, salt, ctext)

Eksblowfish Algorithm:

EksBlowfishSetup (cost, salt, key)


7|Page
State ~ InitState()
State ~ ExpandKey (state, salt, key)
repeat (2^cost)
state ~ ExpandKey (state, 0, salt)
state ~ ExpandKey (state, 0, key)
return state
//
EksblowfishSetup has three input parameters: a cost, a salt, and the encryption key. It returns a
set of subkeys and S-boxes, also known as a key schedule. The cost parameter controls how
expensive the key schedule is to compute. The salt is a 128-bit value that modifies the key
schedule so that the same key need not always produce the same result. Finally, the key
argument is a secret encryption key, which can be a user-chosen password of up to 56 bytes
(including a terminating zero byte when the key is an ASCII string).

 EksBlowfishSetup begins by calling InitState, a function that copies the digits of the
number first into the subkeys, then into the S-boxes.
 EksBlowfishSetup begins by calling InitState, a function that copies the digits of the
number first into the subkeys, then into the S-boxes. Subsequently, ExpandKey blowfish-
encrypts the first 64 bits of its salt argument using the current state of the key schedule.
The resulting ciphertext replaces subkeys P1 and P2. That same ciphertext is also XORed with
the second 64-bits of salt, and the result encrypted with the new state of the key schedule.
The process continues, alternating between the first and second 64 bits salt.
When ExpandKey finishes replacing entries in the P-Array, it continues on replacing S-box
entries two at a time. After replacing the last two entries of the last S-box, S4[254]
and S4[255], ExpandKey returns the new key schedule.
 After calling InitState to fill a new key schedule with the digits
of, EksBlowfishSetup calls Expand Key with the salt and key. This ensures that all
subsequent state depends on both, and that no part of the algorithm can be precomputed
without both salt and key. Thereafter, ExpandKey is alternately called with the salt and then
key for iterations. For all but the first invocation of ExpandKey, the second argument is a
block of 128 0 bits.
//

MD5 Algorithm:

8|Page
 The MD5 algorithm first divides the input in blocks of 512 bits each. 64 Bits are inserted at
the end of the last block. These 64 bits are used to record the length of the original input. If
the last block is less than 512 bits, some extra bits are 'padded' to the end.

 Next, each block is divided into 16 words of 32 bits each. These are denoted as M0 ... M15.

 MD5 uses a buffer that is made up of four words that are each 32 bits long. These words are
called A, B, C and D. They are initialized as:
word A: 01 23 45 67
word B: 89 ab cd ef
word C: fe dc ba 98
word D: 76 54 32 10

 MD5 further uses a table K that has 64 elements. Element number i is indicated as K i. The
table is computed beforehand to speed up the computations. The elements are computed
using the mathematical sin function:
Ki = abs(sin(i + 1)) * 232

 In addition MD5 uses four auxiliary functions that each take as input three 32-bit words and
produce as output one 32-bit word. They apply the logical operators and, or, not and xor to
the input bits.
F(X,Y,Z) = (X and Y) or (not(X) and Z)
G(X,Y,Z) = (X and Z) or (Y and not(Z))
H(X,Y,Z) = X xor Y xor Z
I(X,Y,Z) = Y xor (X or not(Z))

 The contents of the four buffers (A, B, C and D) are now mixed with the words of the input,
using the four auxiliary functions (F, G, H and I). There are four rounds, each involves 16
basic operations. One operation is illustrated in the figure below.

The figure shows how the auxiliary function F is applied to the four buffers (A, B, C and D),
using message word Mi and constant Ki. The item "<<<s" denotes a binary left shift by s bits.

9|Page
SHA-1 pseudocode:

Note 1: All variables are unsigned 32-bit quantities and


wrap modulo 232 when calculating, except for
ml, the message length, which is a 64-bit quantity,
and
hh, the message digest, which is a 160-bit
quantity.
Note 2: All constants in this pseudo code are in big endian.
Within each word, the most significant byte is
stored in the leftmost byte position

Initialize variables:

h0 = 0x67452301
h1 = 0xEFCDAB89
h2 = 0x98BADCFE
h3 = 0x10325476
h4 = 0xC3D2E1F0

ml = message length in bits (always a multiple of the


number of bits in a character).

Pre-processing:
append the bit '1' to the message e.g. by adding 0x80 if
message length is a multiple of 8 bits.
append 0 ≤ k < 512 bits '0', such that the resulting message
length in bits
is congruent to −64 ≡ 448 (mod 512)
append ml, the original message length, as a 64-bit big-
endian integer.
Thus, the total length is a multiple of 512 bits.

Process the message in successive 512-bit chunks:


break message into 512-bit chunks
for each chunk
break chunk into sixteen 32-bit big-endian words w[i],
0 ≤ i ≤ 15

10 | P a g e
Message schedule: extend the sixteen 32-bit words into
eighty 32-bit words:
for i from 16 to 79
Note 3: SHA-0 differs by not having this
leftrotate.
w[i] = (w[i-3] xor w[i-8] xor w[i-14] xor w[i-16])
leftrotate 1

Initialize hash value for this chunk:


a = h0
b = h1
c = h2
d = h3
e = h4

Main loop:[3][56]
for i from 0 to 79
if 0 ≤ i ≤ 19 then
f = (b and c) or ((not b) and d)
k = 0x5A827999
else if 20 ≤ i ≤ 39
f = b xor c xor d
k = 0x6ED9EBA1
else if 40 ≤ i ≤ 59
f = (b and c) or (b and d) or (c and d)
k = 0x8F1BBCDC
else if 60 ≤ i ≤ 79
f = b xor c xor d
k = 0xCA62C1D6

temp = (a leftrotate 5) + f + e + k + w[i]


e = d
d = c
c = b leftrotate 30
b = a
a = temp

Add this chunk's hash to result so far:


h0 = h0 + a
h1 = h1 + b
h2 = h2 + c
h3 = h3 + d
h4 = h4 + e

11 | P a g e
Produce the final hash value (big-endian) as a 160-bit
number:
hh = (h0 leftshift 128) or (h1 leftshift 96) or (h2
leftshift 64) or (h3 leftshift 32) or h4

Implementation

 Description of Modules/Programs – Hash suite:

Storing user passwords in the plain text naturally results in an instant


compromise of all passwords if the password file is compromised. To reduce
this danger, Windows applies a cryptographic hash function, which
transforms each password into a hash, and stores this hash. This hash function
is one-way in the sense that it is infeasible to infer a password back from its
hash, except via the trial and error approach described below. To authenticate
a user, the password presented by the user is hashed and compared with the
stored hash.

Hash Suite, like all other password hash crackers, does not try to "invert" the
hash to obtain the password (which might be impossible). It follows the same
procedure used by authentication: it generates different candidate passwords
(keys), hashes them and compares the computed hashes with the stored
hashes. This approach works because

12 | P a g e
users generally select passwords that are easy to remember, and as a side-
effect, these passwords are typically easy to crack. Another reason why this
approach is so very effective is that Windows uses password hash functions
that are very fast to compute, especially in an attack (for each given candidate
password).

 Source Code
MD5:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>

typedef union uwb {


unsigned w;
unsigned char b[4];

} MD5union;
typedef unsigned DigestArray[4];
unsigned func0( unsigned abcd[] ){
return ( abcd[1] & abcd[2]) | (~abcd[1] & abcd[3]);}
unsigned func1( unsigned abcd[] ){
return ( abcd[3] & abcd[1]) | (~abcd[3] & abcd[2]);}
unsigned func2( unsigned abcd[] ){
return abcd[1] ^ abcd[2] ^ abcd[3];}
unsigned func3( unsigned abcd[] ){
return abcd[2] ^ (abcd[1] |~ abcd[3]);}
typedef unsigned (*DgstFctn)(unsigned a[]);

unsigned *calctable( unsigned *k)


{
double s, pwr;
int i;
pwr = pow( 2, 32);
for (i=0; i<64; i++) {
s = fabs(sin(1+i));
k[i] = (unsigned)( s * pwr );

13 | P a g e
}
return k;
}unsigned rol( unsigned r, short N )
{ unsigned mask1 = (1<<N) -1;

return ((r>>(32-N)) & mask1) | ((r<<N) & ~mask1);


}
unsigned *md5( const char *msg, int mlen)
{
static DigestArray h0 = { 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476 };
static DgstFctn ff[] = { &func0, &func1, &func2, &func3 }; static short M[] = { 1, 5,
3, 7 }

static short O[] = { 0, 1, 5, 0 };


static short rot0[] = { 7,12,17,22};
static short rot1[] = { 5, 9,14,20};
static short rot2[] = { 4,11,16,23};
static short rot3[] = { 6,10,15,21};
static short *rots[] = {rot0, rot1, rot2, rot3 };
static unsigned kspace[64];
static unsigned *k;
static DigestArray h;
DigestArray abcd;
DgstFctn fctn; short
m, o, g; unsigned f;
short *rotn;
union {
unsigned w[16];
char b[64];
}mm;
int os = 0;
int grp, grps, q, p;
unsigned char *msg2;
if (k==NULL) k= calctable(kspace);
for (q=0; q<4; q++) h[q] = h0[q]; // initialize
{
grps = 1 + (mlen+8)/64;
msg2 = malloc( 64*grps);
memcpy( msg2, msg, mlen);
msg2[mlen] = (unsigned char)0x80;
q = mlen + 1;

14 | P a g e
while (q < 64*grps){ msg2[q] = 0; q++ ; }

{
MD5union u;
u.w = 8*mlen;
q -= 8;
memcpy(msg2+q, &u.w, 4 );
}
}

for (grp=0; grp<grps; grp++)


{
memcpy( mm.b, msg2+os, 64);
for(q=0;q<4;q++) abcd[q] = h[q]; for
(p = 0; p<4; p++) {
fctn = ff[p];
rotn = rots[p];
m = M[p]; o= O[p];
for (q=0; q<16; q++) { g
= (m*q + o) % 16;
f = abcd[1] + rol( abcd[0]+ fctn(abcd) + k[q+16*p] + mm.w[g], rotn[q%4]);
abcd[0] = abcd[3];
abcd[3] = abcd[2];
abcd[2] = abcd[1];
abcd[1] = f;
}
}
for (p=0; p<4; p++)
h[p] += abcd[p];
os += 64;
}
return h;
}
int main( int argc, char *argv[] )
{
FILE* file_ptr = fopen("md5.txt", "a+");
int j,k;
const char *msg = "This is a Network Security Project...";
printf("----------------------------------------------------\n");

15 | P a g e
printf("-------------Made by C codechamp--------------------\n");
printf("----------------------------------------------------\n\n");
printf("\t MD5 ENCRYPTION ALGORITHM IN C
\n\n"); int n1;
char inp[200];
printf("Enter the value of inputs you want to hash or encrypt\n"); scanf("%d",&n1);
int i1; for(int
i1=0;i1<n1;i1++){
scanf("%s",inp);

printf("Input String to be Encrypted using bcrypt :


\n\t%s",inp); unsigned *d = md5(inp, strlen(inp)); MD5union
u;
printf("\n\n\nThe bcrypt code for input string is :
\n"); printf("\t= 0x");
fprintf(file_ptr, "%s", "0x");
//fclose(file_ptr);

for (j=0;j<4; j++){


u.w = d[j];
for (k=0;k<4;k++){
fprintf(file_ptr, "%02x",u.b[k]);
printf("%02x",u.b[k]);
}
}
fprintf(file_ptr, "\n");
}
fclose(file_ptr);
printf("\n");
printf("\n\t bcrypt Encyption Successfully Completed!!!\n\n");
getch();
system("pause");
return 0;
}

16 | P a g e
PYTHON CODE:

bcrypt:

import bcrypt
def hash():
plaintext = str(input("Enter the string to be hashed:"))
b = bytes(plaintext, 'utf-8')
b = plaintext.encode('utf-8')
my_hashed_password = bcrypt.hashpw(b, bcrypt.gensalt())
print(my_hashed_password)
n=int(input("Enter the number of strings to be hashed:"))
for i in range (0,n):
hash()

4.3 Test Case

 BRITANIA123
 jamming
 bcrypt@99

4.1 Execution Snapshot

bcrypt:

17 | P a g e
Inputs followed by Outputs:
 BRITANIA123:
b'$2b$12$0rHOKqnxAZdgjinlJPprH.1/KmF97N9MNmM.1soOsYwZwYFSnuZZu'

 jamming:
b'$2b$12$NwNTGN8vNDLY3GvhesgJr.qRh0aWfgbS2J1n7oPIwnnchjLwbPuFy'

 bcrypt@99
b'$2b$12$S8TGpmN/EkZ1Wk5NYtIGR.PXc.KtNNpAmYzIrdrC3oVuDjGEx.euG'

MD5:

INPUTS Followed by Outputs:

 BRITANIA123 – 0xf3e273b6d01f5a460949d234ce46a128

 jamming – 0x5dbe54e8b84fbed546eee70532e52933

 bcrypt@99 – 0x35c62fe76505e55bec402a5149b2f22

18 | P a g e
Conclusion

So with a one-way hash password, a server does not store plain text
passwords to authenticate a user. A password has a hashing algorithm
applied to it to make it more secure. When it comes to encryption and
hashing faster is never better. bcrypt can expand what is called its Key
Factor to compensate for increasingly more-powerful computers and
effectively “slow down” its hashing speed.
Salting will increase the time needed to find other user's passwords. Because
the attacker would need to create a Rainbow Table for every salt used
because salts change the output in unpredictable ways. Cracking the hash
for one user wouldn't be much harder but cracking the hash for all users
would be exponentially harder. Strong salts should be used in
conjunction with strong passwords.
In Comparison to MD5, bcrypt hashes the password ‘yaaa’ in about 0.3
seconds on the other hand MD5 takes less than a microsecond. So as slower
the better bcrypt is preferable MD5. It is not vulnerable to rainbow tables
(since creating them is too expensive) and not even vulnerable to brute
force attacks. However, 11 years later, many are still using SHA2x with salt
for storing password hashes and bcrypt is not widely adopted.

Link to the video discussion of review:-

https://vimeo.com/426708486

19 | P a g e
References:

1. Wiemer, Friedrich & Zimmermann, Ralf. (2015). High-speed implementation of bcrypt


password search using special-purpose hardware. 2014 International Conference on
Reconfigurable Computing and FPGAs, ReConFig 2014.
10.1109/ReConFig.2014.7032529.
2. Bošnjak, Leon & Sres, J. & Brumen, B.. (2018). Brute-force and dictionary attack on
hashed real-world passwords. 1161-1166. 10.23919/MIPRO.2018.8400211.
3. Yiannis, Chrysanthou , Modern Password Cracking: A hands-on approach to creating an
optimised and versatile attack.
4. Raza, Mudassar & Iqbal, Muhammad & Sharif, Muhammad & Haider, Waqas. (2012). A
Survey of Password Attacks and Comparative Analysis on Methods for Secure
Authentication. World Applied Sciences Journal. 19. 439-444.
10.5829/idosi.wasj.2012.19.04.1837.

5. Madiraju, Tarun, "Dictionary Attacks and Password Selection" (2014). Thesis. Rochester
Institute of Technology.

6. Yiannis, Chrysanthou , Modern Password Cracking: A hands-on approach to creating an


optimised and versatile attack.
7. Techopedia – home/dictionary/tags/Security/Dictionary Attacks.
8. theEconomicTimes/definition/cryptography
9. learncryptography.com(attack-vectors/dictionary-attack)

10. learncryptography.com(attack-vectors/dictionary-attack)
11. cs.cmu.edu(lectures/Hashing)
12. https://www.usenix.org/legacy/publications/library/proceedings/usenix99/full_papers/pr
ovos/provos_html/node5.html
13. https://www.usenix.org/legacy/publications/library/proceedings/usenix99/full_papers/pr
ovos/provos_html/node4.html

14. http://www.faqs.org/rfcs/rfc1321.html

15. https://www.iusmentis.com/technology/hashfunctions/md5/

20 | P a g e

You might also like