간단히 말해서, 나는 어느 날 지루했고 친구 인 Maximus Hackerman에게 나에게 할 일을 줄 것을 요청했습니다. 즉시, 그는 "Reverseme.exe"(Virustotal Link)라는 간단한 실행 파일을 통해“자신의 응용 프로그램에 숨겨진 메시지를 인쇄”에 대한 간단한 의견을 보냈습니다. 당연히 CPP 헤더 파일은 제공되지 않았습니다. 실행 파일을 실행하면 콘솔 기반 애플리케이션으로 열립니다. 인쇄는 "모든 마법 패킷을 보냈습니다. 그리고 거의 즉시 종료됩니다. “곧”는 주관적이라고 생각합니다.
다행히도, 방지 방지가없는 것처럼 보이지 않으므로 해커 맨이 그 날에 기분이 좋다고 생각합니다.
WindBG와 Disassembler를 첨부 한 후, 우리는 응용 프로그램이 처음으로 나타나는 것으로 보이며, 처리되지 않은 채로 divide by zero 알 수 있습니다.
그러나 면밀한 검사 에서이 예외는 가시 메시지를 인쇄하기 직전에 발생합니다. 이 시점 이전에 2 개의 차량 (벡터 예외 처리기)이 약간 등록되어 있으므로이 예외는 의도적이며 제어 흐름을 관리하는 데 사용될 가능성이 높습니다. 우리를 정말로 시도하고 던지는 저렴한 트릭!
현재 차량을 잊어 버리면 응용 프로그램이 패킷을 "전송"하는 경우 Winsock Send 기능을 사용하여 그렇게한다고 가정하는 것이 안전합니다 (응용 프로그램은 명확하게“마법”패킷이라고 말하지만 우리는 볼 수 있습니다).
예상대로, 마법으로 패킷을 보낼 수 없으므로 Winsock send 기능이 실제로 가져옵니다.
전송 함수에 중단 점을 표시하면 전송 당시 추상화 할 수있는 관련 데이터가 있는지 살펴보고 볼 수 있습니다. 안타깝게도 버퍼가 함수에로드 될 때까지 데이터가 이미 암호화되어 있습니다. 그러나 버퍼의 길이는 항상 3 바이트이고 소켓은 항상 0x69 ( NIC )의 하드 코드 값에 바인딩되며 전송 기능 중단 점이 총 14 배에 도달한다고 판단 할 수 있습니다.
암호화를 돌아 다니는 방법에는 몇 가지가 있습니다. 하나는 완전히 반전하는 것입니다. 많은 노력이 될 수 있습니다. 다른 하나는 암호화 전에 원하는 데이터를 찾아 암호화되기 전에 추상화하는 것입니다. 후자는 전자보다 훨씬 쉽기 때문에 우리는 그와 함께 가고 있습니다. 그래도 암호화를 되돌리십시오. 나는 간단한 모습을 보았고 끔찍하지 않습니다. 또는 화려하다고 느끼면 항상 소켓 핸들 값을 덮어 쓰고 수신 응용 프로그램 거래를 할 수 있습니다.
안타깝게도 초기 메시지와는 별도로 읽기 전용 데이터 세그먼트에 유용한 문자열이 없으므로 해당 측면에서 도움이되는 포인터가 없습니다!
차량으로 돌아가서 등록 된 핸들러는 모두 알 수없는 객체를 할당 된 메모리 버퍼에 복사하는 데 사용됩니다. 이것은 Memcpy를 두 번째 매개 변수로 사용하여 Memcpy를 사용하여 수행되는 것으로 보이며, 이는 두 번째 매개 변수로 하드 코딩 된 정수를 사용합니다. 이러한 하드 코드 값은 어느 시점에서도 14를 초과하지 않는다는 점은 주목할 가치가 있습니다. 스크린 샷에 차량 중 하나만 포함 시켰습니다. 하드 코드 값을 제외하고는 기본적으로 코드별로 동일하기 때문입니다.
Memcpy 함수 중 하나에서 브레이크 포인트를하고 두 번째 매개 변수에서 하위 기능을 검사함으로써, 하드 코딩 된 정수 (아래 예제의 13 / 0dh)가 A1 매개 변수의 첫 번째 바이트로 설정되고 A1+1은 Pointer가 대답 된 직후에 문자를 포함 함을 알 수 있습니다.
이 기능에 대한 다른 14 개의 호출 중 일부를 확인하면 동일한 동작 반복을 찾을 수 있습니다. 해당 숫자를 순서 표시기로 사용하여 1에서 14로 문자를 정리하면 읽을 수있는 단어를 철자하기 시작합니다. 이제 우리는 매우 게으르고 콘솔 앱을 만들어 메시지를 인쇄 할 수 있습니다. 또한 메시지는 어느 시점에서 변경 될 수 있습니다. 따라서 암호화되기 전에 값을 가로 채기 위해 코드 동굴을 작성해 봅시다.
죄송하지만 C ++를 사용하고 있습니다! C#을 사용하는 것을 선호하는 경우 970 만 기능의 전체 플랫폼 호출 프로세스를 자유롭게 살펴보고 완료되면 여기로 돌아 오십시오. 어쨌든 먼저 동굴을 코딩 할 위치를 결정해야합니다. 운 좋게도 우리는 이미 알고 있습니다! 그러나 위의 함수를 분석함으로써 간단히 말하면, 강조 표시된 지점에 따라 RDX 인덱스를 포함하고 RDX+1 에는 해당 문자가 포함되어 있음을 알고 있습니다. 아래는 논의 된 기능에 대한 어셈블리 코드입니다.
논리적으로 말하면, 우리는 실제로 세 번째 매개 변수에 관심이 없지만 RDX 레지스터와 RDX+1 가로 채고 싶기 때문에 mov [rsp+arg_8], rdx 에 있습니다. 이 아이들을하기 위해서는 MOV 명령에 대한 10 바이트가 필요합니다. 코드 동굴의 주소를 레지스터로 옮기려면 (우리는 RAX 사용하고, 나중에 10시 뉴스 중에 더 많이 사용), JMP 명령에 2 바이트를 사용하여 레지스터로 이동합니다. 추가 수학에서 PTSD를 가진 사람들에게는 총 12 바이트입니다. 이제 Bessie 이모가 모두 writeprocessmemory를 사용하여 해당 코드를 스포 게화하기 전에 위의 어셈블리에서 12 바이트를 대체 할 이상적인 장소가 없다는 것을 고려해야합니다. 오프셋 0x2905 ( mov [rsp+arg_8], rdx )에서 뛰어 내리려면 12 바이트가 필요하다면 그렇게하려면 0x2917 오프셋하는 데있어 두 개의 MOV 지침 사이에 차단됩니다. 불행히도, 만약 우리가 단순히 바이트를 거기에 글을 쓰려고한다면, 그것은 어셈블리를 완전히 제작하고 일부는 "흥미로운"부작용을 일으킬 것입니다. 결과적으로, 패딩을위한 1 바이트 지침을 추가하고 지시가 끝날 때 마무리하는 것이 더 쉬울 것입니다 (좀 더 해킹하고 죄송합니다). 오신 것을 환영합니다, 0x90.
어쨌든, 이제 우리는 계획이 무엇인지 알았으므로 코드를 작성해 봅시다 : 큐 강렬한 해커 맨 음악 !
아래는 바이트가 응용 프로그램 어셈블리에 기록되면 자체 NOP 슬라이드로 완성되면 코드 동굴로의 초기 점프가 보일 것입니다.
그러나 실제로 어셈블리를 작성하고 교체하기 전에 정지 상태에서 신청서를 시작해야합니다. 이렇게하면 초기 단계에서 응용 프로그램 런타임이 중단되므로 응용 프로그램이 관심있는 명령에 도달하기 전에 메모리 변경이 이루어질 수 있습니다. 아래 코드 추출물은이 프로세스를 보여 주고이 repo의 소스 파일에서 볼 수 있듯이 너무 많이 진행하지 않으며 대부분 자체적으로 말합니다.
프로세스가 생겨 났으므로 프로세스의 기본 주소를 얻어야합니다. 일반적으로이를 위해 열 열렬한 공정 모드를 사용할 수 있지만, 메인 프로세스 스레드를 즉시 중단 했으므로 PEB에는 완전히 채워진 PEB_LDR_DATA 구조, 특히 InMemoryOrderModuleList 포함되어 있으므로 현재 기본 주소를 얻을 수 없습니다. 그건 그렇고, 이것은 MSDN의 어느 곳에서나 문서화되지 않은 것으로 보입니다. 운 좋게도 이것은 상대적으로 우회하기가 쉽습니다. 프로세스를 매우 빠르게 재개하고 모듈을 쿼리 한 다음 프로세스를 재현 탁하여 프로세스가 너무 많이 진행되지 않고 필요한 정보를 얻을 수 있습니다. Windows의 대부분의 것들과 마찬가지로 Microsoft는 NtSuspendProcess 및 NtResumeProcess 와 같은 기능을 다시 문서화하지 않음으로써 운영 체제가 우수하다는 것을 반복하는 것을 좋아합니다. 편리하게 아빠는 Bill Gates와 친구이며, 이러한 기능은 ntdll.dll 에 결합되어 있으며, 이전에 만든 아래 수업을 사용하여 가져올 수 있습니다.
이제 필요한 두 가지 기능을 갖추 었으므로 프로세스를 재개하고 모듈을 쿼리하며 프로세스를 재현 탁시킬 수 있습니다.
While Loop이 왜 하나가 아닌 두 개의 모듈 발견을 기다리고 있습니까? 글쎄, Microsoft는 EnumProcessModules 모듈이 발견 한 첫 번째 모듈이 ntdll.dll이 될 것이라는 언급을 언급하지 않으면 서 문서화되지 않은 스파게티 네트의 뒷면에 미트볼과 함께 해트릭을 득점하려고합니다. 이것은 합리적으로 들리지만 실행 파일이 발견되면 인덱스를 ntdll.dll로 바꿉니다. 예는 다음과 같습니다.
첫 번째 모듈 만 쿼리 한 후 결과 :
두 모듈을 쿼리 한 후 결과 :
더 나아 가기 전에 코드 동굴로의 초기 점프와 코드 동굴 자체를 수행하는 어셈블리 코드를 작성해야합니다. 기본적으로 오프셋 0x2905 에서 시작하여 메모리를 덮어 쓰고, 점프를하고, 코드 캐비링 스파이를 한 다음 0x2911 로 다시 점프하여 일반 프로그램 흐름을 계속합니다. 처음에, 우리는 주소를 RAX 레지스터로 이동하는 것을 MOV RAX, 0x0 으로 선언합니다. RAX 휘발성 레지스터이며 어쨌든 곧 덮어 쓰기 때문에 사용하기위한 안전한 레지스터입니다. 재미있는 사실, 이것은 Nintendo가 오리지널 Mario Bros 플랫 포머를 프로그래밍 한 방법입니다. 아래는 코드가 컴파일러에서 어떻게 보이는지입니다. 다른 방식으로 만들 수는 있지만 필요한 어셈블리 지침을 바이트 코드로 분산시키기로 선택했습니다. 집 에서이 일을하고 싶다면이 웹 사이트를 사용하십시오.
실제 코드 동굴에 대한 코드는 조금 더 복잡 하며이 파일에도 논리가 주석을 달 수 있지만 다음은 거친 프로세스입니다.
R10 으로 이동RDX 의 하단 부분을 R11B 로 이동RDX 의 두 번째 바이트를 R11B 로 이동+12 )에서 R10 ( 0x2911 )으로 점프 한 주소를 이동하십시오.R10 으로 이동하여 스택 등을 보존하기 위해 우리가 덮어 쓰는 어셈블리 코드 (NOP 슬라이드 포함)를 코드 동굴에 다시 작성해야합니다. 이 코드는 NOPS를 제외한 " Predetermined Assembly "영역에서 참조됩니다. 아래는 못생긴 영광의 코드 동굴입니다.주로 어려운 부분.
이 시점에서, 우리는 본질적으로 숨겨진 메시지를 메모리에서 사이펀하는 데 필요한 모든 것을 가지고 있습니다. 우리는 단지 그것을 구현하면됩니다. 요약하기 위해, 우리는 스폰 된 중단 된 프로세스, 어셈블리 로직, 현탁 된 프로세스의 기본 주소, modules[0] 에 저장된 2 바이트 어레이 및 점프 로직을 작성 해야하는 위치의 오프셋을 가지고 있습니다. 아래 코드 스 니펫은 3 바이트 메모리 저장소의 주소 인 코드 동굴 (Jump to To)의 주소 (주소)에서 주소를 작성하고 조립 코드에 주소를 작성한 다음, 조립 코드를 중단 된 프로세스를 작성하기 전에 다음을 재개합니다.
codeCaveStorageAddr 의 마법의 해리포터 스타일 캐스팅은 주소를 바이트로 변환하는 것이며, 루프의 하드 코드 값은 어셈블리 배열의 동적 주소 배치를위한 것입니다. 물론, 이것은 훨씬 깨끗한 방식으로 수행 할 수 있습니다 (마법 숫자 어린이를 쓰지 마십시오). 나는 모든 바이트를 수동으로 작성한 후에 게으르다. 마지막 몇 가지 비트는 readprocessmemory를 사용하여 읽기, 메모리, 문자 및 색인을 주문한 바이트 배열로 저장하고, 현재 메모리의 판독 값을 읽을 때 WriteProcessMemory 를 사용하여 바이트를 신호하며 숨겨진 메시지를 인쇄하는 루프입니다. 우리는 보내기 기능이 14 번만 발생한다는 것을 알고 있으므로 바이트 배열이 채워지면 while 루프를 종료합니다. 이는 더 많은 메모리 편집으로 변경 될 수 있으며, 프로세스가 "종료되고"하드 코드 값 14를 사용하지 않고 루프를 중지 할 수 있지만이 예제에는 작동합니다.
결과? 숨겨진 메시지는 자체 콘솔 응용 프로그램에 인쇄됩니다! 누구든지 궁금한 점이 있다면 NPT는 다른 소프트웨어 최대 해커를 참조하는 것입니다.