제퍼넷 로고

심각한 보안: KePass의 "마스터 비밀번호 크랙"과 이를 통해 배울 수 있는 것

시간

지난 XNUMX주 동안 인기 있는 오픈 소스 암호 관리자인 KeePass에서 "마스터 암호 크랙"이라고 설명된 일련의 기사를 보았습니다.

이 버그는 공식 미국 정부 식별자를 얻을 만큼 충분히 중요한 것으로 간주되었습니다. CVE-2023-32784, 그것을 찾고 싶다면), 암호 관리자의 마스터 암호가 전체 디지털 성의 열쇠라는 점을 감안할 때 이야기가 많은 흥분을 불러일으킨 이유를 이해할 수 있습니다.

좋은 소식은 이 버그를 악용하려는 공격자는 컴퓨터를 이미 맬웨어로 감염시켰을 가능성이 거의 확실하므로 어쨌든 키 입력과 실행 중인 프로그램을 감시할 수 있다는 것입니다.

즉, 이 버그는 KeePass의 제작자가 업데이트를 내놓을 때까지 쉽게 관리할 수 있는 위험으로 간주될 수 있으며, 업데이트는 곧 나타날 것입니다(2023년 XNUMX월 초, 분명히).

버그를 공개한 사람이 다음을 처리하므로 지적:

강력한 암호로 전체 디스크 암호화를 사용하고 시스템에 [맬웨어가 없는 경우] 괜찮을 것입니다. 이 발견만으로는 아무도 인터넷을 통해 원격으로 비밀번호를 훔칠 수 없습니다.

위험 설명

크게 요약하면 버그는 기밀 데이터의 모든 추적을 완료한 후에 메모리에서 제거되도록 보장하는 어려움으로 귀결됩니다.

메모리에 비밀 데이터가 전혀, 심지어 잠시라도 있는 것을 피하는 방법에 대한 문제는 여기에서 무시할 것입니다.

이 기사에서 우리는 보안에 민감한 리뷰어가 승인한 코드가 "자체 이후에 올바르게 정리되는 것 같습니다"와 같은 주석을 사용하여 코드를 승인했음을 모든 곳의 프로그래머에게 상기시키고자 합니다.

...실제로 완전히 정리되지 않을 수 있으며 잠재적인 데이터 유출은 코드 자체에 대한 직접적인 연구에서 분명하지 않을 수 있습니다.

간단히 말해서 CVE-2023-32784 취약점은 KeyPass 프로그램이 종료된 후에도 시스템 데이터에서 KeePass 마스터 비밀번호를 복구할 수 있음을 의미합니다. 잠시 후 켜짐) 시스템 스왑 또는 절전 파일에 남아 할당된 시스템 메모리가 나중을 위해 저장될 수 있습니다.

시스템이 꺼져 있을 때 BitLocker가 하드 디스크를 암호화하는 데 사용되지 않는 Windows 컴퓨터에서는 노트북을 훔친 도둑이 USB 또는 CD 드라이브에서 부팅하고 마스터 암호를 복구할 수 있는 기회를 제공합니다. KeyPass 프로그램 자체는 디스크에 영구적으로 저장하지 않도록 주의합니다.

메모리에 장기간 암호가 누출된다는 것은 이론적으로 암호를 입력한 후 오랜 시간이 지난 후, 그리고 KeePass가 실행된 후 오랜 시간이 지난 후에도 해당 덤프가 확보된 경우에도 이론적으로 KeyPass 프로그램의 메모리 덤프에서 암호를 복구할 수 있음을 의미합니다. 그 자체는 더 이상 그것을 유지할 필요가 없었습니다.

확실히 시스템에 이미 있는 맬웨어는 입력할 때 활성화되어 있는 한 다양한 실시간 스누핑 기술을 통해 거의 모든 입력된 암호를 복구할 수 있다고 가정해야 합니다. 그러나 위험에 노출된 시간이 몇 분, 몇 시간 또는 며칠이 지난 후 또는 아마도 컴퓨터를 종료한 후를 포함하여 그 이상으로 연장되지 않고 짧은 타이핑 시간으로 제한될 것이라고 합리적으로 예상할 수 있습니다.

무엇이 남을까요?

따라서 우리는 비밀 데이터가 코드에서 직접적으로 드러나지 않는 방식으로 메모리에 남겨질 수 있는 방법에 대해 높은 수준에서 살펴볼 것이라고 생각했습니다.

당신이 프로그래머가 아니더라도 걱정하지 마십시오. 간단하게 설명할 것입니다.

다음을 수행하여 암호 입력 및 임시 저장을 시뮬레이트하는 간단한 C 프로그램에서 메모리 사용 및 정리를 살펴보는 것으로 시작하겠습니다.

  • 전용 메모리 청크 할당 특별히 암호를 저장합니다.
  • 알려진 텍스트 문자열 삽입 필요한 경우 메모리에서 쉽게 찾을 수 있습니다.
  • 16개의 의사 난수 8비트 ASCII 문자 추가 범위 AP에서.
  • 인쇄 시뮬레이션된 비밀번호 버퍼.
  • 메모리 확보 비밀번호 버퍼를 삭제하기 위해.
  • 종료 프로그램.

크게 단순화된 C 코드는 C 런타임 함수에서 품질이 낮은 의사 난수를 사용하여 오류 검사 없이 다음과 같이 보일 수 있습니다. rand(), 그리고 버퍼 오버플로 검사를 무시합니다(실제 코드에서는 이 작업을 절대 수행하지 마십시오!).

 // Ask for memory char* buff = malloc(128); // Copy in fixed string we can recognise in RAM strcpy(buff,"unlikelytext"); // Append 16 pseudo-random ASCII characters for (int i = 1; i <= 16; i++) { // Choose a letter from A (65+0) to P (65+15) char ch = 65 + (rand() & 15); // Modify the buff string directly in memory strncat(buff,&ch,1); } // Print it out, so we're done with buff printf("Full string was: %sn",buff); // Return the unwanted buffer and hope that expunges it free(buff);

실제로 우리가 테스트에서 마지막으로 사용한 코드에는 아래와 같은 몇 가지 추가 비트와 조각이 포함되어 있어 원하지 않거나 남은 콘텐츠를 찾기 위해 임시 비밀번호 버퍼의 전체 콘텐츠를 사용할 때 덤프할 수 있습니다.

호출 후 의도적으로 버퍼를 덤프합니다. free(), 이것은 기술적으로 use-after-free 버그이지만 여기서는 버퍼를 다시 건네준 후 중요한 것이 남아 있는지 확인하는 비열한 방법으로 여기에서 수행하고 있습니다. 이는 실생활에서 위험한 데이터 누출 구멍으로 이어질 수 있습니다.

우리는 또한 두 개를 삽입했습니다 Waiting for [Enter] 프로그램의 주요 지점에서 메모리 덤프를 생성할 수 있는 기회를 주기 위해 코드에 프롬프트를 표시하고 프로그램이 실행될 때 남겨진 것을 확인하기 위해 나중에 검색할 원시 데이터를 제공합니다.

메모리 덤프를 수행하기 위해 Microsoft Sysinternals 도구 procdump 와 더불어 -ma 옵션 (모든 메모리 덤프), Windows를 사용하기 위해 자체 코드를 작성할 필요가 없습니다. DbgHelp 시스템과 그 다소 복잡한 MiniDumpXxxx() 기능.

C 코드를 컴파일하기 위해 우리는 Fabrice Bellard의 무료 및 오픈 소스의 작고 간단한 자체 빌드를 사용했습니다. 작은 C 컴파일러, 64비트 Windows에서 사용 가능 소스 및 이진 형식 GitHub 페이지에서 직접.

기사에 나오는 모든 소스 코드의 복사 및 붙여넣기 가능한 텍스트가 페이지 하단에 나타납니다.

이것은 우리가 테스트 프로그램을 컴파일하고 실행했을 때 일어난 일입니다.

C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Tiny C Compiler - Copyright (C) 2001-2023 Fabrice Bellard Stripped down by Paul Ducklin for use as a learning tool 버전 ​​petcc64-0.9.27 [0006] - 64비트 생성 PE 전용 -> unl1.c -> c:/users/duck/tcc/petccinc/stdio.h [. . . .] -> c:/users/duck/tcc/petcclib/libpetcc1_64.a -> C:/Windows/system32/msvcrt.dll -> C:/Windows/system32/kernel32.dll -------- -------- virt 파일 크기 섹션 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ----------------------- <- unl1.exe (3584 바이트) C:UsersduckKEYPASS> unl1.exe 시작 시 '새' 버퍼 덤프 00F51390: 90 57 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .W......P....... 00F513A0: 73 74 65 6D 33 32 5C 63 6D 64 2E 65 78 65 00 44 줄기32cmd. exe.D 00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win 00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr 00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData 00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_ 00F513 0F42: 52 4 57F 53 45 52 5 41F 50 50 5 50F 52 4 46F 00 BROWSER_APP_PROF 51400F49: 4 45C 5 53F 54 52 49 4 47E 3 49D 6 74E 65 72 00 ILE_STRING=인터 51410F6: 65E 74 20 45 78 70 6 7C 56A 4 F3 4C AC 00B 00 00 net ExplzV.< .K.. 전체 문자열은 다음과 같습니다 A 51390 75 6E 6 69 6 65 EJJCPOMDJHAN.eD 6F79B74 : 65 78 74 4 48 4 4 00 513 0D 45 4A 4C 43 50 4E riverData=C:Win 4F44C4: 48 41F 4 00 65C 00 44 00 513 0 72D 69 76 65C 72 44 dowsSystem61Dr 74F61D3: 43 3 5 57 69 6C 00 513 0 64 6 77 73 5 53 79 iversDriverData 73F74E65: 6 33 32 5 44F 72 32 00 513 0D 69 76 65 72 73 5F .EFC_44=72.FPS_ 69F76F65: 72 44 61F 74 61 00 513 0F 00 45 46 43F 5 34 33F 37 BROWSER_APP_PROF 32F3: 31 00C 46 50F 53 5 4372 1 00E 513 0D 42 52E 4 57 53 ILE_STRING=인터 45F52: 5E 41 50 50 5 50 52 4C 46A 00 F51400 49C AC 4B 45 5 순 ExplzV.<.K.. 버퍼를 해제하기 위해 [ENTER]를 기다리는 중... free() 후 버퍼 덤프 중() 53F54: A52 49 F4 47 3 49 6 74 65 72 F00 51410 6 65 74 20 .g......P...... . 45F78A70: 6 7A 56A 4 3 4F 00D 00 00A 51390 0 67E 5 00 00 00 00 00 50E riverData=C:승리 01F5C00: 00 00F 00 00 00C 513 0 45 4 4 43D 50 4 4C 44 4 dowsSystem48Dr 41F4D00: 65 00 44 00 513 0C 72 69 76 65 72 44 61 74 61 3 iversDriverData 43F3E5: 57 69 6 00 513 0F 64 6 77 73 5D 53 79 73 74 65 6F .EFC_33=32.FPS_ 5F44F72: 32 00 513F 0 69 76 65 72F 73 5 44 72F 69 76 65F 72 BROWSER_APP_PROF 44F61: 74 61C 00 513F 0 00 45 46 43E 5 34D 33 37E 32 3 31 ILE_STRING=인터 00F46: 50E 53 5 4372 1 00 513 0C 42D 52 4 57D AC 53B 45 52 순 ExplM..MK. main()을 종료하기 위해 [ENTER]를 기다리는 중... C:UsersduckKEYPASS>

이 실행에서는 프로세스 메모리 덤프를 가져오지 않아도 됩니다. 출력에서 ​​이 코드가 데이터를 유출한다는 것을 바로 알 수 있기 때문입니다.

Windows C 런타임 라이브러리 함수 호출 직후 malloc(), 우리가 반환하는 버퍼에는 프로그램의 시작 코드에서 남겨진 환경 변수 데이터처럼 보이는 것이 포함되어 있으며 처음 16바이트는 일종의 남은 메모리 할당 헤더처럼 보이도록 분명히 변경된 것을 볼 수 있습니다.

(이 16바이트가 두 개의 8바이트 메모리 주소처럼 보이는 방식에 유의하십시오. 0xF557900xF50150, 각각 메모리 버퍼 바로 뒤와 바로 앞에 있습니다.)

암호가 메모리에 있다고 가정하면 예상대로 버퍼에서 전체 문자열을 명확하게 볼 수 있습니다.

하지만 전화를 한 후 free(), 메모리 할당자가 재사용할 수 있는 메모리의 블록을 추적할 수 있도록 버퍼의 처음 16바이트가 다시 한 번 가까운 메모리 주소처럼 보이는 것으로 어떻게 다시 작성되었는지 확인하십시오.

… 하지만 "말소된" 암호 텍스트의 나머지 부분(마지막 12개의 임의 문자 EJJCPOMDJHAN) 남겨졌습니다.

C에서 자체 메모리 할당 및 할당 해제를 관리해야 할 뿐만 아니라 데이터 버퍼를 정확하게 제어하려면 데이터 버퍼에 적합한 시스템 기능을 선택해야 합니다.

예를 들어 대신 이 코드로 전환하면 메모리에 있는 항목을 좀 더 제어할 수 있습니다.

에서 전환하여 malloc()free() 하위 수준 Windows 할당 기능을 사용하려면 VirtualAlloc()VirtualFree() 직접, 우리는 더 나은 제어를 얻습니다.

그러나 우리는 속도에 대한 대가를 지불합니다. VirtualAlloc() 호출하는 것보다 더 많은 작업을 수행합니다. malloc(), 사전 할당된 저수준 메모리 블록을 지속적으로 나누고 세분화하여 작동합니다.

사용 VirtualAlloc() 작은 블록에 대해 반복적으로 또한 전체적으로 더 많은 메모리를 사용합니다. VirtualAlloc() 일반적으로 4KB의 배수 메모리를 사용합니다. 대용량 메모리 페이지), 위의 128바이트 버퍼는 4096바이트로 반올림되어 3968KB 메모리 블록의 끝에서 4바이트를 낭비합니다.

그러나 보시다시피 우리가 되찾은 메모리는 자동으로 비워져(XNUMX으로 설정됨) 이전에 무엇이 있었는지 볼 수 없으며 이번에는 use-after-free를 시도할 때 프로그램이 충돌합니다. 트릭, Windows는 우리가 더 이상 소유하지 않는 메모리를 엿보려고 시도하고 있음을 감지하기 때문입니다.

C:UsersduckKEYPASS> unl2 시작 시 '신규' 버퍼 덤프 0000000000 0000 00 00 00 00 00 00 00 00 00 00 00 00 .. .............. 00EA00: 00 00 0000000000 0010 00 00 00 00 00 00 00 00 00 00 00 00 ............ 00EA00: 00 00 0000000000 0020 00 00 00 00 00 00 00 00 00 00 00 00 ............ 00EA00: 00 00 0000000000 0030 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0040 00 00 00 00 00 00 00 00 00 00 00 00 ...... 00EA00: 00 00 0000000000 0050 00 00 00 00 00 00 00 00 00 00 00 00 ..... 00EA00: 00 00 0000000000 0060 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 전체 문자열은 다음과 같습니다 00 00F 00 0000000000F 0070 00 00C 00C 00C 00 00 00 00 JPPHEOPOIDLL .... 00EA00 : 00 00 00 00 00 00 0000000000 0080 00 00 00 00 00 00 00 ................ 00 : 00 00 00 00 00 00 00 00 0000000000 0000 75 6 6 69 6 65 ............ 6EA79: 74 65 78 74 49 42 49 50 0000000000 0010 4 50 50 48 45 4 ...... 50EA4: 49 44 4 4 00 00 00 00 0000000000 0020 00 00 00 00 00 00 ............... ... 00 00 00 00 ............ 00EA00: 00 00 00 00 0000000000 0030 00 00 00 00 00 00 00 00 00 00 ...... ... 버퍼를 해제하기 위해 [ENTER]를 기다리는 중... free() 00EA00 이후 버퍼 덤프 중: [Windows가 우리의 use-after-free를 포착했기 때문에 여기에서 프로그램이 종료되었습니다.]

확보한 메모리는 재할당이 필요하기 때문에 VirtualAlloc() 다시 사용할 수 있기 전에 재활용되기 전에 XNUMX이 될 것이라고 가정할 수 있습니다.

그러나 공백인지 확인하려면 특수 Windows 기능을 호출할 수 있습니다. RtlSecureZeroMemory() 해제하기 직전에 Windows가 먼저 버퍼에 XNUMX을 쓰도록 보장합니다.

관련 기능 RtlZeroMemory(), 궁금하다면 비슷한 일을 하지만 실제로 작동한다는 보장은 없습니다. 왜냐하면 컴파일러는 나중에 버퍼가 다시 사용되지 않는다는 것을 알게 되면 이론적으로 중복된 것으로 제거할 수 있기 때문입니다.

보시다시피, 메모리에 저장된 비밀이 나중을 위해 남아 있을 수 있는 시간을 최소화하려면 올바른 Windows 기능을 사용하는 데 상당한 주의를 기울여야 합니다.

이 기사에서는 암호를 물리적 RAM에 잠가서 실수로 스왑 파일에 비밀이 저장되는 것을 방지하는 방법을 살펴보지 않을 것입니다. (힌트: VirtualLock() 실제로 그 자체로는 충분하지 않습니다.) 낮은 수준의 Windows 메모리 보안에 대해 자세히 알고 싶은 경우 의견을 통해 알려주십시오. 향후 기사에서 살펴보겠습니다.

자동 메모리 관리 사용

스스로 메모리를 할당, 관리 및 할당 해제하지 않아도 되는 깔끔한 방법 중 하나는 다음을 처리하는 프로그래밍 언어를 사용하는 것입니다. malloc()free()VirtualAlloc()VirtualFree()자동으로

와 같은 스크립팅 언어 , Python, 루아, 자바 스크립트 다른 사람들은 백그라운드에서 메모리 사용량을 추적하여 C 및 C++ 코드를 괴롭히는 가장 일반적인 메모리 보안 버그를 제거합니다.

앞에서 언급했듯이 위의 잘못 작성된 샘플 C 코드는 현재 잘 작동하지만 여전히 고정 크기 데이터 구조를 사용하는 매우 간단한 프로그램이기 때문에 검사를 통해 128- 바이트 버퍼로 시작하는 실행 경로는 하나뿐입니다. malloc() 해당하는 것으로 끝납니다. free().

그러나 가변 길이 암호 생성을 허용하도록 업데이트하거나 생성 프로세스에 추가 기능을 추가하면 우리(또는 다음에 코드를 유지하는 사람)는 쉽게 버퍼 오버플로, use-after-free 버그 또는 메모리 문제로 끝날 수 있습니다. 절대 해제되지 않으므로 더 이상 필요하지 않은 후에도 오랫동안 비밀 데이터를 남겨둡니다.

Lua와 같은 언어에서는 전문 용어로 알려진 것을 다음과 같이 수행하는 Lua 런타임 환경을 허용할 수 있습니다. 자동 가비지 수집, 시스템에서 메모리를 획득하고 사용을 중지했음을 감지하면 반환합니다.

위에 나열된 C 프로그램은 메모리 할당 및 할당 해제가 처리될 때 훨씬 더 간단해집니다.

문자열을 저장할 메모리를 할당합니다. s 단순히 문자열을 할당하여 'unlikelytext' 그것.

우리는 나중에 루아에게 우리가 더 이상 관심이 없다는 것을 명시적으로 암시할 수 있습니다. s 값을 할당하여 nil (모든 nils 본질적으로 동일한 Lua 개체) 또는 사용 중지 s Lua가 더 이상 필요하지 않음을 감지할 때까지 기다립니다.

어느 쪽이든, 사용하는 메모리 s 결국 자동으로 복구됩니다.

그리고 텍스트 문자열에 추가할 때 버퍼 오버플로 또는 잘못된 크기 관리를 방지하기 위해(Lua 연산자 ..발음 연결, 본질적으로 다음과 같이 두 개의 문자열을 함께 추가합니다. + Python에서) 문자열을 확장하거나 줄일 때마다 Lua는 기존 메모리 위치에서 원래 문자열을 수정하거나 교체하는 대신 완전히 새로운 문자열을 위한 공간을 마술처럼 할당합니다.

이 접근 방식은 느리고 텍스트 조작 중에 할당된 중간 문자열로 인해 C에서 얻을 수 있는 것보다 더 높은 메모리 사용량 피크를 초래하지만 버퍼 오버플로와 관련하여 훨씬 더 안전합니다.

하지만 이런 종류의 자동 문자열 관리(전문 용어로는 불변성, 문자열은 절대 얻지 못하기 때문에 돌연변이, 또는 생성된 후 제자리에서 수정됨) 자체적으로 새로운 사이버 보안 골칫거리를 가져옵니다.

프로그램이 종료되기 직전에 두 번째 일시 중지까지 Windows에서 위의 Lua 프로그램을 실행했습니다.

C:UsersduckKEYPASS> lua s1.lua 전체 문자열: unknowntextHLKONBOJILAGLNLN 문자열을 해제하기 전에 [ENTER]를 기다리는 중... 종료하기 전에 [ENTER]를 기다리는 중...

이번에는 다음과 같이 프로세스 메모리 덤프를 수행했습니다.

C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - Sysinternals 프로세스 덤프 유틸리티 Copyright (C) 2009-2022 Mark Russinovich 및 Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] 덤프 1 시작됨: C:UsersduckKEYPASSlua-s1.dmp [00:00:00] 덤프 1 쓰기: 예상 덤프 파일 크기는 10MB입니다. [00:00:00] 덤프 1 완료: 10초 동안 0.1MB 기록 [00:00:01] 덤프 수에 도달했습니다.

그런 다음 이 간단한 스크립트를 실행하여 덤프 파일을 다시 읽고 메모리의 모든 곳에서 알려진 문자열을 찾습니다. unlikelytext 나타나서 덤프 파일에서의 위치 및 바로 뒤에 오는 ASCII 문자와 함께 출력합니다.

이전에 스크립트 언어를 사용했거나 소위 말하는 프로그래밍 생태계에서 일한 적이 있더라도 관리되는 문자열, 여기서 시스템은 사용자를 위해 메모리 할당 및 할당 해제를 추적하고 적절하다고 판단되는 대로 처리합니다...

…이 메모리 스캔이 생성하는 출력을 보고 놀랄 수도 있습니다.

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: 아닌 것 같은 텍스트ALJBNGOAPLLBDEB 006D8B3C: 가능성 없는 텍스트ALJBNGOA 006D8B7C: 가능성 없는 텍스트ALJBNGO 006D8BFC: 가능성 없는 텍스트ALJBNGOAPLLBDEBJ 006D8CBC: 가능성 없는 텍스트ALJBN 006D8D7C: 가능성 없는 텍스트 JBNGOAP 006D903C: 있을 법하지 않은 텍스트ALJBNGOAPL 006D90BC: 있을 법하지 않은텍스트ALJBNGOAPLL 006D90FC: 있을 법하지 않은텍스트ALJBNG 006D913C: 있을 법하지 않은텍스트ALJBNGOAPLLB 006D91BC: 있을 법하지 않은텍스트ALJB 006D91FC: 있을 법하지 않은텍스트ALJBNGOAPLLBD 006D923C : 있을 법하지 않은 텍스트ALJBNGOAPLLBDE 006DB70C: 있을 법하지 않은텍스트ALJ 006DBB8C: 있을 법하지 않은텍스트AL 006DBD0C: 있을 법하지 않은텍스트A

보라, 우리가 메모리 덤프를 잡았을 때, 비록 우리가 문자열로 끝냈지만 s (그리고 Lua에게 더 이상 필요하지 않다고 말했습니다. s = nil), 그 과정에서 코드가 생성한 모든 문자열은 아직 복구되거나 삭제되지 않은 채 여전히 RAM에 존재했습니다.

실제로 RAM에 나타나는 순서를 따르지 않고 문자열 자체로 위의 출력을 정렬하면 한 번에 한 문자를 암호 문자열에 연결하는 루프 중에 발생한 일을 그릴 수 있습니다.

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | 정렬 /+10 006DBD0C: 있을 법하지 않은 텍스트A 006DBB8C: 있을 법하지 않은 텍스트AL 006DB70C: 있을 법하지 않은 텍스트ALJ 006D91BC: 있을 법하지 않은텍스트ALJB 006D8CBC: 있을 법하지 않은텍스트ALJBN 006D90FC: 있을 법하지 않은텍스트ALJBNG 006D8B7C: 있을 법하지 않은텍스트ALJBNGO 006D8B3C: 있을 법하지 않은텍스트ALJBNGOA 006D8D 7C: 있을 법하지 않은텍스트ALJBNGOAP 006D903C: 있을 법하지 않은텍스트ALJBNGOAPL 006D90BC: 있을 법하지 않은텍스트ALJBNGOAPLB 006D913C: 있을 것 같지 않은텍스트ALJBNGOAPLLB 006D91FC: 있을 것 같지 않은텍스트ALJBNGOAPLBD 006D923C: 있을 법하지 않은텍스트ALJBNGOAPLLBDE 006D8AFC: 있을 법하지 않은텍스트ALJBNGOAPLBDEB 006D 8BFC : 있을 법하지 않은텍스트ALJBNGOAPLLBDEBJ

모든 임시 중간 문자열은 여전히 ​​존재하므로 최종 값을 성공적으로 삭제하더라도 s, 우리는 여전히 마지막 문자를 제외한 모든 것을 유출하고 있을 것입니다.

사실 이 경우에는 특수한 Lua 함수를 호출하여 불필요한 데이터를 모두 폐기하도록 의도적으로 프로그램을 강제한 경우에도 collectgarbage() (대부분의 스크립팅 언어에는 비슷한 것이 있습니다.) 성가신 임시 문자열에 있는 대부분의 데이터는 어쨌든 RAM에 남아 있습니다. malloc()free().

즉, Lua 자체가 임시 메모리 블록을 다시 사용하기 위해 회수한 후에도 우리는 이러한 메모리 블록이 언제 어떻게 재사용되는지 제어할 수 없었습니다. 스니핑, 덤프 또는 다른 방식으로 유출되기를 기다리는 데이터에 대해.

.NET 입력

하지만 이 기사가 시작된 KeePass는 어떻습니까?

KeePass는 C#으로 작성되었으며 .NET 런타임을 사용하므로 C 프로그램에서 발생하는 메모리 관리 오류 문제를 피할 수 있습니다…

...하지만 C#은 Lua가 관리하는 것과 달리 자체 텍스트 문자열을 관리하므로 다음과 같은 의문이 생깁니다.

프로그래머가 작업을 마친 후 전체 마스터 암호를 한 곳에 저장하는 것을 피하더라도 메모리 덤프에 액세스할 수 있는 공격자는 그럼에도 불구하고 마스터 암호를 추측하거나 복구할 수 있는 충분한 남은 임시 데이터를 찾을 수 있습니다. 공격자는 ?에 암호를 입력한 후 몇 분, 몇 시간 또는 며칠 후에 컴퓨터에 액세스할 수 있습니다.

간단히 말해서, 마스터 암호가 말소될 것으로 예상한 후에도 RAM에 남아 있는 탐지 가능한 유령 같은 잔해가 있습니까?

귀찮게도 Github 사용자로서 Vdohney 발견에 대한 답변(적어도 2.54 이전의 KeePass 버전의 경우)은 "예"입니다.

분명히 말씀드리자면 작성자가 전체 암호를 저장하지 않도록 방해가 되지 않는 마스터 암호 입력을 위한 특수 기능을 만들었기 때문에 KeePass 메모리 덤프에서 실제 마스터 암호를 단일 텍스트 문자열로 복구할 수 있다고 생각하지 않습니다. 쉽게 발견하고 스니핑할 수 있는 암호.

우리는 마스터 비밀번호를 다음과 같이 설정하여 만족했습니다. SIXTEENPASSCHARS, 입력한 다음 메모리 덤프를 즉시, 짧게, 오랫동안 가져옵니다.

다음과 같이 8비트 ASCII 형식과 16비트 UTF-16(Windows widechar) 형식 모두에서 암호 텍스트를 찾는 간단한 Lua 스크립트로 덤프를 검색했습니다.

결과는 고무적이었습니다.

C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp 덤프 파일 읽기... 완료. SIXTEENPASSCHARS를 8비트 ASCII로 검색하는 중... 찾을 수 없습니다. SIXTEENPASSCHARS를 UTF-16으로 검색하는 중... 찾을 수 없습니다.

그러나 CVE-2023-32784의 발견자인 Vdohney는 마스터 암호를 입력할 때 KeePass가 유니코드 "blob" 문자로 구성된 자리 표시자 문자열을 생성하고 표시하여 시각적 피드백을 제공한다는 사실을 알아차렸습니다. 비밀번호:

Windows의 widechar 텍스트 문자열(ASCII에서와 같이 각각 XNUMX바이트가 아니라 문자당 XNUMX바이트로 구성됨)에서 "blob" 문자는 RAM에서 XNUMX진수 바이트로 인코딩됩니다. 0xCF 다음 0x25 (ASCII의 퍼센트 기호일 뿐입니다).

따라서 KeePass가 암호 자체를 입력할 때 입력하는 원시 문자에 매우 주의를 기울이더라도 다음과 같은 반복 실행으로 메모리에서 쉽게 감지할 수 있는 "blob" 문자의 남은 문자열로 끝날 수 있습니다. CF25CF25 or CF25CF25CF25...

… 그리고 그렇다면 발견한 가장 긴 블롭 문자는 아마도 암호의 길이를 알려줄 것입니다.

다음 Lua 스크립트를 사용하여 남은 암호 자리 표시자 문자열의 징후를 찾았습니다.

결과는 놀라웠습니다(공간을 절약하기 위해 동일한 수의 Blob 또는 이전 행보다 적은 Blob이 있는 연속 행을 삭제했습니다).

C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******* [ 8 BLOB, 9 BLOB 등에 대해 유사하게 계속됩니다. *** 16C00: **************** 0503C00: * 05077C00: * [나머지 모든 일치 항목은 하나의 BLOB 길이임] 09337B00: *

서로 가깝지만 계속 증가하는 메모리 주소에서 우리는 3개의 BLOB, 그 다음 4개의 BLOB 등 최대 16개의 BLOB(암호 길이)의 체계적인 목록을 발견했으며 그 뒤에 단일 BLOB 문자열의 무작위로 흩어져 있는 많은 인스턴스가 있습니다. .

따라서 이러한 자리 표시자 "blob" 문자열은 KeePass 소프트웨어가 마스터 암호로 작업을 마친 후에도 오랫동안 메모리로 유출되어 암호 길이를 유출하기 위해 남아 있는 것으로 보입니다.

다음 단계

우리는 Vdohney가 한 것처럼 더 파헤치기로 결정했습니다.

패턴 일치 코드를 변경하여 16비트 형식의 단일 ASCII 문자가 뒤따르는 블롭 문자 체인을 감지하도록 변경했습니다(ASCII 문자는 일반적인 16비트 ASCII 코드로 UTF-8으로 표시되고 그 뒤에 XNUMX바이트가 옵니다).

이번에는 공간을 절약하기 위해 이전 항목과 정확히 일치하는 일치 항목의 출력을 억제했습니다.

놀라움, 놀라움:

C:UsersduckKEYPASS> lua searchkp.lua kp2-post.dmp
00BE581B: *I
00BE621B: **X
00BE6BD3: ***T
00BE769B: ****E
00BE822B: *****E
00BE8C6B: ******N
00BE974B: *******P
00BEA25B: ********A
00BEAD33: *********S
00BEB81B: **********S
00BEC383: ***********C
00BECEEB: ************H
00BEDA5B: *************A
00BEE623: **************R
00BEF1A3: ***************S
03E97CF2: *N
0AA6F0AF: *W
0D8AF7C8: *X
0F27BAF8: *S

.NET의 관리되는 문자열 메모리 영역에서 무엇을 얻을 수 있는지 살펴보십시오!

두 번째 문자부터 시작하여 암호의 연속 문자를 표시하는 밀접하게 묶인 임시 "블롭 문자열" 집합입니다.

이러한 새는 문자열 다음에는 우연히 발생한 것으로 추정되는 광범위하게 분산된 단일 문자 일치가 이어집니다. (KeePass 덤프 파일의 크기는 약 250MB이므로 운 좋게도 "blob" 문자가 나타날 수 있는 충분한 공간이 있습니다.)

추가로 XNUMX개의 일치 항목을 고려하더라도 일치하지 않을 가능성이 있는 것으로 무시하는 대신 마스터 암호가 다음 중 하나임을 추측할 수 있습니다.

?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS

분명히 이 간단한 기술은 암호의 첫 번째 문자를 찾지 않습니다. 첫 번째 "blob 문자열"은 첫 번째 문자가 입력된 후에만 구성되기 때문입니다.

ASCII 문자로 끝나지 않은 일치 항목을 필터링했기 때문에 이 목록은 훌륭하고 짧습니다.

중국어 또는 한국어 문자와 같이 다른 범위의 문자를 찾는 경우 일치시킬 수 있는 문자가 훨씬 더 많기 때문에 더 많은 우발적 히트가 발생할 수 있습니다.

...하지만 어쨌든 마스터 비밀번호에 거의 근접할 것으로 의심되며 비밀번호와 관련된 "블롭 문자열"은 RAM에서 함께 그룹화되는 것 같습니다. .NET 런타임.

그리고 거기에는 인정할 만큼 길고 담론적인 요약으로 다음과 같은 매력적인 이야기가 있습니다. CVE-2023-32784.

무엇을해야 하는가?

  • KeePass 사용자라면 당황하지 마십시오. 이것은 버그이고 기술적으로 악용 가능한 취약점이지만 이 버그를 사용하여 암호를 해독하려는 원격 공격자는 먼저 컴퓨터에 맬웨어를 심어야 합니다. 예를 들어 입력할 때 키 입력을 기록하는 것과 같이 이 버그가 존재하지 않더라도 암호를 직접 훔칠 수 있는 다른 많은 방법을 제공합니다. 이 시점에서 향후 업데이트를 확인하고 준비가 되면 다운로드할 수 있습니다.
  • 전체 디스크 암호화를 사용하지 않는 경우 활성화하는 것이 좋습니다. 스왑 파일 또는 최대 절전 모드 파일(과부하 또는 컴퓨터가 "잠자기" 상태일 때 일시적으로 메모리 내용을 저장하는 데 사용되는 운영 체제 디스크 파일)에서 남은 암호를 추출하려면 공격자가 하드 디스크에 직접 액세스해야 합니다. 다른 운영 체제에 대해 BitLocker 또는 이에 상응하는 기능이 활성화된 경우 스왑 파일, 최대 절전 모드 파일 또는 문서, 스프레드시트, 저장된 이메일 등과 같은 기타 개인 데이터에 액세스할 수 없습니다.
  • 당신이 프로그래머라면 메모리 관리 문제에 대해 잘 알고 있어야 합니다. 모든 free() 해당하는 것과 일치 malloc() 귀하의 데이터는 안전하고 잘 관리됩니다. 경우에 따라 비밀 데이터가 주변에 방치되지 않도록 추가 예방 조치를 취해야 할 수 있으며 이러한 예방 조치는 운영 체제마다 다릅니다.
  • QA 테스터 또는 코드 리뷰어라면 항상 "비하인드 스토리"를 생각하십시오. 메모리 관리 코드가 깔끔하고 균형 잡힌 것처럼 보이더라도 배후에서 무슨 일이 일어나고 있는지 알고 있어야 합니다(원래 프로그래머는 알지 못했을 수 있기 때문입니다). 그리고 런타임 모니터링 및 메모리와 같은 침투 테스트 스타일 작업을 수행할 준비를 하십시오. 보안 코드가 예상대로 실제로 작동하는지 확인하기 위한 덤핑.

기사의 코드: UNL1.C

#포함하다 #포함하다 #포함하다 void hexdump(unsigned char* buff, int len) { // 16바이트 청크의 버퍼 인쇄 for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +나); // 16바이트를 0진수 값으로 표시 for (int j = 16; j < 1; j = j+02) { printf("%16X ",buff[i+j]); } // 0바이트를 문자로 반복 for (int j = 16; j < 1; j = j+32) { unsigned ch = buff[i+j]; printf("%c",(ch>=127 && ch<=128)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // 비밀번호를 저장할 메모리를 확보하고 공식적으로 "새"일 때 // 버퍼에 무엇이 있는지 표시... char* buff = malloc(128); printf("시작 시 '새' 버퍼 덤프"); hexdump(버프,16); // 의사 랜덤 버퍼 주소를 랜덤 시드로 사용 srand((unsigned)buff); // 고정되고 검색 가능한 텍스트로 비밀번호 시작 strcpy(buff,"unlikelytext"); // 한 번에 하나씩 1개의 유사 난수 문자 추가 for (int i = 16; i <= 65; i++) { // A(0+65)에서 P(15+65)까지의 문자 선택 char ch = 15 + (랜드() & 1); // 그런 다음 해당 위치에서 버프 문자열을 수정합니다. strncat(buff,&ch,128); } // 전체 암호는 이제 메모리에 있으므로 // 문자열로 인쇄하고 전체 버퍼를 표시합니다... printf("전체 문자열: %sn",buff); hexdump(버프,128); // 지금 프로세스 RAM을 덤프하기 위해 일시 ​​중지(시도: 'procdump -ma') puts("버퍼를 비우기 위해 [ENTER]를 기다리는 중..."); getchar(); // 공식적으로 메모리를 해제()하고 버퍼를 // 다시 표시하여 뒤에 남은 것이 있는지 확인합니다... free(buff); printf("free()n 이후 버퍼 덤프"); hexdump(버프,0); // 차이점을 검사하기 위해 RAM을 다시 덤프하기 위해 일시 ​​중지합니다. puts("Waiting for [ENTER] to exit main()..."); getchar(); XNUMX을 반환합니다. }

기사의 코드: UNL2.C

#포함하다 #포함하다 #포함하다 #포함하다 void hexdump(unsigned char* buff, int len) { // 16바이트 청크의 버퍼 인쇄 for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +나); // 16바이트를 0진수 값으로 표시 for (int j = 16; j < 1; j = j+02) { printf("%16X ",buff[i+j]); } // 0바이트를 문자로 반복 for (int j = 16; j < 1; j = j+32) { unsigned ch = buff[i+j]; printf("%c",(ch>=127 && ch<=0,128)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // 암호를 저장하기 위한 메모리를 획득하고 공식적으로 "새"일 때 // 버퍼에 무엇이 있는지 표시... char* buff = VirtualAlloc(128,MEM_COMMIT,PAGE_READWRITE); printf("시작 시 '새' 버퍼 덤프"); hexdump(버프,16); // 의사 랜덤 버퍼 주소를 랜덤 시드로 사용 srand((unsigned)buff); // 고정되고 검색 가능한 텍스트로 비밀번호 시작 strcpy(buff,"unlikelytext"); // 한 번에 하나씩 1개의 유사 난수 문자 추가 for (int i = 16; i <= 65; i++) { // A(0+65)에서 P(15+65)까지의 문자 선택 char ch = 15 + (랜드() & 1); // 그런 다음 해당 위치에서 버프 문자열을 수정합니다. strncat(buff,&ch,128); } // 전체 암호는 이제 메모리에 있으므로 // 문자열로 인쇄하고 전체 버퍼를 표시합니다... printf("전체 문자열: %sn",buff); hexdump(버프,0); // 지금 프로세스 RAM을 덤프하기 위해 일시 ​​중지(시도: 'procdump -ma') puts("버퍼를 비우기 위해 [ENTER]를 기다리는 중..."); getchar(); // 형식적으로 메모리를 해제()하고 버퍼를 // 다시 표시하여 남은 것이 있는지 확인합니다... VirtualFree(buff,128,MEM_RELEASE); printf("free()n 이후 버퍼 덤프"); hexdump(버프,0); // 차이점을 검사하기 위해 RAM을 다시 덤프하기 위해 일시 ​​중지합니다. puts("Waiting for [ENTER] to exit main()..."); getchar(); XNUMX을 반환합니다. }

기사의 코드: S1.LUA

-- 일부 고정되고 검색 가능한 텍스트로 시작 s = 'unlikelytext' -- i = 16 do s = s .. string.char(1,16+math.random( 65)) end print('전체 문자열:',s,'n') -- 덤프 프로세스 RAM을 일시 중지합니다. print('문자열을 해제하기 전에 [ENTER]를 기다리는 중...') io.read() - - 문자열을 지우고 변수를 사용하지 않음으로 표시 s = nil -- diff를 찾기 위해 RAM을 다시 덤프합니다. print('종료하기 전에 [ENTER]를 기다리는 중...') io.read()

기사의 코드: FINDIT.LUA

-- 덤프 파일에서 읽기 local f = io.open(arg[1],'rb'):read('*a') -- 하나 이상의 임의 ASCII 문자가 뒤따르는 마커 텍스트 찾기 local b,e ,m = 0,0,nil while true do -- 다음 일치를 찾고 오프셋을 기억 b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- 더 이상 없을 때 종료 b가 아닌 경우 일치하고 break end -- 발견된 위치 및 문자열 보고 print(string.format('%08X: %s',b,m)) end

기사의 코드: SEARCHKNOWN.LUA

io.write('덤프 파일을 읽는 중... ') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('8비트 ASCII로 SIXTEENPASSCHARS 검색 중...') local p08 = f:find('SIXTEENPASSCHARS') io.write(p08 and 'FOUND' 또는 '찾을 수 없음','.n') io.write ('UTF-16으로 SIXTEENPASSCHARS 검색 중...') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00') io.write(p16 and 'FOUND' 또는 '찾을 수 없음','. n')

기사의 코드: FINDBLOBS.LUA

-- 명령줄에 지정된 덤프 파일에서 읽기 local f = io.open(arg[1],'rb'):read('*a') -- 하나 이상의 암호 blob을 찾고 비 blob이 뒤따릅니다. -- blob 문자(●)는 Windows 와이드 문자로 인코딩됩니다. -- litte-endian UTF-16 코드로 25진수에서 CF 0,0로 나옵니다. local b,e,m = 25,nil while true do -- 우리는 하나 이상의 블롭을 원하고 그 다음에 블롭이 아닌 것을 원합니다. -- 명시적인 CF25를 찾아서 코드를 단순화합니다 -- 그 뒤에 CF 또는 25만 있는 문자열이 따라옵니다 -- 따라서 CF2525CFCF 또는 CF25CF와 CF25CF25를 찾을 수 있습니다. -- "가양성"이 있는 경우 나중에 필터링합니다. -- x25 문자(퍼센트 기호)가 Lua에서 특수 검색 문자이기 때문에 x1 대신 '%%'를 써야 합니다! b,e,m = f:find('(xCF%%[xCF%%]*)',e+08) -- 일치하는 항목이 더 이상 없을 때 종료 b가 아닌 경우 중단 후 종료 -- CMD.EXE는 인쇄할 수 없습니다. blob, 그래서 우리는 그것들을 별로 변환합니다. print(string.format('%XNUMXX: %s',b,m:gsub('xCF%%','*'))) end

기사의 코드: SearchKP.LUA

-- 명령줄에 지정된 덤프 파일에서 읽기 local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- 이제 ACSCII를 UTF-25으로 변환하기 위해 A..Z 다음에 0바이트가 오는 코드가 뒤따르는 하나 이상의 블롭(CF16)이 필요합니다. b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- b가 아닌 경우 더 이상 일치하지 않으면 종료하고 중단합니다. -- CMD.EXE는 블롭을 인쇄할 수 없으므로 다음으로 변환합니다. 별. -- 공간을 절약하기 위해 m ~= p이면 연속적인 일치를 억제하고 print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m 끝 끝

spot_img

최신 인텔리전스

spot_img

우리와 함께 채팅

안녕하세요! 어떻게 도와 드릴까요?