'Programing'에 해당되는 글 79건

  1. 2022.02.24 9498번 시험 성적
  2. 2022.02.24 1008번 A/B
  3. 2022.02.24 10869번 사칙연산
  4. 2022.02.24 10998 AxB
  5. 2022.02.24 10718번 We love kriii
  6. 2022.02.24 1001번 A-B
  7. 2022.02.23 1000번 A+B
  8. 2022.02.22 2557번 Hello World
  9. 2017.07.12 지뢰찾기핵 만들기 - 결과
  10. 2017.07.12 지뢰찾기핵 만들기 - 코드

문제 : https://www.acmicpc.net/problem/9498 (백준 온라인 저지)

정답 코드

if문으로 처리를 해도 되지만 switch문으로 수행하면 더 간결해보일 것 같아 switch문으로 작성하였습니다. 다른 코드들은 1000번 문제에서 설명드렸으니 설명은 생략하겠습니다.

switch(parseInt(score/10)){

switch 문을 선언합니다. switch문은 지정한 표현식과 일치하는 case 문부터 코드를 수행하며 입력받은 점수에 따라 다른 코드를 수행하기 위해 사용하였습니다. 점수를 처리해주기 위해 표현식을 score/10으로 지정하였는데 표현식을 score만으로 해버리면 60~100 값을 처리할 case문을 다 작성해주어야 하기 처리 때문에 코드가 어마어마하게 길어집니다. 그래서 10으로 나눈 몫을 이용하여 6~10 값만을 처리할 case문만 작성하였습니다. 사용해야할 case문이 확 줄어든 겁니다.

case 10:
case 9:
     console.log("A");
     break;
case 8:
     console.log("B");
     break;
case 7:
     console.log("C");
     break;
case 6:
     console.log("D");
     break;

score 점수를 10으로 나눈 몫이 10인 경우 case 10부터 코드를 수행합니다. 즉 점수가 100점이라면 case 10부터 코드를 수행합니다. 그런데 코드를 보시면 case 10 부분에 코드가 없는데 코드가 없는 이유는 case 9와 수행하는 코드가 동일하기 때문에 굳이 작성할 필요가 없기 때문입니다. 앞에서 말씀드렸듯이 switch 문은 표현식과 일치하는 case문부터 코드를 수행합니다. 즉 case 10부터 코드를 수행한다면 case 10, case 9, case 8.... default 문의 코드까지 수행되게 되므로 case10부터 코드를 수행한다해도 case 9 코드 또한 실행된다는 뜻입니다.

그런데 case 10부터 코드를 수행한다고 하면 case 8, case 7 등의 코드들도 수행이 되버리므로 모든 성적이 출력되버리는 불상사가 발생합니다. 그래서 이를 방지하기 위해 성적출력 코드 밑에 break 문을 추가합니다. 여기서 break문은 switch문 실행을 중지하는 역할을 하므로 case 10 코드를 수행할 때 case 9 코드까지 수행한 후 break 문을 통해 switch문을 종료하게 됩니다. 이 내용을 정리하면 다음과 같습니다.

-> score가 100이면

1. case 10 수행 후 밑의 case 9로 이동합니다.

2. case 9의 console.log("A"); 실행 후 break 문을 통해 switch문을 종료합니다. 즉 case 8은 실행되지 않습니다.

-> score가 93이면

1. case 9의 console.log("A"); 실행 후 break 문을 통해 switch문을 종료합니다. 즉 case 8은 실행되지 않습니다.

-> score가 79이면

1. case 7의 console.log("C"); 실행 후 break 문을 통해 switch문을 종료합니다. 즉 case 6은 실행되지 않습니다.

이제 다음 코드를 살펴봅시다.

default:
     console.log("F");

default는 switch문 가장 마지막에 작성하는 문이며 표현식이 위의 case 중 아무것에도 해당되지 않는다면 default 문을 실행하게 됩니다. 즉 값이 0,1,2,3,4,5 가 나온다면 default의 코드가 수행되며 default 코드를 수행하고 나면 switch문이 종료되므로 break문을 넣어줄 필요가 없어 break를 넣지 않았습니다.

'Programing > Baekjoon 문제 알고리즘(node.js)' 카테고리의 다른 글

2438번 별 찍기  (0) 2022.02.24
2739번 구구단  (0) 2022.02.24
1008번 A/B  (0) 2022.02.24
10869번 사칙연산  (0) 2022.02.24
10998 AxB  (0) 2022.02.24
Posted by englishmath
,

문제 : https://www.acmicpc.net/problem/1008 (백준 온라인 저지)

정답 코드

10869번 문제와는 다르게 몫이 소수로 나와도 정답으로 인정해줍니다. 그래서 10869번 문제풀이 코드에서 A/B에 적용된 parseInt를 제거하였습니다.

'Programing > Baekjoon 문제 알고리즘(node.js)' 카테고리의 다른 글

2739번 구구단  (0) 2022.02.24
9498번 시험 성적  (0) 2022.02.24
10869번 사칙연산  (0) 2022.02.24
10998 AxB  (0) 2022.02.24
10718번 We love kriii  (0) 2022.02.24
Posted by englishmath
,

출처 : https://www.acmicpc.net/problem/10869 (백준 온라인 저지)

정답 코드

Javascript에서 Number 데이터 타입은 IEEE 754 국제 표준을 따르는 64비트 배정밀도 부동 소수점 방식을 사용하여 숫자를 나타냅니다. 그래서 Number 숫자들을 서로 나눗셈하면 몫이 실수로 나올 수도 있기 때문에 몫을 정수로 변환시켜주기위해 A/B 코드에는 parseInt를 사용하였습니다.

'Programing > Baekjoon 문제 알고리즘(node.js)' 카테고리의 다른 글

9498번 시험 성적  (0) 2022.02.24
1008번 A/B  (0) 2022.02.24
10998 AxB  (0) 2022.02.24
10718번 We love kriii  (0) 2022.02.24
1001번 A-B  (0) 2022.02.24
Posted by englishmath
,

문제 : https://www.acmicpc.net/problem/10998 (백준 온라인 저지)

정답 코드

1000번 문제 풀이 코드에서 A+B를 A*B로 수정하였습니다. 자세한 설명은 1000번 문제풀이에 나와있습니다.

'Programing > Baekjoon 문제 알고리즘(node.js)' 카테고리의 다른 글

1008번 A/B  (0) 2022.02.24
10869번 사칙연산  (0) 2022.02.24
10718번 We love kriii  (0) 2022.02.24
1001번 A-B  (0) 2022.02.24
1000번 A+B  (0) 2022.02.23
Posted by englishmath
,

문제 : https://www.acmicpc.net/problem/10718 (백준 온라인 저지)

정답 코드

2줄 출력이므로 설명은 생략하겠습니다.

'Programing > Baekjoon 문제 알고리즘(node.js)' 카테고리의 다른 글

10869번 사칙연산  (0) 2022.02.24
10998 AxB  (0) 2022.02.24
1001번 A-B  (0) 2022.02.24
1000번 A+B  (0) 2022.02.23
2557번 Hello World  (0) 2022.02.22
Posted by englishmath
,

문제 : https://www.acmicpc.net/problem/1001 (백준 온라인 저지)

정답 코드

1000번문제의 A+B를 A-B로 수정하였습니다. 나머지 코드는 1000번 문제에서 설명드렸으므로 따로 언급하지 않겠습니다.

'Programing > Baekjoon 문제 알고리즘(node.js)' 카테고리의 다른 글

10869번 사칙연산  (0) 2022.02.24
10998 AxB  (0) 2022.02.24
10718번 We love kriii  (0) 2022.02.24
1000번 A+B  (0) 2022.02.23
2557번 Hello World  (0) 2022.02.22
Posted by englishmath
,

문제 : https://www.acmicpc.net/problem/1000 (백준온라인저지)

 

두 값을 입력받아 합을 출력하는 문제입니다. 다른 언어로는 쉽게 풀 수 있으나 node.js는 일반적으로 C의 scanf나 python의 input 함수처럼 표준 입력 스트림(standard input stream)에 접근하는 내장함수를 지원하지 않으므로 직접 표준 입력 스트림에 접근해야 합니다. 먼저 아래의 실패한 코드를 살펴보겠습니다.

실패한 코드

let process = require("process");
let stdin = process.stdin;
 
process 객체를 가져온 후 process 객체에 포함된 stdin 객체를 변수에 저장합니다. 여기서 process 객체는 Node.js가 실행될 때 생성되는 Node.js 프로세스에 대한 정보를 담고 있는 객체이며 stdin 객체는 Node.js 프로세스가 사용하는 표준 입력 스트림에 대한 정보를 담고 있는 객체입니다. 일반적으로 process 전역변수는 명시를 따로 하지 않아도 process 객체를 담고 있지만 공식 Node.js 문서에서는 가급적 명시하라고 하여 코드를 명시하였습니다.
 
stdin.on("readable", function scanf(){
    let stdinBuffer = stdin.read();
    let stdinNumbers = stdinBuffer.toString().split(" ");
    let A = parseInt(stdinNumbers[0]);
    let B = parseInt(stdinNumbers[1]);
    console.log(A+B);
});
 
stdin 객체에서 "readable" 이벤트가 발생했을 때 수행할 함수(이벤트 리스너)를 등록합니다. readable 이벤트는 스트림에 읽을 수 있는 데이터가 존재할 때 발생되는 이벤트이며 scanf는 데이터를 읽어오기 위해 임의로 작성한 함수입니다.  즉 표준 입력 스트림에서 읽을 수 있는 데이터가 생길 경우 scanf함수를 호출한다는 의미이며 scanf 코드는 다음과 같습니다.
 
let stdinBuffer = stdin.read();
let stdinNumbers = stdinBuffer.toString().split(" ");
 
표준 입력 스트림에서 데이터를 읽어 버퍼에 저장합니다. 일반적으로 stdin객체의 read 메소드는 데이터를 읽을 때 공백, CR, LF 또한 데이터로 인식하여 읽어들이므로 실질적으로 사용할 입력값을 추출하기 위해 읽어들인 데이터가 담긴 버퍼에 적절한 처리를 해주어야 합니다.
 
읽어들인 버퍼를 문자열로 변환한 후 변환한 문자열을 공백을 기준으로 나눕니다. 나누어진 문자열을 요소로 가지는 배열로 저장하면 배열에는 실질적으로 사용할 입력값만 남게 되며 쉽게 정리하면 다음과 같습니다.
 
표준 입력 스트림 -> 1 2
 
버퍼 -> [0x31, 0x20, 0x32, 0x0d, 0x0a]
 
숫자버퍼 -> 버퍼.toString().split(" ")
 
숫자버퍼 -> ["1","2"]
 
let A = parseInt(stdinNumbers[0]);
let B = parseInt(stdinNumbers[1]);
console.log(A+B);
 
숫자버퍼의 각 요소를 정수로 변환한 후 합을 출력합니다.
 
 
위의 코드를 내 PC의 node.js로 실행하니 정상적으로 수행이 되었습니다. 그래서 백준 사이트에서 코드를 입력 하였는데 백준에서는 놀랍게도 다음의 결과가 나왔습니다.

 

TypeError 발생

javascript에서 TypeError는 함수에 잘못된 타입의 인수를 주었거나 객체에서 존재하지 않는 메소드를 호출한 경우에 발생합니다.(물론 TypeError가 발생하는 이유는 이것보다 더 다양합니다.) 내 PC에서는 제대로 동작하였는데 왜 백준에서는 에러가 발생했을까요? 몇 시간을 삽질한 결과 다음과 같은 가설을 세워보았습니다.

1.  Node.js 버전이 달라 구문해석을 잘못한 경우

    -> 다만 이경우에는 SyntaxError가 발생했을 것이므로 아니라고 판단하였습니다.

2.  Node.js에 모듈이 존재하지 않아 실패한 경우

    -> process 모듈이 존재하지 않는 경우는 확률이 너무 낮으므로 일단 패스하였습니다.

3.  이벤트 리스너가 중복 추가되어서 한 번 이벤트가 발생했을 때 이벤트 리스너가 중복 호출되는 경우

    -> 내 PC에서는 입력을 받은 자바스크립트가 이벤트 리스너를 호출하고 자연스럽게 종료합니다. node.js 프로세스가 종료되면 사용했던 자원은 다 반납되므로 추가했던 이벤트 리스너는 사라집니다. 즉 하나의 node.js 프로세스가 하나의 스크립트를 실행한 후 종료하므로 node.js를 100번 실행한다해도 이벤트 리스너가 중복 추가될 일은 없습니다. 하지만 백준 사이트에서는 하나의 node.js 프로세스가 여러개의 자바스크립트를 처리하여 각각의 자바스크립트들이 자원을 서로 공유할수도 있겠다라는 생각이 들었습니다. 자원을 공유한다는 것은 process 등의 객체를 공유한다는 것을 의미하므로 이미 이벤트 리스너가 등록된 process객체에 이벤트 리스너가 또 추가될 수 있는 상황인 것입니다.

이벤트 리스너가 중복 추가된 경우 한 번의 이벤트가 발생했을 때 이벤트 리스너가 여러 개 호출될 수 있습니다. 즉 stdin 객체에서 readable 이벤트에 대해 2개의 scanf 이벤트 리스너가 중복 추가된 경우 readable 이벤트가 발생하면 scanf가 2번 호출될 수 있다는 것입니다. 이렇게 되면 첫 번째 scanf에서 데이터를 이미 다 처리했기 때문에 2번 째 호출되는 scanf에서는 값을 읽어오지 못해 null 버퍼가 될 것이며  null 버퍼에서 toString 메소드를 호출할 수는 없으므로 TypeError 에러가 발생할 수 있겠다라는 생각이 들었습니다.

3번 가설이 맞는지 확인하기 위해 코드를 수정해보겠습니다. 리스너가 중복 추가되서 scanf가 여러번 호출되는 것이 문제이므로 scanf 함수를 호출할 때마다 등록된 리스너를 제거해주면 될 것 같습니다. 수정된 코드는 다음과 같습니다.

수정한 코드
stdin.removeListener("readable", scanf)
 
scanf 함수 처리가 끝나면 removeListener 메소드를 호출하여 stdin 객체의 readable 이벤트에 대한 scanf 이벤트 리스너를 제거합니다. 이렇게 코드를 수행하면 scanf 함수가 호출될 때마다 등록된 scanf 이벤트 리스너가 제거될 것이므로 이벤트 리스너가 중복 호출될 일은 없을 것입니다.
 
내 PC에서는 문제없이 동작합니다. 백준에서 코드를 수행해보겠습니다.
 
성공
 

PS. stdin.removeListener 메소드 대신 process.exit() 메소드를 호출해도 성공합니다. 아마 node.js 프로세스가 종료되면서 자원을 전부 반납하기 때문에 이벤트 리스너가 중복추가가 안되는 것으로 보입니다. 다만 처리시간이 기존코드보다 조금 오래 걸리는데 이 이유는 node.js 프로세스가 종료되면 node.js를 다시 재실행하는 과정을 거쳐서 시간이 조금 오래 걸리는 것이라고 추측해볼 수 있겠습니다.(아닐 수도 있습니다.) 수정된 코드는 다음과 같습니다.

process.exit() 메소드를 호출하는 코드

 

'Programing > Baekjoon 문제 알고리즘(node.js)' 카테고리의 다른 글

10869번 사칙연산  (0) 2022.02.24
10998 AxB  (0) 2022.02.24
10718번 We love kriii  (0) 2022.02.24
1001번 A-B  (0) 2022.02.24
2557번 Hello World  (0) 2022.02.22
Posted by englishmath
,

안녕하십니까. 오랜만에 포스팅을 다시 할려고 합니다. 포스팅 주제는 백준 온라인 저지에 올라와있는 알고리즘 문제 풀이입니다. 문제 풀이 순서는 정답자가 많은 문제부터 차례대로 풀어갈 예정입니다.

 

문제 : https://www.acmicpc.net/problem/2557 (백준 온라인 저지)

성공한 코드

이 문제는 크게 설명할 필요가 없으므로 넘기겠습니다.

'Programing > Baekjoon 문제 알고리즘(node.js)' 카테고리의 다른 글

10869번 사칙연산  (0) 2022.02.24
10998 AxB  (0) 2022.02.24
10718번 We love kriii  (0) 2022.02.24
1001번 A-B  (0) 2022.02.24
1000번 A+B  (0) 2022.02.23
Posted by englishmath
,

안녕하십니까 이번 포스팅에서는 직접 만든 지뢰찾기핵 프로그램을 이용하여 xp버전의 지뢰찾기를 한 번 해보도록 하겠습니다.

프로그램을 동작시켰습니다.

FindLM버튼을 누르니 지뢰를 전부 보여주는 군요.

InitTime버튼을 누르니 흘러간 시간이 초기화 되었습니다.

지뢰를 다시 숨기고 무적 버튼을 누르니 지뢰를 밟아도 게임이 계속 진행됩니다.

승리 버튼을 누르니 바로 승리하는 것을 볼 수 있습니다. 시간 초기화와 같이 쓰면 0초 랭킹도 가능하네요.

gameover버튼을 누르면 마스코트가 죽은 표정을 하고 있으며 더이상 게임이 동작되지 않는 것을 볼 수 있습니다.

이상으로 지뢰찾기핵 만들기 포스팅을 마치겠습니다.

Posted by englishmath
,

안녕하십니까 이번 포스팅에서는 지뢰찾기핵의 코드를 살펴보겠습니다.

자 이제 코드를 하나씩 살펴봅시다. 앞 포스팅에서 설명한 부분은 따로 설명드리지 않겠습니다.

- #define FindLM 0
  #define Stoptime 1
  #define initTime 2
  #define PowerOverwhelming 3
  #define victory 4
  #define gameover 5

버튼의 ID들을 미리 상수로 정의합니다.

- LRESULT WINAPI WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
BOOL WINAPI EnumDesktopProc(LPTSTR lpszDesktop,LPARAM lParam);
BOOL WINAPI EnumWindowsProc(HWND hwnd,LPARAM lParam);

윈도우 프로시저 함수와 작업관리자를 만들때 쓰인 EnumDesktopProc함수와 EnumWindowsProc함수를 선언합니다. 이는 바탕화면에서 지뢰찾기 프로세스를 찾기 위해 사용되었습니다. 원리는 작업관리자에서 쓰인 것과 동일합니다.

- int Errorprocessing(void);
  void codeinjection(DWORD FA,int byte,DWORD FP,DWORD GetAccessAddr);
  int timecheck(void);

  DWORD* finelandmine(DWORD number);
  DWORD* stoptime(DWORD killtimer);
  DWORD* initializationTime(void);
  DWORD* startTime(DWORD setTimer);
  DWORD* poweroverwhelming(void);
  DWORD* Cancel_poweroverwhelming(void);
  DWORD* Victory(void);
  DWORD* Gameover(void);

앞에서 설명드린 함수들을 선언합니다.

- DWORD G_LandMinePID = NULL;
  HWND G_MainHWND,G_POW,G_ST,G_FLM;

지뢰찾기의 PID를 전역변수로 저장하기 위한 G_LandMinePID변수와 버튼들의 핸들을 전역변수로 저장하기 위한 변수들을 선언합니다.

WinMain함수는 간단하니 생략하겠습니다. 바로 윈도우 프로시저로 넘어갑시다.

- DWORD address;

DLL에 들어있는 함수(SetTimer,KillTimer)의 주소값을 저장하기 위해 선언하였습니다. 대부분의 프로그램 코드에서 사용자가 임의로 만든 함수가 아닌 DLL에서 로드하여 호출하는 함수는 주소값이 정해져있지 않기 때문에 미리 DLL에서 함수의 주소를 구하여 그 값을 인자로 넣어 호출하는 식으로 동작해야 합니다.

- TCHAR ButtonTEXT[100] = L"";

버튼의 텍스트를 가져와서 저장시키기 위한 변수를 선언합니다.

- switch (message)
    case WM_CREATE:
   G_MainHWND = hWnd;
   G_FLM = CreateWindow(L"button",L"FindLM",WS_CHILD | WS_VISIBLE,0,50,105,25,hWnd,(HMENU)FindLM,NULL,NULL);
   G_ST = CreateWindow(L"button",L"StopTime",WS_CHILD | WS_VISIBLE,0,75,105,25,hWnd,(HMENU)Stoptime,NULL,NULL);
   CreateWindow(L"button",L"initTime",WS_CHILD | WS_VISIBLE,0,100,105,25,hWnd,(HMENU)initTime,NULL,NULL);
   G_POW = CreateWindow(L"button",L"PowerOverwhelming",WS_CHILD | WS_VISIBLE,0,125,140,25,hWnd,(HMENU)PowerOverwhelming,NULL,NULL);
   CreateWindow(L"button",L"victory",WS_CHILD | WS_VISIBLE,0,150,105,25,hWnd,(HMENU)victory,NULL,NULL);
   CreateWindow(L"button",L"gameover",WS_CHILD | WS_VISIBLE,0,175,105,25,hWnd,(HMENU)gameover,NULL,NULL);

어떤 코드를 지뢰찾기에 삽입할 건지를 사용자로부터 입력받기 위해 각각의 버튼들을 생성합니다. 이 중 버튼의 핸들을 전역변수에 저장시키는 버튼이 있는데 이는 버튼의 텍스트를 읽어올 필요가 있기 때문입니다.

- case WM_COMMAND:
   switch(LOWORD(wParam))
     case FindLM:

FindLM버튼을 눌렀을 경우 즉 사용자로부터 지뢰를 보여달라는 명령을 받았을 경우 아래의 코드를 수행합니다.

- EnumDesktops(GetProcessWindowStation(),EnumDesktopProc,0);
       if(Errorprocessing())
             break;

EnumDesktops함수를 호출하여 바탕화면에 실행중인 지뢰찾기 프로그램의 PID를 구합니다. 이렇게 구한 PID는 G_LandMinePID에 저장되어지며 이 G_LandMinePID값에 따라 다음 코드인 Errorprocessing함수에 의해 에러처리를 해주게 됩니다.

- GetWindowText(G_FLM,ButtonTEXT,100);

xp버전의 지뢰찾기가 동작되고 있다는 것이 확인되면 먼저 FindLM버튼으로부터 텍스트를 가져와 ButtonTEXT에 저장합니다.

- if(lstrcmp(ButtonTEXT,L"FindLM") == 0)
      codeinjection((DWORD)finelandmine,34,0x0A,NULL);
      SetWindowText(G_FLM,L"Cancel");

가져온 버튼의 텍스트가 FindLM이면 codeinjection함수를 호출합니다. 인자는 어셈블리코드가 적힌 finelandmine함수의 주소값이며 크기는 34바이트, finelandmine의 인자값은 16진수 A, 그리고 특정주소의 보안을 얻을 필요는 없으므로 마지막 인자값은 NULL로 주었습니다. 여기서 34바이트라는 크기는 finelandmine함수에 적혀있는 어셈블리 코드의 총 크기를 의미하며 칼 같이 계산할 필요는 없이 그냥 크기값을 예측하여 무조건 많이 주면 됩니다. 어차피 RETN코드로 인해 추가로 쓰인 코드는 실행되지 않습니다. 그리고 0x0A라는 값은 finelandmine에 적혀있는 0x01002F80함수의 인자값으로 들어가게 되는데 이 값은 지뢰를 0x0A라는 비트맵으로 보여달라는 뜻입니다. xp지뢰찾기에서 0x0A비트맵은 다음 모양을 의미합니다.

코드 인젝션을 수행하고 나면 FindLM버튼의 텍스트를 Cancel로 바꿉니다.

- else
    codeinjection((DWORD)finelandmine,34,0x0F,NULL);
    SetWindowText(G_FLM,L"FindLM");

가져온 텍스트 값이 FindLM이 아닐 경우 즉 Cancel일 경우 codeinjection함수를 호출합니다. 다만 이번에는 함수의 파라미터 값이 0x0F인데 이는 지뢰를 0x0F라는 비트맵으로 보여달라는 뜻입니다. xp지뢰찾기에서 0x0F비트맵은 다음 모양을 의미합니다.

즉 이 뜻은 사용자에게 보여주는 지뢰를 다시 감추라는 뜻이 됩니다. 다른 말로 위의 지뢰를 보여달라는 기능을 취소하는 것이지요. 이렇게 기능을 취소시키고 나면 SetWindowText함수로 버튼의 텍스트값을 다시 FindLM으로 복구시킵니다.

- G_LandMinePID = 0;

코드 인젝션 기법이 완료되었으면 G_LandMinePID변수를 초기화시킵니다.

- case Stoptime:

Stoptime버튼을 눌렀을 경우 아래의 코드를 수행합니다. G_LandMinePID값을 구하고 에러처리를 하는 부분은 위에서 설명하였으니 생략하겠습니다.
     
- GetWindowText(G_ST,ButtonTEXT,100);
     if(lstrcmp(ButtonTEXT,L"StopTime") == 0)

버튼의 텍스트가 StopTime일 경우 아래의 코드를 수행합니다.

- if(timecheck())
       MessageBox(hWnd,L"타이머가 동작중이 아닙니다.",L"Error",MB_OK);

timecheck함수를 호출하여 지뢰찾기의 타이머값을 체크합니다. 만약 참을 반환한다면 타이머의 값이 0이라는 뜻이므로 타이머가 동작중이 아님을 알리고 switch문을 빠져나갑니다.

- address = (DWORD)GetProcAddress(GetModuleHandle(L"user32.dll"),"KillTimer");

타이머가 동작중이라면 GetProcAddress함수를 호출하여 KillTimer함수의 주소값을 구해 address에 저장시킵니다. GetProcAddress함수를 살펴봅시다.

지정한 DLL에서 함수의 주소나 변수의 주소값을 구해주는 함수라고 되어있습니다. 인자들을 한 번 살펴봅시다.

* hModule

DLL 모듈의 핸들값을 인자로 받습니다. 보통 LoadLibrary함수나 GetModuleHandle함수를 사용하여 핸들값을 구하는데 두 함수의 차이점이 있다면 GetModuleHandle함수는 호출프로세스가 해당 DLL을 로드하고 있어야 함수가 성공한다는 점이고 LoadLibrary함수는 DLL의 경로를 인자값으로 주어야한다는 차이점이 있습니다. 기본적으로 비주얼 스튜디오로 만든 프로그램은 우리가 원하는 user32.dll을 로드하고 있으므로 그냥 GetModuleHandle함수를 사용하였습니다.

* lpProcName

모듈에서 찾을려는 함수나 변수명입다. 유니코드로 적어주시면 되겠습니다.

즉 이 코드가 호출되고 나면 KillTimer함수의 주소값이 address변수에 저장되어집니다.

- codeinjection((DWORD)stoptime,35,address,NULL);
  SetWindowText(G_ST,L"Cancel");

구한 주소값을 이용하여 codeinjection함수를 호출합니다. 이러면 stoptime함수의 인자값으로 address값(KillTimer 주소값)을 받게 됩니다. 코드를 삽입하고 나면 버튼의 텍스트를 Cancel로 바꿉니다.

- else
      address = (DWORD)GetProcAddress(GetModuleHandle(L"user32.dll"),"SetTimer");
      codeinjection((DWORD)startTime,35,address,NULL);
      SetWindowText(G_ST,L"StopTime");

버튼 값이 Cancel인 경우 즉 타이머가 KillTimer함수에 의해 파기되어 타이머가 더이상 동작되지 않는 경우 SetTimer함수의 주소값을 구해 startTime함수의 코드를 삽입합니다. 코드가 정상적으로 삽입이 되었다면 버튼의 텍스트를 StopTime으로 복구시킵니다.

모든 처리가 끝나면 G_LandMinePID변수값을 0으로 초기화합니다. 앞으로 이부분도 생략하도록 하겠습니다.

- case initTime:

   codeinjection((DWORD)initializationTime,40,NULL,NULL);

initTime버튼을 눌렀을 경우 initializationTime함수의 코드를 삽입합니다. 이 함수는 인자값이 없으므로 세번째 인자값을 NULL로 주었습니다.

- case PowerOverwhelming:

     GetWindowText(G_POW,ButtonTEXT,100);
     if(lstrcmp(ButtonTEXT,L"PowerOverwhelming") == 0)
           codeinjection((DWORD)poweroverwhelming,35,NULL,0x01003512);
           SetWindowText(G_POW,L"Cancel");

PowerOverwhelming버튼을 눌렀을 때 버튼의 텍스트 값이 PowerOverwhelming이면 poweroverwhelming함수의 코드를 삽입합니다. 이 때 네번째 인자값을 0x01003512로 주었는데 이는 poweroverwhelming함수가 값을 변경하는 0x01003591주소 값의 보안수치를 변경하기 위함입니다.

0x01003512주소는 xp지뢰찾기의 함수주소를 의미하고 0x01003591값은 0x01003512함수 주소 내에 포함되어 있으므로 0x01003512 함수의 보안 수치를 통째로 변경하기 위해 값을 이렇게 주었습니다. 이 때 0x01003512 함수의 총 크기는 165바이트입니다.코드를 삽입하고 나면 버튼의 텍스트를 Cancel로 변경합니다.

- else
     codeinjection((DWORD)Cancel_poweroverwhelming,35,NULL,0x01003512);
     SetWindowText(G_POW,L"PowerOverwhelming");

    
버튼의 텍스트값이 Cancel라면 Cancel_poweroverwhelming함수의 코드를 삽입후 버튼의 텍스트를 PowerOverwhelming로 복구시킵니다.

- case victory:
     codeinjection((DWORD)Victory,40,NULL,NULL);

victory버튼을 눌렀을 경우 Victory코드를 삽입합니다.

- case gameover:
     codeinjection((DWORD)Gameover,40,NULL,NULL);

gameover버튼을 눌렀을 경우 Gameover코드를 삽입합니다.

중요한 부분은 다 설명을 마쳤습니다. 다음은 EnumDesktopProc를 살펴봅시다.

- HDESK desktop;

데스크탑의 핸들을 선언합니다.

 - desktop = OpenDesktop(lpszDesktop,0,NULL,DESKTOP_READOBJECTS);

   EnumDesktopWindows(desktop,EnumWindowsProc,NULL);

   CloseDesktop(desktop);

데스크탑을 열고 EnumDesktopWindows함수를 호출한 후 데스크탑의 핸들을 닫습니다. 이부분은 작업관리자 코드에서 설명을 하였으므로 그냥 넘어가겠습니다.

마지막으로 EnumWindowsProc함수를 살펴봅시다.

- TCHAR titleName[500] = L"";
  WINDOWINFO Wininfo;

윈도우의 타이틀을 저장시킬 변수와 윈도우의 정보를 저장시킬 WINDOWINFO 구조체 변수를 선언합니다.

- Wininfo.cbSize = sizeof(WINDOWINFO);

Wininfo.cbSize멤버의 값에 구조체 크기를 넣어줍니다.

-  if(GetWindowTextLength(hWnd) == 0)
    return TRUE;
   if(GetParent(hWnd) != NULL )
    return TRUE;
   if(IsWindowVisible(hWnd) == FALSE)
    return TRUE;

타이틀값이 0이거나 자식 윈도우거나 보이지 않는 윈도우라면 TRUE를 반환하여 다음 윈도우를 검사합니다.

- GetWindowText(hWnd,titleName,500);

윈도우의 타이틀명을 가져옵니다.

- if(lstrcmp(titleName,L"지뢰 찾기") == 0)
    GetWindowInfo(hWnd,&Wininfo);

타이틀 명이 지뢰 찾기 라면 그 핸들의 정보를 가져와 Wininfo구조체에 저장시킨 후 아래의 if문을 수행합니다.

- if(Wininfo.wCreatorVersion == 1024)
    GetWindowThreadProcessId(hWnd,&G_LandMinePID);
    return FALSE;

wCreatorVersion멤버의 값이 1024라면 그 핸들의 PID를 구해 G_LandMinePID에 저장시키고 FALSE를 리턴하여 윈도우 검사를 중지합니다. wCreatorVersion멤버는 해당 윈도우가 만들어졌을 때의 윈도우즈 버전 값을 저장합니다. 윈도우즈 xp라면 값은 1024가 저장되며 윈도우즈 7은 다른 값이 저장됩니다. xp와 7지뢰찾기를 구분할 수 있는 아주 간단한 방법입니다.

- else
   G_LandMinePID = -1;
   return TRUE;

만약 찾은 지뢰 찾기 핸들이 xp버전이 아니라면 G_LandMinePID에 -1을 저장시키고 다음 윈도우를 찾습니다. 만약 xp버전의 지뢰찾기를 못찾았다면 G_LandMinePID는 -1을 저장시킨채로 검사가 종료되게 됩니다.

- return TRUE;

return FALSE문을 만나지 못했다면 만날 때 까지 혹은 윈도우를 전부 검사할 때까지 반복합니다.

자 여기까지가 코드의 마지막입니다. 다만 비주얼 스튜디오로 이 프로그램을 코딩해 실행할려면 하나의 작업을 더해주어야 합니다. 그것은 비주얼 스튜디오에서 기본적으로 제공해주는 기능인 증분 링크 기능을 해제시켜주는 것이지요.

프로젝트 - 속성 - 링커 - 일반 - 증분 링크 사용 부분을 아니오로 바꿔줍시다.

이 증분 링크 라는 것은 링크 시간을 줄여주는 기능을 의미합니다. 다만 이 기능을 사용하며 링크를 하게 되면 프로그램의 크기가 커지며 무엇보다 프로그램에 점프 썽크가 포함되어 진다는 것입니다.

그렇다면 점프 썽크란 무엇일까요? 말로만 설명하기 뭐하니 직접 보여드리겠습니다.

위에 있는 프로그램은 비증분링크로 짜여진 프로그램입니다.

위에 있는 프로그램은 증분링크로 짜여진 프로그램입니다. 딱봐도 JMP코드가 들어가 있는 것을 볼 수 있지요? 이 JMP코드가 바로 점프 썽크입니다. 이 점프 썽크의 특징은 함수를 호출하게 될 때 바로 함수의 주소로 가는 것이 아니라 이 점프 썽크를 통해 함수의 주소로 가게 됩니다.

즉 시작주소값이 0x12345678인 함수를 호출한다면 바로 0x12345678로 가는 것이 아니라 JMP 12345678 코드가 적힌 부문으로 간 다음에 이 JMP에 의해 0x12345678로 가게 되는 것이지요.

그렇다면 문제가 뭘까요? 자 한번 지뢰찾기에 코드를 한번 삽입해보도록 하겠습니다.

먼저 비증분링크로 짜여진 프로그램으로 winmine의 가상메모리에 코드를 삽입해보겠습니다. finelandmine에 적힌 어셈블리코드가 들어가 있는 것을 볼 수 있습니다. 본 코드가 34바이트보다 작기 때문에 다소 쓸데없는 값이 추가로 들어가긴 했지만 RETN 문이 있으므로 별 상관이 없습니다.

자 그럼 이제 증분링크로 짜여진 프로그램으로 코드를 삽입해봅시다.

finelandmine함수의 주소값을 이용하여 코드를 작성하도록 했지만 보시다시피 점프썽크로 인해 finelandmine함수의 주소값이 실질적인 주소값이 아닌 JMP주소값이 되버렸습니다. 그래서 그 JMP 코드가 적힌 부분부터 34바이트 코드를 집어넣는 것을 볼 수 있습니다.

이래놓고 스레드를 실행시키면 지뢰찾기가 특이한 코드를 버텨내지 못하고 에러가 나게 되는 것이지요. 없는 주소로 점프하라하니 지뢰찾기가 버티겠습니까?

그래서 보통 비주얼 스튜디오에서는 함수의 주소값을 이용하지 않은 쉘코드를 사용하여 코드 인젝션 기법을 수행합니다. 이 방법은 증분링크에 구애받지 않기 때문이지요.

자 아무튼 설명도 끝났고 비증분링크로 프로그램을 코딩하는 것도 마쳤으면 이제 프로그램 결과를 보는 것만 남았군요.

다음 포스팅에선 지뢰찾기핵 결과를 포스팅하도록 하겠습니다.

Posted by englishmath
,