저번 강좌에서는 상당히 쉬운 크랙미 프로그램을 크랙했습니다.
이번에는 저번 강좌보다는 조금 어려우면서도 쉬운 크랙미 프로그램을 크랙해보겠습니다.
우선 크랙미 프로그램을 받습니다.
파일을 받으시고 압축을 푸신 후에 abexcm5.exe 라는 파일을 실행해보세요.
절대로 abexcm5.exe 외에 파일들은 건드리시면 안됩니다.
왜냐면 그 외에 파일들은 해당 프로그램의 크랙과 관련 된 정답이기 때문입니다.
즉, 정답을 미리 보고 하시면 안되겠죠.
프로그램을 실행하면 다음 그림과 같이 뜹니다.
이 상태에서 체크 버튼을 누르시면 시리얼 번호가 틀리다고 나옵니다.
우리는 이 시리얼 번호를 알아내는 것 입니다.
가장 중요한 것은 이전 크랙과는 다르게 프로그램을 패치하시면 안됩니다.
이전 크랙미에서는 프로그램의 기계어 코드를 디스어셈블 시켜서
그 디스어셈블리 된 소스를 수정해서 패치하는 식으로 크랙하였습니다.
하지만 이번 크랙미는 실행 파일 자체를 수정하시면 안됩니다.
이 크랙미가 원하는 것은 시리얼 번호를 알아내고
더 나아가서 키젠을 만드는 것 입니다.
즉,이번 크랙미에서는 프로그램 자체를 크랙한다기보다는
소프트웨어를 역공학 ( 리버스 엔지니어링 ) 한다고 보시면 되겠습니다.
프로그램을 올리 디버거로 열어봅시다.
디스 어셈블리 된 코드가 나옵니다.
여기서 중요한 것은 처음 크랙미를 크랙한 것 처럼 한줄 한줄씩 일일이 해석 할 필요가 없다는 것 입니다.
우리가 하고자 하는것은 올바른 시리얼 키를 구하는 것 입니다.
즉, 시리얼 키를 처리하는 루틴부분을 분석하고 다른 부분은 분석 할 필요가 없습니다.
스크롤 바를 조금 내리시면 시리얼 키가 맞는지 틀린지 처리하는 코드 부분이 나옵니다.
0x401101 주소 부분에서 메세지 박스를 띄어줍니다.
시리얼 키가 틀리다는 내용을 보여줍니다.
0x401117 주소 부분에서 메세지 박스를 띄어줍니다.
시리얼 키가 맞다는 내용입니다.
다시 한 번 말씀드리자면 이번 크랙미는 프로그램 코드를 수정하시면 안됩니다.
아주 간단하게 비교 문에서 시리얼 키가 맞다는 메세지 박스를 띄어주는 코드로 무조건 점프시키면 크랙이 쉽게 되지만
이 크랙미가 원하는 것은 그것이 아닙니다.
이제 시리얼 키를 점검하는 시작 주소부분을 정확하게 구하셔야 합니다.
분명 시리얼 키를 점검하는 루틴은 체크 버튼을 클릭 시 실행될 껍니다.
즉,이 부분에 코드가 어느 주소에서 시작되는지 찾아야 되는데
짐작 가는 부분에 브레이크 포인트를 걸고 프로그램을 실행시킵니다.
브레이크 포인트는 F2 키로 걸 수 있습니다.
그리고 체크 버튼을 누를 때 브레이크 포인트를 설정한 코드 부분에서 멈춘다면
시리얼 키 체크 루틴임을 짐작 할 수 있습니다.
그리고 다시 그 바로전 주소에 브레이크 포인트를 걸고 체크 버튼을 클릭 시
그 코드 부분에서도 멈춘다면 시리얼 키 체크 루틴임을 알 수 있는데
이런식으로 계속 주소를 거슬러 올라가면 시리얼 키를 체크하는 루틴의 시작주소를 알 수 있습니다.
( 주소를 거슬러 올라가다보면 체크 버튼을 누를 때 브레이크 포인트가 걸리지 않는다면 그 다음 주소가 시작 루틴 )
일반적으로 이런 루틴은 스택에 푸쉬하는 명령어인 PUSH 명령어가 시작되는 곳에서 시작됩니다.
실제로 위에 설명한대로 한줄 한줄씩 반복노가다를 하지는 않습니다.
그냥 코드를 보고 이쯤에서 실행 될꺼 같다고 생각하고 브레이크 포인트를 몇 번 걸어보면 나오게 됩니다.
위에 설명은 극단적인 예시일 뿐입니다.
여차여차 해서 시리얼 키 비교 루틴은 0x40106C 에서 시작됩니다.
다음과 같이 시리얼 키 체크 루틴에 브레이크 포인트를 걸으시고
브레이크 포인트가 걸리는 시점부터 F8 단축키를 통해 Step Over 방식으로 디버깅을 시작합니다.
0x040107D 부분에 코드가 시작되려고 할 때 0x40106E 주소 부분을 마우스로 선택해보세요.
GetDlgItemTextA 함수가 호출 된 후에 가져온 값이 0x402324 부분에 저장됨을 알 수 있습니다.
가져오는 데이터 값은 사용자가 입력한 시리얼 키 입니다.
지금은 기본값으로 그냥 나뒀기 때문에 다음과 같이 뜨는군요.
그럼 시리얼 키 체크 루틴에서 가장 중요한 코드부분을 살펴보겠습니다.
0040107D |. 6A 00 PUSH 0 ; /pFileSystemNameSize = NULL
0040107F |. 6A 00 PUSH 0 ; |pFileSystemNameBuffer = NULL
00401081 |. 68 C8204000 PUSH abexcm5.004020C8 ; |pFileSystemFlags = abexcm5.004020C8
00401086 |. 68 90214000 PUSH abexcm5.00402190 ; |pMaxFilenameLength = abexcm5.00402190
0040108B |. 68 94214000 PUSH abexcm5.00402194 ; |pVolumeSerialNumber = abexcm5.00402194
00401090 |. 6A 32 PUSH 32 ; |MaxVolumeNameSize = 32 (50.)
00401092 |. 68 5C224000 PUSH abexcm5.0040225C ; |VolumeNameBuffer = abexcm5.0040225C
00401097 |. 6A 00 PUSH 0 ; |RootPathName = NULL
00401099 |. E8 B5000000 CALL
0040109E |. 68 F3234000 PUSH abexcm5.004023F3 ; /StringToAdd = "4562-ABEX"
004010A3 |. 68 5C224000 PUSH abexcm5.0040225C ; |ConcatString = ""
004010A8 |. E8 94000000 CALL
이 부분에서는 GetVolumeInformationA 함수를 통해서 드라이브의 정보를 읽어오게 됩니다.
00401097 |. 6A 00 PUSH 0 ; |RootPathName = NULL
루트 경로는 NULL 이므로 C 드라이브의 정보를 읽어오게 됩니다.
0040109E |. 68 F3234000 PUSH abexcm5.004023F3 ; /StringToAdd = "4562-ABEX"
0x004023F3 주소에는 "4562-ABEX" 다음과 같은 값이 들어가 있습니다.
이쯤에서 눈치를 채셔야 합니다.
004010A3 |. 68 5C224000 PUSH abexcm5.0040225C ; |ConcatString = ""
004010A8 |. E8 94000000 CALL
lstrcatA 함수는 문자열을 합친는 함수입니다.
0x0040225C 에 담긴 문자열과 0x004023F3 담긴 문자열을 합쳐서 합쳐진 문자열을 0x0040225C 주소에 적재합니다.
위에 코드를 제대로 보셨다면 0x0040225C 주소를 보고 아 그렇구나 라고 아셔야 합니다.
00401092 |. 68 5C224000 PUSH abexcm5.0040225C ; |VolumeNameBuffer = abexcm5.0040225C
다음과 같이 0x40225C 에는 하드 디스크의 이름이 들어가게 됩니다.
결국엔 하드 디스크 이름 + "4562-ABEX" 라는 문자열을 합치는 코드가 되게 됩니다.
일반적으로 하드 디스크 이름을 지정하지 않았을 때에는 다음과 같이 아무런 값도 들어가지 있지 않습니다.
그러나 하드 디스크 이름이 정해져 있을 경우 다음과 같이 하드 디스크 이름이 뜨게 됩니다.
여기서 하드 디스크 이름은 "안녕하세요"입니다.
올리 디버거에서는 한글로 된 문자열이 제대로 않 나오는 경우가 있습니다.
참고하시길 바랍니다.
그래서 시험 삼아 C 드라이브의 하드 디스크 이름을 "ABCD" 로 다시 바꾼 후 브레이크 포인트를 0x4010A8 에 걸은 후
프로그램을 다시 디버깅해보겠습니다.
브레이크 포인트가 걸린 화면입니다.
브레이크 포인트가 걸린 다음줄 소스 코드를 분석보아요.
004010AD |. B2 02 MOV DL,2
004010AF |> 8305 5C224000 >/ADD DWORD PTR DS:[40225C],1
004010B6 |. 8305 5D224000 >|ADD DWORD PTR DS:[40225D],1
004010BD |. 8305 5E224000 >|ADD DWORD PTR DS:[40225E],1
004010C4 |. 8305 5F224000 >|ADD DWORD PTR DS:[40225F],1
004010CB |. FECA |DEC DL
004010CD |.^75 E0 \JNZ SHORT abexcm5.004010AF
dl 레지스터에 2 라는 값이 들어갔고 dec dl 명령어를 통해서 dl 의 값을 감소시키고
jnz 명령어를 통해서 특정 주소로 점프시킵니다.
이 코드는 루프를 돌게 되는데 총 2 번 돌게 됩니다.
즉 jnz 는 dl 레지스터가 0 이 아닐 때 0x004010AF 주소로 점프합니다.
004010AF |> 8305 5C224000 >/ADD DWORD PTR DS:[40225C],1
004010B6 |. 8305 5D224000 >|ADD DWORD PTR DS:[40225D],1
004010BD |. 8305 5E224000 >|ADD DWORD PTR DS:[40225E],1
004010C4 |. 8305 5F224000 >|ADD DWORD PTR DS:[40225F],1
다음 코드를 보시면 0x40225C 부터 총 4글자에 값을 1씩 증가시키는 것을 볼 수 있습니다.
루프가 총 두번 돌으므로 이 값은 총 2가 증가하게 됩니다.
이제 0x4010CF 에 브레이크 포인트를 걸고 그곳에서 브레이크 포인트가 걸리면
다음과 같이 0x40225C 에 있는 값이 바뀌었습니다.
0x40225C 값이 "ABCD" + "4562-ABEX" 연산으로 문자열이 합쳐지고 그 값이 0x40225C 에 다시 들어가고
루프를 돌음으로써 총 4글자가 2의 값만큼 증가해서
ABCD 가 CDEF 로 바뀐 것 입니다.
이제 F8 키를 눌러서 한줄 한줄씩 실행합니다.
0x4010DE 주소에서 멈추세요.
바로 위에 코드와 비슷하므로 그림은 첨부하지 않습니다.
004010CF |. 68 FD234000 PUSH abexcm5.004023FD ; /StringToAdd = "L2C-5781"
004010D4 |. 68 00204000 PUSH abexcm5.00402000 ; |ConcatString = "L2C-5781"
004010D9 |. E8 63000000 CALL
lstrcatA 함수를 호출하기전에는 0x00402000 에 값은 "" 이었습니다.
하지만 lstrcatA 함수에 의해서 지금은 "L2C-5781" 라는 값이 들어가게 되었네요.
계속 F8 키를 눌러서 실행한 후에 0x4010ED 에서 멈추면..
004010DE |. 68 5C224000 PUSH abexcm5.0040225C ; /StringToAdd = "CDEF4562-ABEX"
004010E3 |. 68 00204000 PUSH abexcm5.00402000 ; |ConcatString = "L2C-5781CDEF4562-ABEX"
004010E8 |. E8 54000000 CALL
다음과 같이 값들이 변경되었습니다.
0x004010E8 주소에 lstrcatA 함수가 호출후에 코드를 보고 계시는데
이전에 0x00402000 값과 + 0x0040225C 값이 합쳐진 것을 알 수 있습니다.
이제 다음 줄을 보시면
004010ED |. 68 24234000 PUSH abexcm5.00402324 ; /String2 = "Enter your serial"
004010F2 |. 68 00204000 PUSH abexcm5.00402000 ; |String1 = "L2C-5781CDEF4562-ABEX"
004010F7 |. E8 51000000 CALL
다음과 같이 사용자가 입력한 시리얼키와 프로그램내에서 하드 디스크 이름을 통해서 생성한 시리얼 키를 비교하는 것을
볼 수 있습니다.
즉, 시리얼키는 "L2C-5781CDEF4562-ABEX" 입니다.
프로그램을 다시 실행시켜서 시리얼키인 "L2C-5781CDEF4562-ABEX" 를 입력하시고 체크버튼을 누르시면..
다음과 같이 시리얼 키가 맞다는 메세지가 뜨게 됩니다.
여기서 끝난 것이 아닙니다.
키젠을 만들어야 겠죠.
이 프로그램을 하드 디스크 이름에 따라서 시리얼키가 다름으로 방금 입력한 시리얼키가 정답이라고 할 수가 없습니다.
따라서 키젠을 만들어주어야 합니다.
제가 만든 키젠의 코드는 다음과 같습니다.언어는 파스칼 언어이고 컴파일러는 델파이 7 입니다.
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
SerialAddOne, SerialAddTwo: String;
lpVolumeNameBuffer:Array[0..49] of Char;
lpMaximumComponentLength, lpFileSystemFlags: DWORD;
begin
GetVolumeInformation(nil, lpVolumeNameBuffer, 50,
nil, lpMaximumComponentLength, lpFileSystemFlags, nil, 0);
SerialAddTwo := lpVolumeNameBuffer + '4562-ABEX';
for i := 0 to 3 do begin
Inc(SerialAddTwo[i + 1], 2);
end;
SerialAddOne := 'L2C-5781';
edtSerial.Text := SerialAddOne + SerialAddTwo;
end;
단지 위에 해석한 어셈블리 코드를 사람이 알아 보기 쉬운 고급 언어로 바꾼 것 뿐입니다.
이로써 키젠이 만들어졌고 리버싱이 끝났습니다.
키젠은 받은 크랙미안에 keygen 이라는 폴더에 있습니다.
'dekar' 카테고리의 다른 글
[스크랩] 데이터 형식 요약 정보 (0) | 2009.02.20 |
---|---|
[스크랩] abex1 크랙 강좌 (0) | 2009.02.20 |
[스크랩] [어셈블리어] 어셈블리어 강좌 6 (0) | 2009.02.20 |
[스크랩] 어셈블리어 Jxx문 총정리 (0) | 2009.02.20 |
[스크랩] [어셈블리어] 어셈블리어 강좌 5 (0) | 2009.02.20 |