{
	"id": "ad8b64c8-7f6f-4a01-9569-d134e5f1be6c",
	"created_at": "2026-04-06T00:17:43.203451Z",
	"updated_at": "2026-04-10T03:21:04.729871Z",
	"deleted_at": null,
	"sha1_hash": "6aaed1da3ed95ff43f80a65d25fb62c89a2d9d57",
	"title": "Writing a decryptor for Jaff ransomware",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 369819,
	"plain_text": "Writing a decryptor for Jaff ransomware\r\nPublished: 2023-02-14 · Archived: 2026-04-05 19:59:54 UTC\r\nOverview\r\nRecently, I’ve been trying to learn more about reverse engineering ransomware. Jaff is ransomware from a campaign dating\r\nback to 2017, and I was told that it had a vulnerability that would make it possible to write a decryptor. I analyzed a sample\r\nto see if I could rediscover the vulnerability myself.\r\nYou can find the sample I used on MalShare, and its SHA256 hash is\r\n0746594fc3e49975d3d94bac8e80c0cdaa96d90ede3b271e6f372f55b20bac2f .\r\nInitial Observations\r\nThe sample is a 32-bit PE excutable written in C++. The executable did not seem to import any functions related to\r\ncryptography, and it contained a very long chunk of encrypted data. This meant that the most important functions of this\r\nprogram were likely being decrypted dynamically.\r\nBy setting breakpoints on VirtualAlloc and VirtualProtect , I kept track of each time a RWX segment of memory was\r\nallocated. After several calls to VirtualAlloc and VirtualProtect , the program wrote a PE file to one of these segments,\r\nwhich I dumped from memory. This turned out to be the actual encryptor, and it’s what I’ll be focusing on for the remainder\r\nof my analysis.\r\nBehaviors\r\nWhen run, the sample calls itself Ffv opg me liysj sfssezhz :\r\nhttps://clairelevin.github.io/malware/2023/02/14/jaff.html\r\nPage 1 of 7\n\nAdditionally, a GET request is made to fkksjobnn43[.]org/a5/ . As I don’t have access to this C2 server, I have no way of\r\nknowing what was expected from this server or whether the encryption process would have proceeded differently if I’d been\r\nable to connect.\r\nStrings, Imports, and Resources\r\nThe binary I dumped from memory imports cryptography-related functions such as CryptEncrypt , CryptExportKey , and\r\nCryptGenKey , as well as file enumeration functions such as FindFirstFileW and FindNextFileW . This is how I knew I\r\nwas looking at the actual encryptor.\r\nAdditionally, there were several resources containing data used in the encryption process:\r\n#105 : The string representation of the numbers\r\n35326054031861368139563306184134167018130718569482731666001650817539108744401016633231304437224730790638615766740272106\r\nand\r\n35326054031861368139563306184134167018130718569482731666001650829864568371094444203557601170206844003631101722202233367\r\n#106 : The file extensions to encrypt:\r\n.xlsx .acd .pdf .pfx .crt .der .cad .dwg .MPEG .rar .veg .zip .txt .jpg .doc .wbk .mdb .vcf .docx .ics .vsc .mdf\r\n#109 : The ransom note in HTML form, with the string [ID5] in place of the victim’s decryption ID.\r\n#110 : The string .jaff , which is the extension appended to encrypted files.\r\n#111 : The URL fkksjobnn43[.]org/a5/ .\r\n#112 : The ransom note in text form, again with [ID5] in place of the ID.\r\nhttps://clairelevin.github.io/malware/2023/02/14/jaff.html\r\nPage 2 of 7\n\n#113 : A string of bytes which, when XORed with the second number in #106 , gives the strings ReadMe.txt ,\r\nReadMe.bmp , and ReadMe.html .\r\nAdditionally, the string cmd /C del /Q /F %s found in the program suggests that it is intended to delete itself once\r\nencryption is complete.\r\nThe Encryption Process\r\nThe sample uses 256-bit AES to encrypt files. For debugging purposes, I set a breakpoint on CryptImportKey to read the\r\nkey blob from memory:\r\nA new key is generated using CryptGenKey each time the program is run.\r\nBeginning with the root directory, the program enumerates all files and subdirectories and uses CryptEncrypt to AES\r\nencrypt each file. The program uses GetLogicalDrives to find all drives connected to the system, and encrypts all drives\r\nthat are not CD-ROM drives (possibly because a CD-ROM drive would make a noticeable noise as it started up).\r\nThe .jaff extension is appended to the encrypted file, and the AES-encrypted bytes are written. We can see that there are\r\nmultiple WriteFile calls to the encrypted file, revealing that something else is appended to the .jaff file before the\r\nencrypted data:\r\nThe appended value turned out to be the ASCII representation of a large number.\r\nAdditionally, the ransom note is dropped in each encrypted directory. The note is dropped in text, HTML, and image forms,\r\nwith file names of ReadMe.txt , ReadMe.html , and ReadMe.bmp respectively.\r\nhttps://clairelevin.github.io/malware/2023/02/14/jaff.html\r\nPage 3 of 7\n\nA new victim ID is generated each time the program is run.\r\nEncryption of the AES Key\r\nI suspected that the long number appended before the encrypted data in the .jaff files was likely an encryption of the\r\nAES key. A new AES key was generated for each victim, so the program would need some way to store it.\r\nRepresenting The Key Bytes\r\nI found that the AES key was being passed as an argument to sub_402d70 . When passed into this function, the AES key\r\nblob was being stored as a decimal representation in little-endian format, with each decimal digit being stored as a 16-bit\r\ninteger. Each byte of the key blob was converted to three decimal digits; for instance, 08 would be stored as 008 and 8A\r\nwould be stored as 138 . Additionally, the digit “1” was appended to the sequence:\r\nFor example, during one run of the program, the original AES key blob was the following:\r\n08 02 00 00 10 66 00 00 20 00 00 00 52 8A A4 D0 46 E3 4F FE E8 C6 A0 F5 91 0C 25 81 03 0E 5C 3C 57 F6 A0 43 08 32 C9 83 2\r\nIt was stored as the sequence of bytes\r\n04 00 04 00 00 00 01 00 03 00 01 00 01 00 00 00 02 00 00 00 05 00 00 00 08 00 00 00 00 00 07 00 06 00 00 00 00 00 06 00 0\r\nwhich corresponds to the number\r\nhttps://clairelevin.github.io/malware/2023/02/14/jaff.html\r\nPage 4 of 7\n\n1008002000000016102000000032000000000082138164208070227079254232198160245145012037129003014092060087246160067008050201131\r\nTo convert this representation back into bytes, I used the following function:\r\ndef convert_from_decimal(s):\r\nresult = b''\r\ns_fixed = s[1:]\r\nfor i in range(0, len(s_fixed) ,3):\r\ncurr_num = s_fixed[i:i+3]\r\nresult += int(curr_num).to_bytes(1, 'little')\r\nreturn result\r\nEncrypting The Key\r\nAt this point, it was time to look at what sub_402d70 was actually doing. The arguments to the function were the AES key,\r\nan array of bytes that were either 1 or 0, and the decimal representation of the number\r\n35326054031861368139563306184134167018130718569482731666001650829864568371094444203557601170206844003631101722202233367975968\r\nNote that this is one of the two numbers that appeared in resource #105 .\r\nBy experimenting with this subrouting in a debugger, I found that the program was calling functions that performed\r\nmultiplication and division on arbitrarily large numbers. Sepecifically, the AES key was being squared over and over, and\r\nsomething different was done with the result based on the values in the array of 1s and 0s.\r\nThis proved to be the repeated-squaring method for modular exponentiation. The AES key was being raised to an exponent,\r\nwhich was passed as an argument in binary form in order to aid in the repeated-squaring algorithm. The modulus was the\r\nlong number stored in the resource.\r\nThe use of modular exponentiation immediately suggested that RSA was being used. Normally, this would mean we\r\nwouldn’t be able to decrypt the AES key, as we need the private key for that.\r\nHowever, resource #105 contains two numbers, and we’ve only used one so far. One of them is the public modulus n, and\r\nthe other number is very close to it. It seemed possible that the second number was phi(n), which is needed to compute the\r\nprivate exponent d from the public exponent e. I wrote the following script to test it:\r\nhttps://clairelevin.github.io/malware/2023/02/14/jaff.html\r\nPage 5 of 7\n\ndef rsa_decrypt(msg, e, n, phi_n):\r\nd = pow(e, -1, phi_n)\r\nreturn pow(msg, d, n)\r\nSure enough, passing in the second number as phi(n) returned the decrypted AES key! Since the RSA key was hard-coded,\r\nthis meant that we had enough information to write a decryptor for any files encrypted with this sample, even if the AES key\r\nchanged each time.\r\nThe Public Exponent\r\nTo generate the private exponent for the decryptor, I not only needed phi(n), but also the public exponent. However, the\r\nprogram generated a new public exponent each time it was run.\r\nUpon closer inspection, I found that the public exponent was usually close to the victim ID given in the ransom note.\r\nSometimes they matched exactly, but sometimes the exponent was slightly more than the ID, and occasionally they didn’t\r\nseem to match at all.\r\nEventually, I found that the victim ID seemed to be randomly generated. If a negative number was generated, the bits were\r\nnegated in order to produce a positive result.\r\nAfter correcting for this, I found that either the victim ID or its negation was always close to the exponent, but there didn’t\r\nseem to be much of a pattern to the exact difference.\r\nIt turned out that the victim ID sometimes needed to be modified before it could work as a public exponent. In RSA, the\r\npublic exponent needs to be invertible modulo phi(n), meaning that the exponent and phi(n) need to be relatively prime.\r\nHowever, the process that generated the victim IDs did not guarantee a result that was relatvely prime to phi(n).\r\n(This is just speculation, but my guess is that this is why phi(n) was hard-coded in the executable - they needed to guarantee\r\nthat they had a valid public exponent, so they had to check whether the ID and phi(n) were relatively prime. However, this\r\nalso gives us enough information to decrypt the files ourselves!)\r\nBy incrementing the victim ID until I got a number that was relatively prime to phi(n), I managed to retrieve the public\r\nexponent.\r\ndef get_relatively_prime(e, phi_n):\r\n while(math.gcd(e, phi_n) != 1):\r\n e += 2\r\n return e\r\nPutting It All Together\r\nWe now have enough information to write a decryptor that decrypts the victim’s files using only the encrypted .jaff file\r\nand the ID number in the ransom note.\r\nimport binascii\r\nimport math\r\nfrom Crypto.Cipher import AES\r\nfrom struct import pack, unpack\r\nphi_n = 353260540318613681395633061841341670181307185694827316660016508175391087444010166332313044372247307906386157667402\r\nn = 3532605403186136813956330618413416701813071856948273166600165082986456837109444420355760117020684400363110172220223336\r\nhttps://clairelevin.github.io/malware/2023/02/14/jaff.html\r\nPage 6 of 7\n\ndef convert_from_decimal(s):\r\nresult = b''\r\ns_fixed = s[1:]\r\nfor i in range(0, len(s_fixed) ,3):\r\ncurr_num = s_fixed[i:i+3]\r\nresult += int(curr_num).to_bytes(1, 'little')\r\nreturn result\r\ndef rsa_decrypt(msg, e, n, phi_n):\r\nd = pow(e, -1, phi_n)\r\nreturn pow(msg, d, n)\r\ndef get_relatively_prime(e, phi_n):\r\n while(math.gcd(e, phi_n) != 1):\r\n e += 2\r\n return e\r\ndef aes_decrypt(ciphertext, blob):\r\niv = b'\\x00'*16\r\nkey_bytes = blob[12:]\r\nkey = AES.new(key_bytes, AES.MODE_CBC, iv)\r\npadded_text = ciphertext + b'\\x00'*(16 - len(ciphertext)%16)\r\nreturn key.decrypt(padded_text)\r\ndef decrypt(filename, id):\r\n#parse the encrypted AES key and data from the file\r\nenc_file = open(filename, 'rb').read()\r\nnum_size = unpack('\u003cI', enc_file[0:4])[0]\r\nkey_str = enc_file[4:num_size+4]\r\nciphertext = enc_file[num_size+8:]\r\nkeys = [int(i) for i in key_str.split()]\r\naes_key = []\r\n#test both the victim ID and its negation for a valid public exponent\r\nexp1 = get_relatively_prime(id | 1, phi_n)\r\nfor k in keys: aes_key.append(rsa_decrypt(k, exp1, n, phi_n))\r\nif(str(aes_key[0]))[0:6] != '100800':\r\naes_key = []\r\nnot_id = ~id \u0026 0xffffffff\r\nexp2 = get_relatively_prime(not_id | 1, phi_n)\r\nfor k in keys: aes_key.append(rsa_decrypt(k, exp2, n, phi_n))\r\n#decode the key blob from its decimal representation\r\naes_key_bytes = b''\r\nfor k in aes_key: aes_key_bytes += convert_from_decimal(str(k))\r\nreturn aes_decrypt(ciphertext, aes_key_bytes)\r\nSource: https://clairelevin.github.io/malware/2023/02/14/jaff.html\r\nhttps://clairelevin.github.io/malware/2023/02/14/jaff.html\r\nPage 7 of 7",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://clairelevin.github.io/malware/2023/02/14/jaff.html"
	],
	"report_names": [
		"jaff.html"
	],
	"threat_actors": [],
	"ts_created_at": 1775434663,
	"ts_updated_at": 1775791264,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/6aaed1da3ed95ff43f80a65d25fb62c89a2d9d57.pdf",
		"text": "https://archive.orkl.eu/6aaed1da3ed95ff43f80a65d25fb62c89a2d9d57.txt",
		"img": "https://archive.orkl.eu/6aaed1da3ed95ff43f80a65d25fb62c89a2d9d57.jpg"
	}
}