안녕하세요. Message입니다.

오늘은 알고스팟의 WILDCARD(와일드카드) 문제를 Java로 풀이하는 포스팅입니다.

문제링크 : https://algospot.com/judge/problem/read/WILDCARD

 

 

1. 문제파악

이번 문제에서의 어려운 점은 교재에도 나와 있듯이 "" 문자를 어떻게 해결하느냐 입니다.

일반 문자와 "?" 문자는 1:1 매칭으로 해결되기 때문에 어려운점은 없습니다.

하지만 "" 문자가 나올 경우 크게 두가지 정도를 생각해야 합니다.

  ① "*" 문자가 파일명의 몇번째 인덱스까지 대응되는지 계산하는 로직

  ② "*******a" : "ab" 케이스처럼 많은 중복 계산이 발생하는 경우

1번은 어느정도 고민이 필요한 부분 같습니다만,  2번의 경우에는 동적계획법을 이용하면 될 것같습니다.

 

 

2. 일반문자, "?" 문자 매칭

일반문자와 "?" 문자는 1:1로 매칭해서 인덱스만 올려주면 되기 때문에 크게 어려울 건 없습니다.

하지만 이것을 반복문으로 구현할지, 재귀형태로 구현할지 선택지가 나뉘게 됩니다.

반복문으로 구현하고자 한다면, 아래와 같이 while문의 조건만 잘 성정해주면 문제 없습니다.

이후에 왜 반복문을 탈출했는지(문자 불일치, 길이초과, '*' 문자를 만난 경우 등) 확인해주면 됩니다. 

1
2
3
4
5
while(wIdx < wc.length && fnIdx < fn.length
    && (wc[wIdx] == '?' || wc[wIdx] == fn[fnIdx]) ){
        ++wIdx;
        ++fnIdx;
}
cs

 

재귀로 구현하고자 할 경우에는 다음 호출되는 함수에 증가된 인덱스를 넣어주면 간단하게 처리할 수 있습니다.

교재에서는 반복문보다 재귀로 구현할 경우 시간 복잡도 측면에서 유리하다고 나와 있네요.

(잘 이해는 안가지만요..)

1
2
3
if(wIdx < wc.length && fnIdx < fn.length)
    if(wc[wIdx] == '?' || wc[wIdx] == fn[fnIdx])
        return cache[wIdx][fnIdx] = match(wIdx+1, fnIdx+1);
cs

 

Tip. for문에서 전위/후위 증감연산자

for문에서 전위/후위 증감연산자는 동일한 기능을 수행합니다.

하지만 내부적인 동작원리가 다르기 때문에 전위 증감연산자를 사용하는 것이 좋습니다.

  - 전위 증감 연산자 : ① i=i+1 ② return i

  - 후위 증감 연산자 : ① const int temp=i ② i=i+1 ③ return temp

차이점을 살펴보면, 후위 증감 연산자의 경우 임시 변수인 temp를 만들어서 리턴합니다.

따라서 후위 증감 연산자를 사용할 경우 반복문 크기가 커질수록 비효율적입니다.

 

 

3. "*" 문자 처리

"*" 문자를 처리하는 방법도 반복문과 재귀 두가지로 구현할 수 있습니다.

반복문으로 구현할 경우 와일드카드의 인덱스는 1을 높여주어서 "*" 다음의 문자를 입력합니다.

그리고 일반 문자는 파일명의 길이를 넘지 않을 때 까지(fIdx+i<=fn.length) 1씩 증가시켜서 비교합니다. 

1
2
3
4
5
6
if(wc[wIdx] == '*'){
    for(int i=0; fIdx+i<=fn.length++i){ 
        if(match(wIdx+1, fIdx+i)) 
            return true;
    }
}
cs

 

재귀로 구현하는 케이스는 for문을 재귀함수로 고치는 과정에서 if문 안에 함수가 2개가 들어갑니다.

1
2
3
4
5
if (wc[wIdx] == '*') {  
    if (match(wIdx + 1, fnIdx) == 1 ||   
       (fnIdx < fn.length && match(wIdx, fnIdx + 1== 1))  
            return cache[wIdx][fnIdx] = 1;  
}
cs

 

즉, 2가지 중에 한가지만 만족해도 캐쉬에 값을 저장하고 1값을 리턴합니다.

  ① match(wIdx + 1, fnIdx) == 1 : 와일드 카드의 "*" 다음 문자가 파일명과 일치하는 경우 ex) r*ed vs red

  ② match(wIdx, fnIdx + 1== 1 : 와일드 카드는 변동 없고, 파일명의 인덱스만 증가 ex) r*ed vs rrrrrrrrrrrrrrred

저는 반복문을 재귀로 수정하면서 2번이 왜 필요한지 약간 헷갈렸습니다만,

만약 1번만 존재한다면 다음 호출되는 재귀함수에서 "*" 다음 문자와 파일명 문자가 불일치 할 경우 바로 종료되버립니다.

따라서 와일드카드의 인덱스를 유지하면서 파일명의 인덱스만 높여주는 부분도 필요합니다.

즉, 1번이 충족한다면 다시 위에서 구현한 1:1 매칭 로직으로 갈 것이고,

2번의 경우에는 와일드카드의 인덱스를 증가시키지 않았으니 다시 if (wc[wIdx] == '*') 구문을 만나면서

결과적으로 파일명의 인덱스만 증가한 효과를 볼 수 있습니다.

 

 

3. 동적계획법 적용

위의 코드에서도 재귀 호출 부분에 적용되어 있지만, 

동적계획법인 메모이제이션을 적용하여 구현한 코드는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package Algospot;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Scanner;
 
public class Main {
 
    char[] wc;    // 와일드카드 문자열
    char[] fn;    // 파일이름 문자열
    ArrayList<String> fileNameArray;
    
    /* 동적프로그래밍 저장 배열
     * -1 : 계산되지 않음
     *  1 : 일치
     *  0 : 불일치 */
    int[][] cache;
    
    
    Main(String wildCard, ArrayList<String> fileNameArray){
        this.wc = wildCard.toCharArray();
        this.fileNameArray = fileNameArray;
        this.cache = new int[101][101];
    }
    
    public int match(int wIdx, int fnIdx){
        
        // 동적 프로그래밍
        if(cache[wIdx][fnIdx] != -1)
            return cache[wIdx][fnIdx];
        
        // 1:1 대응, 일치하면 인덱스 증가 후 재귀호출
        if(wIdx < wc.length && fnIdx < fn.length)
            if(wc[wIdx] == '?' || wc[wIdx] == fn[fnIdx])
                return cache[wIdx][fnIdx] = match(wIdx+1, fnIdx+1);
        
        // 와일드카드와 파일명 모두 끝에 도달, 파일명도 끝에 도달했어야 일치
        if(wIdx == wc.length)
            return cache[wIdx][fnIdx] = (fnIdx == fn.length)? 1 : 0;
 
        // * 문자를 만난 경우
        if (wc[wIdx] == '*') {
            if (match(wIdx + 1, fnIdx) == 1 || 
               (fnIdx < fn.length && match(wIdx, fnIdx + 1== 1))
                return cache[wIdx][fnIdx] = 1;
        }
        
        return cache[wIdx][fnIdx] = 0;
    }
    
    public void solve(){
        
        ArrayList<String> resFileName = new ArrayList<String>();
        for(int i=0; i<fileNameArray.size(); i++){
            
            // 캐쉬 초기화
            for(int[] arr : this.cache)
                Arrays.fill(arr, -1);
            
            String temp = fileNameArray.get(i);
            fn = temp.toCharArray();
            if(match(0,0== 1)
                resFileName.add(temp);
        }
        
        // 정렬 후 출력
        Collections.sort(resFileName);
        for(String str : resFileName)
            System.out.println(str);
    }
    
    
    public static void main(String[] args) {
        
        Scanner sc = new Scanner(System.in);
        int numTextCase = sc.nextInt();
        
        for(int i =0; i<numTextCase; i++){
            
            String wildcard = sc.next();
            int numFileName = sc.nextInt();
            
            ArrayList<String> fileNameArray = new ArrayList<String>();
            for(int j=0; j<numFileName; j++)
                fileNameArray.add(sc.next());
            
            Main main = new Main(wildcard, fileNameArray);
            main.solve();
        }
    }
}
cs

 

 

4. 마무리

C++과는 달라서 그런진 모르겠지만, Java에서는 int 형과 boolean 형의 캐스팅이 안되더군요

따라서 리턴하면서 [ cache에 값을 저장하기 ② fnIdx가 fn.length와 일치하는지 확인 ]

2가지 작업을 한줄에 구현하려면 2번과 같이 구현해야 합니다.

   return cache[wIdx][fnIdx] = (fnIdx == fn.length); ==> 캐스팅에러 발생, 함수 리턴형이 boolean일때 가능

   return cache[wIdx][fnIdx] = (fnIdx == fn.length)? 1 : 0;

 

그리고 교재에서는 코드를 최적화 하기 위해 아래와 같이 if문에 재귀함수 호출이 여러개 들어가는 경우가 많은데

if (match(wIdx + 1, fnIdx) == 1 || (fnIdx < fn.length && match(wIdx, fnIdx + 1== 1))

저도 빨리 이런 효율적인 코드에 익숙해져야 겠군요...

 

 

posted By Message

Commit your way to the LORD, trust in him and he will do this. [PSALms 37:5]

redScreen.tistory.com Blog :( 

 

posted by Red_Message

안녕하세요. Message입니다.

오늘은 알고스팟의 JUMPGAME(외발뛰기) 문제를 Java로 풀이하는 포스팅입니다.

문제링크 : https://algospot.com/judge/problem/read/JUMPGAME

 

 

1. 재귀를 이용한 완전탐색 + 동적 계획법 

이 문제를 푸는 포인트는 재귀를 이용한 완전탐색을 구현한 뒤에

이미 탐색한 결과를 재활용하는 메모이제이션(Memoization)을 이용하여 탐색 횟수를 줄여야합니다.

만약 동적 계획법을 적용하지 않았을 경우에는 아래처럼 엄청난 차이가 발생하게 됩니다.

당연히 제한 시간인 2초를 넘겨버리기 때문에 '시간초과' 문구가 뜨게됩니다.

  

 

2. Java로 구현 

언제나 말로 풀어쓴 문제를 코드화 하는건 익숙해지기 전까지 참 어려운 일인듯 합니다.

어느정도 머리로 이해가 됐어도 막상 구현하려고 하면 손이 얼음이 되니까요.

아래 2가지를 먼저 구상하고 코드를 구현하면 어느정도 길이 잡힐 듯 싶네요.

1. 지속적으로 호출되는 다음 재귀 함수에 어떤 값을 넣어줄 것인가? -> 게임판 x, y 좌표값

2. 재귀함수를 탈출하는 조건은 무엇인가?

   - 목표가 달성되었을 경우 --> 함수의 인자로 받은 좌표값이 게임판의 끝인 경우

   - 예외가 발생한 경우 --> 함수의 인자로 받은 좌표값이 게임판을 벗어난 경우

3. 무엇을 리턴할 것인가? --> 목표에 도달했으면 True, 예외가 발생한 경우 False

 

이를 바탕으로 구현한 코드는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package Algospot;
import java.util.Arrays;
import java.util.Scanner;
 
 
public class Main {
    int[][] matrix;
    int[][] cache;
    int n;
    
    Main(int[][] matrix){
        n = matrix.length;
        this.matrix = matrix;
        this.cache = new int[n][n];
        for(int[] arr : this.cache)
            Arrays.fill(arr, -1);
    }
    
    public boolean solve(int x, int y){
        
        // 게임판의 범위를 벗어남
        if(x >= n || y >= n) return false;
        
        // 목표지점에 도달
        if(x==n-1 && y==n-1return true;
        
        // 메모이제이션
        if(cache[x][y] == 1return false;
        else cache[x][y] = 1;
        
        int jump = matrix[x][y];
        return solve(x+jump, y) || solve(x, y+jump);
    }
    
    
    public static void main(String[] args) {
        
        int[][] matrix;
        int n;
        
        Scanner sc = new Scanner(System.in);
        int numTestCase = sc.nextInt();
        
        for(int i = 0; i<numTestCase; i++){
            n = sc.nextInt();
            matrix = new int[n][n];
            for(int x=0; x<n; x++){
                for(int y=0; y<n; y++){
                    matrix[x][y] = sc.nextInt();
                }
            }
            
            Main main = new Main(matrix);
            boolean res = main.solve(0,0);
            System.out.println(res? "YES":"NO");
        }
    }
}
 
cs

 

개인적으로 구현한 코드와 알고리즘 문제해결 전략 책에 나온 답안이 얼추 비슷하긴 했지만

책에 소개된 리턴 방식이 좀더 간결해 보여서 적용했습니다.

==> return solve(x+jump, y) || solve(x, y+jump);

함수 리턴값이 Boolean 형이라면 이런 패턴을 기억하고 있다가 써먹으면 좋을 듯 싶네요.

아래 구문은 구글링을 하다보니 알게된 문법입니다. 알아두면 간결한 코드를 짜는게 도움이 될 것 같습니다.

System.out.println(res? "YES":"NO");

 

언제나 결과만 보면 뭐 이런걸로 고민을 했나 싶을 정도로 간단해보이네요..ㅜ

이만 포스팅을 마칩니다.

 

 

posted By Message

Commit your way to the LORD, trust in him and he will do this. [PSALms 37:5]

redScreen.tistory.com Blog :( 

 

posted by Red_Message

안녕하세요. Message입니다.

오늘은 순열 알고리즘을 Java로 구현하는 부분에 대해 간단히 포스팅 하고자 합니다.

개인적으로 조합보다 순열 알고리즘을 이해하는데 좀 더 시간이 걸렸고, 생각해야 될 부분이 다소 있다고 생각되네요.

 

 

1. n개 중에서 r개를 선택한다.

순열은 n개에서 r개를 선택하는 알고리즘입니다.

단순히 3~4개를 가지고 경우의 수를 만드는 케이스라면 알고리즘까지 구현할 필요성을 전혀 못느끼겠으나,

10개만 넘어가도 경우의 수가 엄청나게 불어나기 때문에 알고리즘 구현이 필수적입니다.

순열은 아래와 같이 2가지로 분류할 수 있습니다.

=====

중복을 허용하지 않고 n개중에 r개 뽑기

중복을 허용하여 n개중에 r개 뽑기

=====

 

중복 허용과 비허용이 그저 순열 알고리즘을 더 어렵게 만드는 부분이라고 생각될 수 있지만

실제로 선택해야 될 아이템들에 따라 개발자에게는 매우 중요한 요소가 될 수 있습니다.

 

 

2. 재귀함수 + ArrayList를 이용한 순열

순열의 점화식을 살펴보면 아래와 같습니다.

0 ≤ r ≥ n을 만족하는 정수 n, r이 있을 때, 순서를 고려하여 r개의 원소를 뽑음

=====

P(n, r) = n(n-1)(n-2) ... (n-r+1)

P(n, r) = n! / (n-r)!

=====

 

단순히 개수를 고려한다면 당장 n에 숫자를 대입하여 풀 수 있겠으나,

이를 코드로 구현하기 위해서는 집합에 있는 특정 아이템을 일일이 매칭해주어야 하므로

우리는 코딩의 관점에서 위의 점화식의 n을 자료구조 안에 들어있는 원소로 생각할 필요가 있습니다.

예를 들면 아래와 같이 생각을 확장할 수 있겠습니다.

=====

① P(5, 3) = 5 × 4 × 3

② P(5, 3) = 5개의 아이템중 1개를 선택 × 4개의 아이템중 1개를 선택 × 3개의 아이템중 한개를 선택

③ P(5, 3) =

[ 1, 2, 3, 4, 5 ] 중에서 '1' 선택 × [ 2, 3, 4, 5 ] 중에서 '2' 선택 × [ 3, 4, 5 ] 중에서 '3' 선택 = 123

[ 1, 2, 3, 4, 5 ] 중에서 '1' 선택 × [ 2, 3, 4, 5 ] 중에서 '2' 선택 × [ 3, 4, 5 ] 중에서 '4' 선택 = 124

[ 1, 2, 3, 4, 5 ] 중에서 '1' 선택 × [ 2, 3, 4, 5 ] 중에서 '2' 선택 × [ 3, 4, 5 ] 중에서 '5' 선택 = 125

[ 1, 2, 3, 4, 5 ] 중에서 '1' 선택 × [ 2, 3, 4, 5 ] 중에서 '3' 선택 × [ 2, 4, 5 ] 중에서 '2' 선택 = 132

[ 1, 2, 3, 4, 5 ] 중에서 '1' 선택 × [ 2, 3, 4, 5 ] 중에서 '3' 선택 × [ 2, 4, 5 ] 중에서 '4' 선택 = 134

[ 1, 2, 3, 4, 5 ] 중에서 '1' 선택 × [ 2, 3, 4, 5 ] 중에서 '3' 선택 × [ 2, 4, 5 ] 중에서 '5' 선택 = 135

....(모든 경우의 수를 구할때 까지 반복)

※ 선택된 아이템은 바로 출력하거나, 모든 아이템이 fix된 이후에 출력해도됨, 확정이라고 생각해도 무방함

=====

 

무언가 반복적인 패턴이 보인다면, 이를 재귀적으로 구현할 수 있을겁니다.

'주어진 자료구조에서 원소를 선택' 하는 기능을 구현하고,

다음 호출되는 재귀함수에 선택한 아이템을 뺀 나머지 집합을 넘겨주면 될 것 같습니다.

주의해야할 점은, 재귀함수가 끝난 이후에는 그 다음 계산을 위하여 remove한 원소를 다시 원래 index에 넣어줘야 하는 점입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.ArrayList;
import java.util.Arrays;
 
public class Permutation {
 
    private int n; // nPr의 n
    private int r; // nPr의 r
    private ArrayList<Integer> itemList;
    private int[] res;
    
    // 초기화
    public Permutation(int[] intArr, int r){
        this.r = r;             // nPr의 r
        this.n = intArr.length// nPr의 n
        this.res = new int[r];  // 결과값 배열
        
        // 아이템이 들어갈 리스트
        itemList = new ArrayList<Integer>();
        for(int item : intArr)
            itemList.add(item);
    }
    
    public void perm(int depth){
        perm(itemList, 0);
    }
 
    public void perm(ArrayList<Integer> itemList, int depth) {
        
        // depth가 0부터 시작했을 경우 depth == r에서 리턴
        if (depth == r) {
            System.out.println(Arrays.toString(res));
            return;
        }
        
        for (int i = 0; i < n-depth; i++){
            res[depth] = itemList.remove(i); // 아이템 선택 + 리스트에서 제거
            perm(itemList, depth+1);         // 재귀호출
            itemList.add(i, res[depth]);     // 제거된 아이템 복원
        }
    }
    
    public static void main(String[] args) {
 
        int r = 3;
        int[] arr = {1,2,3,4,5};
        
        Permutation main = new Permutation(arr, r);
        main.perm(0);
    }
}
cs

 

 

그렇다면 여기서 중복을 허용한 코드는 어떻게 해야할까요?

삭제하고 다시 복원해주는 부분을 없애고, i의 범위를 0부터 n까지로 수정해주면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public void perm(ArrayList<Integer> itemList, int depth) {
        
    // depth가 0부터 시작했을 경우 depth == r에서 리턴
    if (depth == r) {
        System.out.println(Arrays.toString(res));
        return;
    }
    
    for (int i = 0; i < n; i++){
        res[depth] = itemList.get(i); // 아이템 선택 + 리스트에서 제거
        perm(itemList, depth+1);            // 재귀호출
    }
}
cs

 

 

2. 재귀함수 + 위치변경(swap)을 이용한 순열

위의 코드는 제가 직관적으로 구현한 코드지만, 구글링을 해보면 swap 메소드를 이용한 순열 알고리즘이 많습니다.

요소와 요소를 자리 바꾸고, depth 변수를 이용하여 선택할 수 있는 요소의 범위를 조절합니다.

 

=====

P(4, 2) =

(첫째[depth]↔첫째(i) swap) [ 1, 2, 3, 4 ] 중에서 '1' 선택 × (둘째[depth]↔둘째(i) swap) [ 1, 2, 3, 4 ] 중에서 '2' 선택 = 12

(첫째[depth]↔첫째(i) swap) [ 1, 2, 3, 4 ] 중에서 '1' 선택 × (둘째[depth]↔셋째(i) swap) [ 1, 3, 2, 4 ] 중에서 '3' 선택 = 13

(첫째[depth]↔첫째(i) swap) [ 1, 2, 3, 4 ] 중에서 '1' 선택 × (둘째[depth]↔넷째(i) swap) [ 1, 4, 3, 2 ] 중에서 '4' 선택 = 14

(첫째[depth]↔둘째(i) swap) [ 2, 1, 3, 4 ] 중에서 '2' 선택 × (둘째[depth]↔둘째(i) swap) [ 2, 1, 3, 4 ] 중에서 '1' 선택 = 21

(첫째[depth]↔둘째(i) swap) [ 2, 1, 3, 4 ] 중에서 '2' 선택 × (둘째[depth]↔셋째(i) swap) [ 2, 3, 1, 4 ] 중에서 '3' 선택 = 23

(첫째[depth]↔둘째(i) swap) [ 2, 1, 3, 4 ] 중에서 '2' 선택 × (둘째[depth]↔넷째(i) swap) [ 2, 4, 31 ] 중에서 '4' 선택 = 24

(첫째[depth]↔셋째(i) swap) [ 3, 2, 1, 4 ] 중에서 '3' 선택 × (둘째[depth]↔둘째(i) swap) [ 3, 2, 1, 4 ] 중에서 '2' 선택 = 32

(첫째[depth]↔셋째(i) swap) [ 3, 2, 1, 4 ] 중에서 '3' 선택 × (둘째[depth]↔셋째(i) swap) [ 3, 1, 2, 4 ] 중에서 '1' 선택 = 31

(첫째[depth]↔셋째(i) swap) [ 3, 2, 1, 4 ] 중에서 '3' 선택 × (둘째[depth]↔넷째(i) swap) [ 3, 4, 12 ] 중에서 '4' 선택 = 34

※ 본인과 본인이 swap 하는 부분이 없으면, 최초 케이스가 누락됨 ex) [1, 2, 3, 4]에서 swap되어 [2, 1, 3, 4] 부터 계산 시작

※ depth가 0부터 시작했을 때, depth == r 이 되면 r개 만큼 출력함, 위에서는 r이 2이므로, index가 0~1까지인 원소 2개만 출력

=====

 

위와 같이 구현하기 위해서는

for문을 이용하여 순차적으로 현재 depth 원소와 i 번째 원소를 swap 한 뒤에 (자연스럽게 swap 되어 첫번째로 옮겨진 원소가 선택됨)

다음으로 호출되는 재귀 함수에는 증가된 인덱스(depth+1)를 넘겨주면 범위가 좁혀지면서 문제를 해결할 수 있습니다.

여기서도 주의해야할 점은, swap 메소드 때문에 원소들의 순서가 변경되기 때문에 이를 다시 되돌려 주어야 합니다.

이를 고려한 Java 코드는 아래와 같습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.Arrays;
 
public class Permutation {
 
    private int n; // nPr의 n
    private int r; // nPr의 r
    private int[] res;
    
    // 초기화
    public Permutation(int n, int r){
        this.n = n;
        this.r = r;
        res = new int[r];
    }
    
    public void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
 
    public void perm(int[] arr, int depth) {
        
        // depth가 0부터 시작했을 경우 depth == r에서 리턴
        if (depth == r) {
            System.out.println(Arrays.toString(res));
            return;
        }
        
        for (int i = depth; i < n; i++) {
            swap(arr, depth, i);     // 스왑
            res[depth] = arr[depth]; // 선택된 원소 저장
            perm(arr, depth +1);     // 재귀호출
            swap(arr, depth, i);     // 복원
        }
    }
    
    public static void main(String[] args) {
 
        int r = 3;
        int[] arr = {1,2,3,4,5};
        
        Permutation main = new Permutation(arr.length, r);
        main.perm(arr, 0);
    }
}
 
 
cs

 

 

그렇다면 중복을 허용하는 순열을 알아봅시다.

위에서는 for문 제어부에 있는 i 초기값이 depth 였기 때문에, 깊이가 증가할수록 선택할 수 있는 원소의 개수가 줄었습니다. 

==>  for(int i=depth; i<n; i++)

따라서 i의 초기값을 0으로 변경해주면 depth가 올라가도 선택할 수 있는 원소의 개수가 동일하기 때문에 중복을 허용하게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void perm(int[] arr, int depth) {
        
    // depth가 0부터 시작했을 경우 depth == r에서 리턴
    if (depth == r) {
        System.out.println(Arrays.toString(res));
        return;
    }
        
    //for (int i = depth; i < n; i++)
    for (int i = 0; i < n; i++) {
        swap(arr, depth, i);     // 스왑
        res[depth] = arr[depth]; // 선택된 원소 저장
        perm(arr, depth +1);     // 재귀호출
        swap(arr, depth, i);     // 복원
    }
}
cs

 

출력해보면 swap 메소드의 영향으로 111, 112, 113... 으로 순서가 이쁘게 출력되진 않지만

중복을 허용하는 경우의 수가 모두 출력됩니다.

이만 포스팅을 마칩니다.

 

 

 

posted By Message

Commit your way to the LORD, trust in him and he will do this. [PSALms 37:5]

redScreen.tistory.com Blog :(

 

posted by Red_Message

안녕하세요. Message입니다.

한동안 큐브리드와 파이썬에서 인코딩 문제로 골머리를 썩었습니다.

특히 아래 오류는 이제 보기만 해도 지긋지긋하네요.

 UnicodeDecodeError : Ascii codec can't decode byte 0xea in position 0: ordinal not in range(128)

 

인코딩이라는 주제가 처음엔 이해가 되는가 싶어도 검색 시간이 늘어날수록 혼란이 늘어나더군요.

문제를 해결했다고 해도, 일주일만(사실 하루이틀..) 지나면 까먹고선 똑같은 검색어로 구글링하고 있습니다. 

지난주에 봤던 블로그가 어디더라 하면서 즐겨찾기를 하지 않은 스스로를 원망합니다 ㅜ

짧게라도 필요했던 부분을 정리해서 포스팅하겠습니다. 인코딩의 늪에 빠져 스트레스 받고 계시는 분들에게 도움이 되길 바랍니다.

 

 

1. 그래서 "인코딩" 이라는게 무엇인고

사실 IT 분야에서 Ascii, Unicode, UTF8, Base64, URL 등 인코딩과 관련된 단어는 많이 보고 들을 수 밖에 없기 때문에

책에서 스치듯 읽은 내용이나 주워들은 지식만으로 문자 인코딩을 알고있다고 생각했습니다.

하지만 인코딩에 대한 정확한 이해가 없다보니 읽어들인 Hex값이 뭘 의미하는지, 

어떤 방식으로 인코딩/디코딩을 해야 내가 원하는 문자가 화면에 딱 나오는지 알수가 없어 막막하더군요.

이번 기회에 제가 주관적으로 이해한 인코딩에 대해 적어보겠습니다.

 

1) 정의 살펴보기

일단 가장 객관적인 인코딩의 정의를 보고 넘어갑니다.

"특정한 문자 집합 안의 문자들을 컴퓨터 시스템에서 사용할 목적으로 일정한 범위 안의 정수들로 변환하는 방법"

분명 나는 IT를 전공했고, 한국인임에도 불구하고 이게 무슨말인지 이해가 안갑니다.

일단 지금 당장은 한글 UTF8이 3바이트로 저장된다거나 유니코드의 한글 범위가 어디서 어디까지다..라는

상세한 내용은 접어두고 인코딩의 원리를 이해하는데 초점을 맞추어 하나씩 실습해보겠습니다.

 

2) 직접 테스트하기

변수에 "안녕"이라는 한글 문자열을 UTF8 인코딩으로 저장하면 어떤 바이너리 형태로 저장될까요?

Python IDLE을 이용하여 확인해봅니다. "\x" 문자가 붙으면서 16진수 형태로 저장되네요.

어떤 원리로 인코딩이란게 되어 저런값이 최종적으로 나왔는지는 아직 모릅니다.

 

그럼 반대로 UTF8로 인코딩된 Hex값을 직접 입력해서 출력해봅니다. "안녕"이 찍히네요...(헐)

 

신기해서 "0xEC"만 입력 했더니 "ㅇ" 는 출력이 안됩니다. (ㅋㅋ) 사실 이부분이 중요하다고 느낀건,

1byte를 출력하면 외계어가 출력되지만 3바이트를 모두 모아서 출력하면 한글이 출력된다는 사실입니다.

 

여기서 파이썬이 사용자 입력한 "안녕" 문자열을 "0xEC9588 0xEB8595"으로 저장하기 위해 매칭한 표가 바로

인코딩의 정의에서 읽어도 이해가 되지 않았던 "문자 집합" 입니다.

아래 테이블은 한글 문자에 해당하는 UTF8, Unicode 값과 매치 해놓은 표입니다.

그림출처 : http://www.utf8-chartable.de/unicode-utf8-table.pl

 

사용자가 "감" 이라는 문자를 유니코드로 저장하려 합니다. 만약 우리가 파이썬이라면 어떻게 인코딩 해주어야 할까요?

그럼 위의 테이블에서 "감"에 매칭되는 유니코드에 해당되는 Hex 값인 "0xAC1" 으로 저장해주면 될겁니다.

항상 의심의(?) 눈초리로 직접 확인해봅니다. 앞에 "u"라는 문자가 추가로 붙기는 하지만 값은 맞습니다.

 

만약 encode 함수를 사용하지 않고 변수에 "안녕" 문자열을 할당하게 되면

위에서 보았던 UTF8이나 Unicode가 아닌 또다른 문자 집합으로 인코딩하여 저장합니다. (지정해주지 않았으니까요..)

 

아마 한글을 표현할 수 있는 또다른 인코딩 형태인듯 합니다.

처음에는 한글에 많이 쓰이는 EUC-KR인줄 알았으나, 실제로 아래와 같이 확인해보면 'CP949' 임을 알 수 있습니다.

(이래서 인코딩은 확인 또 확인..!!___2017.07.17 수정)

 

코드페이지 949는 마이크로소프트(MS)에서 도입한 문자 집합이며, Ks_c_5601-1987으로도 불립니다. (위키백과)

사실 CP949는 EUC-KR의 확장이기도 하고, 하위 호환이 되기 때문에 착각할 여지가 있습니다.

아래 EUC-KR의 코드표에서 '감' 문자를 찾아봐도 "0xB0A8"으로 위의 IDLE에서의 결과값과 동일합니다.

 

출처 : http://www.mkexdev.net/Community/Content.aspx?parentCategoryID=4&categoryID=14&ID=125

 

한글이 저장되는 방식이 네가지(Unicode, UTF8, EUC-Kr, CP949)나 나왔습니다. (사실 UTF-8은 유니코드의 저장 방식중 하나지만요)

만약 프로그래밍을 하다가 웹이 되었든 DB가 되었든 읽어들인 값이 "\xb0\xa8" 이었다면

이것이 EUC-KR(CP949)로 인코딩 되었다는 것을 알아야 우리가 원하는 UTF8이나 Unicode 인코딩 형식으로 바꿀 수 있습니다.

만약 이러한 개념이 없다면 무슨 인코딩 방식인지도 모른채 엄한 문자 집합 함수를 남발하게 될것이며...

그럼에도 불구하고 자꾸 한글은 깨지고 UnicodeDecodeError를 보는 횟수는 늘어나고, 시간은 하염없이 흘러갈 확률이 높습니다.. (또르르..)

 

 

2. Unicode, UTF8에 대하여

이쯤에서 다시한번 인코딩의 정의를 짚어보고 넘어갑니다.

"특정한 문자 집합 안의 문자들을 컴퓨터 시스템에서 사용할 목적으로 일정한 범위 안의 정수들로 변환하는 방법"

그러니까...이걸 조금 쉽게 말하면

사용자가 문자를 입력하고 문자집합은 이거야! 라고 정해주면 시스템이 문자집합 테이블에 매칭되는 정수(or Hex)로 저장해주는거군요?

(만약 문자집합을 지정해주지 않으면 시스템이나 프로젝트의 기본 로케일을 따르겠고요, 위에서는 CP949였죠)

이제 인코딩에 대해 쌀 한톨만큼은 알게된 것 같습니다. 적어도 인코딩의 흐름은 안것 같군요.

그렇다면 한글 입/출력시 가장 골머리 썩는 Unicode, UTF8, EUC-KR에 대해 좀 더 상세하게 알아보겠습니다.

 

1) Unicode

유니코드를 헷갈리지 않기 위해서는, 유니코드가 어떤 바이트 인코딩 방식이 아니라는 점을 먼저 알아야 합니다.

유니코드는 문자코드가 각국의 윈도우마다 겹치는 영역이 존재하기 때문에 이러한 현상이 발생하지 않기 위해

전세계 모든 언어에 겹치지 않는 코드를 할당(=매핑)한 코드입니다. 유니코드의 문자 집합은 위에서 이미 보았습니다.

그중에 우리가 자주 쓰는 '한글'이 할당받은 유니코드 범위는 "AC00 ~ D7AF" 인거죠.

니코드 형식으로 저장하면 전세계 언어를 깨짐 없이 제공할 수 있겠군요.


 

2) UTF8

UTF8은 유니코드 기반의 가변 길이 문자 인코딩 방식입니다. 즉, 유니코드를 변환해서 UTF8로 만들었다는 뜻입니다.

그렇게 만든 이유는 유니코드를 8비트 단위에로 만들기 위한 목적이며,

값이 큰 유니코드는 8비트 안에 전부 다 안들어가다 보니(?) 여러개의 바이트로 표현합니다.

(자연스럽게 UTF16은 16비트 기반으로 저장하기 위한 인코딩)

사실 예전부터 "UTF8 한글은 3byte" 라고 외우기만 했지 이런 의미가 있는지는 몰랐습니다.

아래 표를 보면 UTF8이 유니코드 문자 범위에 따라 1 ~ 4 바이트를 사용하는것을 알 수 있으며,

ASCII의 범위는 0 ~ 127 이라서 8비트로 표현이 가능하기 때문에 UTF8에서도 1byte로 표현되며,

"AC00 ~ D7AF" 범위인 한글은 1byte, 2byte로 표현 안되다 보니 3byte가 되었습니다.

이때 바이트 수가 여러개임을 판단 하기 위해 아래표 처럼 UTF8 형식이 지정되어 있습니다.

파란색 숫자는 바이트 수를 의미하며, x로 표현된 곳은 UTF8로 인코딩되기 위해 유니코드 값이 쪼개져서 들어갈 위치입니다.

여기서 중요한 사실은, UTF8 인코딩 방식은 유니코드를 쪼개서 만드는 방식이기 때문에 무조건 유니코드를 한번 거쳐야 합니다.

즉, 유니코드가 아닌 문자열을 UTF8로 만드는 함수에 넣었을 경우 우리가 자주 보았던 인코딩 에러가 발생할겁니다. ()

Tip : UTF8 첫번째 바이트에서 1의 개수는바이트 수를 의미 :: 0 1byte, 110 2byte, 1110 3byte, 111104byte 

 

"안" 문자의 경우에 유니코드값은 "U+C548" 이며, UTF8 인코딩 값은 "0xEC9588" 입니다.

그렇다면 Unicode를 UTF8로 변환하기 위해 한글 3byte의 x 값에 유니코드 값을 순서대로 대입합니다.

그리고 실제로 변환이 오류없이 잘되는지 IDLE로 확인해봅니다.

11101100 10010101 10001000 = 0xEC9588 = "안"

(C = 11005 = 01014 = 01008 = 1000)

 

 

3. 인코딩 변환 에러 케이스별 분석

 

1) encode 함수

이쯤에서 우리가 저지르기 쉬운 실수를 살펴보면, 웹/DB/사용자입력 등에서 얻어온 문자열을

UTF8로 저장하기 위해 encode("UTF-8") 함수를 사용했는데 오류가 발생하는 케이스입니다.

이젠 보기만 해도 짜증이 치밀어 오르는 UnicodeDecodeError 입니다.

(파이썬 3.x 버전에서는 유니코드로 통일되어 아래 코드가 오류가 안날겁니다)

 

하지만 인코딩에 대해 어느정도 공부했으니 보이는게 있지요.

에러문을을 살펴보면, Ascii codec"0xbe"디코딩 하려고 했는데 Ascii 범위의 밖이라서 오류가 났다는 겁니다.

여기서 알 수 있는 사실은 2가지 있습니다.

=====

  ① encode("UTF-8") 함수를 사용했다고 해서 입력값을 "1110xxxx"의 x자리에 값을 우겨 넣어 인코딩을 해주지 않는다.

  ② encode 함수를 실행시키면 기본적으로 Ascii로 디코딩을 수행한 뒤 인코딩을 한다.

=====

따라서 사용자가 입력한 값을 디코딩해서 "안녕"을 처음 입력했을때 처럼 순수한(?) 값으로 되돌려 놓은 다음(뒤에서 다시 설명)

우리가 원하는 UTF 인코딩 과정에 돌입하게 됩니다. 그렇다면 위의 오류는 어떻게 해야 해결될까요?

에러를 다시 보면 "0xbe"를 디코딩 못했고, 우리가 아는 "안" 문자는 각 인코딩별로 값이 아래와 같습니다.

=====

  ① Unicode = "0xC548"

  ② UTF8 = "0xEC9588"

  ③ EUC-KR = "0xbec8"

=====

그렇다면 위의 값을 보고 추측하건데, 내가 입력한 "안" 문자가 EUC-KR(or CP949)로 입력(후 인코딩) 되었고,

그것을 파이썬에 기본 설정되어 있는 아스키 코덱으로 디코딩 하려다 뻗어버렸다고 합리적인(?) 추측을 해볼 수 있습니다.

 

그렇다면 우리가 직접 디코딩 함수를 통해 EUC-KR로 디코딩 해준뒤 UTF8로 인코딩 해주면 어떻게 될까요?

왜 오류가 났는지 파악이 되어 쉽게 해결이 되었습니다.

하지만 다른 상황에서 UnicodeDecodeError가 발생했을 시 EUC-KR 디코딩이 만사해결책은 아닐겁니다.

 

UTF8 인코딩 값은 유니코드를 대입해서 만들기 때문에 위의 코드가 동작하기 위해서는

"안녕".decode("EUC-KR") 함수는 유니코드값을 반환한다는 의미가 되는데, 이것도 실제로 확인해봅니다.

 

그렇다면 일단 유니코드로 저장하게 되면 EUC-KR이나, UTF8, UTF16 등등 다른 인코딩 값으로 쉽게 변환 가능하다는 소린데,

실제로 문자열을 유니코드로 저장한 뒤 종류별로 인코딩을 수행해봅니다. 오류없이 잘 동작하네요.

 

2) str, unicode 함수

먼저 str 함수에 한글을 넣으면 어떻게 저장될까요? 저의 경우에는 EUC-KR(or CP949)로 저장됩니다.

아마 이렇게 EUC-KR로 저장되는건 시스템의 기본 로케일이 적용되었을 가능성이 높습니다.

 

그럼 앞에서 유니코드로 저장해두면 다른 인코딩으로 변환하기 쉽다고 하였으니,

"안녕" 이라는 문자열을 유니코드로 저장하기 위해 unicode 메소드를 실행하면 어떻게 될까요?

 

쉽게 넘어가는게 하나 없네요. 이 환경에서 unicode 함수를 이용하여 유니코드 값을 얻기 위해선 디코딩을 해줘야 합니다.

차라리 선언부터 a = u"안녕" 이라고 선언하는게 더 쉬울지도 모르겠지만, 다른곳에서 문자를 받아오는 경우도 많으니까요. 

 

그렇다면 유니코드 계열인 UTF8 인코딩 문자를 유니코드로 다시 돌리는건 에러 없이 스무스하게 될까요?

아니요. 에러가 납니다...이쯤되면 인코딩 하려면, 허락받고 해야될 거 같습니다..

 

하지만 UTF8의 경우에는 유니코드 계열이나 보니, "UTF8" 이라고 인자를 넣어주면 친절하게 변환해줍니다. (휴)

아니면 decode("UTF-8") 함수를 사용해도 됩니다. 

 

 

4. 마무리..

여기저기 인코딩에 대해 검색하다 가장 기억에 남는 말이 있습니다. 

"이놈의 인코딩은 해결해도 본전이며 못하면 시간낭비" 라고요...ㅎㅎ

아마 개발을 하시는 분들이라면 한번쯤은 정리하셨겠지만... 뒤늦은 기초의 중요성을 체감합니다.

아무쪼록 인코딩 문제로 머리 싸매고 계시던 분들에게 조금이라도 도움이 되셨길 바래요.

 

 

posted By Message

Commit your way to the LORD, trust in him and he will do this. [PSALms 37:5]

redScreen.tistory.com Blog :(

 

posted by Red_Message

안녕하세요. Message입니다.

webhacking.kr 문제를 풀이하다보니, 디코딩이 필요한 문제가 있었습니다.

결과적으로 해당 문제는 XOR 디코딩이 정답이 아니었지만요

평소에 간단한 도구는 직접 코딩해서 사용하는게 좋다고 생각해왔는데

생각만 하면 평생 안할거 같은지라...

이참에 GUI 기반의 간단한 XOR 디코딩 툴을 파이썬으로 제작하는 과정을 포스팅 하려고 합니다.

 

GUI부분은 PyQt4를 이용하였습니다.

RiverBank : PyQt4 Download : https://riverbankcomputing.com/software/pyqt/download

 

 

------------------------------------------------------------------------------------------------------------------------------------------

1. 제작과정

------------------------------------------------------------------------------------------------------------------------------------------

 

1) PyQT4 + QT Designer

저도 파이썬으로 GUI 기능을 구현하는것은 처음이라 이것저것 알아봤더니

PyQT4를 많이 쓴다고 하길래 일단 써보기로 했습니다.

PyQT4를 설치하면 Qt Designer 라는 프로그램을 사용하여 아래와 같이 디자인을 할 수 있습니다.

아래와 같이 기초적인 디자인을 끝냅니다.

 

 

2) Python 코드로 변환

QT Deginer으로 대략적인 디자인을 완성하면,

C:\Python27\Lib\site-packages\PyQt4\uic 폴더의 pyuic.py 을 이용하여 파이썬 코드로 변환이 가능합니다.

ex) C:\Python27\Lib\site-packages\PyQt4\uic> python pyuic.py  -x message.ui  -o message.py

혹시 모듈 에러가 나시면, 파이썬과 PyQT가 64비트인지 확인하세요,

저의 경우 Python 2.7 - 32bit로 설치되어 있어서 오류가 났더랬습니다.

 

성공하면 아래와 같이 기본적인 코드가 생성되니 매우 편리합니다.

이제 필요한 부분을 수정하거나, 추가해주면 됩니다.

 

 

3) List +Model 구현

Xor 디코딩한 결과값을 Gui 로 넘겨서 Lit로 구현해야 하는데,

PyQT에서 좋은 예제 소스를 제공해주고 있습니다.

Window키를 눌러 시작메뉴에서 생성된 폴더를 살펴보면 Examples and Demos 를 발견할 수 있습니다.

 

 

Demo를 실행시키면 아래와 같이 샘플의 분류가 나옵니다. 제가 참고한 부분은 Item View 입니다.

 

각자가 필요한 부분이 있겠지만, 제가 리스트를 구현하는데 필요한 부분은

Basic Sort/Filter Model 이라는 샘플에서 찾을 수 있었습니다.

 

해당 샘플에서 리스트를 QStandardItemModel 모델을 이용하여 구현하였는데,

많은 예제 소스들이 트리구조가 없어도 Qtree를 이용한 부분이 의아하긴 했지만, 완성된 소스가 유용했습니다. 

아래의 레퍼런스를 참고하여 리스트 Row 값 추가, 삭제, 초기화 등을 구현했습니다.

URL : file:///C:/Python27/Lib/site-packages/PyQt4/doc/html/qstandarditemmodel.html

 

4) Xor 연산

현재 구현하고 있는 탭의 가장 중요한 기능입니다. 바로 Xor 연산이죠.

처음에는 아래와 같이 했더니, XOR Key 값의 범위가 1byte(0~255) 까지만 연산이 되더군요

  for i in range(0, len(text)):     

     result = ord( text[i] ) ^ key  

그래서 쉬프트연산자 [ >> ] 를 이용하여 Key 값을 Low, High로 나누어

범위를 2byte(0x00 ~ 0xFFFF) 까지 확장했습니다.

 keyLow = 0x00FF & key           

 keyHigh = (0xFF00 & key) >> 8

보통 XOR 디코딩이 필요할 경우, Key값이 그리 크지 않은 경우가 많으니 어느정도 커버가 될겁니다.

  result = ord( text[i] )     ^  key_low  

  result = ord( text[i+1] ) ^  key_high 

 

------------------------------------------------------------------------------------------------------------------------------------------

2. 마무리

------------------------------------------------------------------------------------------------------------------------------------------ 

 

Ver 1.0

일단 지정된 범위까지 Xor 디코딩을 해주는 부분과, ASCII + Unicode를 모두 표현해주는 기능까지는 완성됐습니다.

ASCII 값의 범위(0~127)가 넘어가는 경우에는 그냥 공백으로 나오길래

Hex 값을 보여주는 툴들을 참고하여 "." 으로 대체했습니다.

 

지금은 Red_Seek 이 바쁘지만,

나중에 시간이 생기면 기능도 보완하고, 새로운 기능은 탭을 늘려나가면서 추가하면 좋을 것 같습니다.

만드는게 생각보다 간단해서 초보인데도 불구하고 2~3일 정도 노력하니 콘솔창을 탈출하는게 성공했네요!

탭을 늘리거나 다른 툴을 제작할때도 좋은 발판이 될 것 같습니다.

 

Ver 1.1

BASE64 인코딩이나 디코딩이 여러번 중첩되는 경우

놓치는 부분을 최소화 하기 위해 , XOR 연산처럼 횟수를 지정하여 볼 수 있는 탭을 추가했습니다.

 

posted by Red_Message

이번에는 ARP 패킷을 만들고 전송하는 부분까지 하겠습니다.

 

간단하게 절차(?)를 써보자면

  1. 지난번에 검색한 네트워크 장치들중 내보낼 장치 선택
  2. ARP 패킷 만들기
  3. 이더넷 프레임 만들기
  4. ARP 패킷에 이더넷 헤더 씌우기
  5. 1번에서 선택한 네트워크 인터페이스로 패킷 전송

이런 순으로 진행되겠네요.

 

 

1. 프로젝트 열기

- 첫번째 포스팅에서 만든 TestApp 이라는 프로젝트를 이용하겠습니다.

 

가. Form 디자인하기 

- 정말 간단하게 만들겠습니다.

- [시작] 버튼을 누르면 패킷이 전송되고, [중지] 버튼을 누르면 패킷 전송이 중지되는 기능으로 하겠습니다.

나. 속성

- [시작], [중이] 버튼의 Text 및 Name은 편하실때로 하시면 되겠습니다.

- 저같은 경우 Name은 각각 btnARPStart, btnARPStop 이라고 하였습니다.

 

2, 코딩하기

- [시작] 버튼을 더블클릭하게 되면 코드를 작성할 수 있는 화면으로 바뀌게 됩니다.

 

가. 참조할 라이브러리 가져오기

- 두번째 포스팅에서 라이브러리 참조하는 방법을 알아보았습니다.

- 사용하기 위하여 지시문을 작성할 필요가 있습니다.

- 코드 작성 부분에서 맨위에 보시면 using ~~~~ 이런식으로 이미 작성되어 있는 코드가 보이실겁니다. (1~9 줄)

- 그아래 작성하였습니다. (13~14 줄)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
 
using System.Net;
using System.Net.NetworkInformation;
using PacketDotNet;
using SharpPcap;
using System.Threading;
cs

 

나. 장치 선택하기

- 두번째 포스팅에서 제가 사용한 인터페이스 인덱스 번호를 저는 1번이라고 정하였습니다.

- 이제 해당 인터페이스를 가져오겠습니다.

1
2
3
4
5
6
7
8
9
10
11
// Millisecond 단위 이며 초기연결 지연설정
int readTimeout = 1000;
 
// 현재 단말기의 네트워크 장치의 리스트들을 불러온다.
CaptureDeviceList devices = CaptureDeviceList.Instance;
 
// 무선 랜카드의 인덱스 번호는 1번(단말기 설정에 따라 다름)
ICaptureDevice device = devices[1];
 
// 무선 랜카드를 프러미스큐어스 모드로 연다.
device.Open(DeviceMode.Promiscuous, readTimeout);
cs

- 2 줄 : 인터페이스를 초기 연결할때 지연시간을 주어도 되고 주지 않아도 된다는데 저는 설정하겠습니다.

- 5 줄 : 익숙하지 않나요? 두번째 포스팅에서 장치 검색하는 부분입니다. 대신 devices 변수는 배열 형식으로 리스트를 저장하게 됩니다. 그렇기 때문에 제가 인덱스 번호를 확인하라고 했었습니다.

- 8 줄 : 저는 1번으로 선택합니다.

- 11 줄 : 1번 인터페이스를 열어줍니다. 보시면 DeviceMode 를 설정할 수 있습니다. Normal과 Promiscuous 모드가 있습니다.

Normal은 일반모드로 장치를 열겠다는 의미입니다. 중요한 것은 Promiscuous 모드입니다.

Promiscuous 모드로 장치를 설정하면 보통은 자신을 목적지로 되어있는 정보만 받게되고 나머지는 버리게 되는데, Promiscuous 모드는 자신의 것 이외의 모든 정보를 검증없이 받아볼 수 있게 됩니다. 스니핑공격의 가장 기본 옵션이기도 합니다. 와이어샤크에서도 이 기능을 확인하실 수 있습니다. ( http://redscreen.tistory.com/entry/SEEK-네트워크-해킹-및-보안-2 : 한창 공부할때 잠시 언급했었습니다.)

패킷을 전송할것이기 때문에 Normal 로 하시든 상관없습니다. 모드가 두가지 있다는 것을 보여드리기 위해서 저렇게 작성하였습니다.

그리고 2번째 줄에서 설정한 지연시간을 인자로 주게되면 초기연결 시 인자값 만큼 지연되게 됩니다.

 

다. 패킷 만들기

1) ARP

- ARP 패킷의 구조는 어떻게 되는지 먼저 살펴 보겠습니다. 

- 패킷을 만들기 위해서 입력해주어야 하는 부분으로 Opcode(requset 인지 reply 인지), 송신측 MAC, IP, 수신측 MAC, IP 이렇게 5가지 입니다.

 

2) 이더넷

- 이더넷의 구조를 살펴 보겠습니다. 

- 이더넷 프레임을 만들기 위해서 입력해주어야 하는 부분으로 송신측 MAC, 수신측 MAC, Type 입니다.

 

3) 코딩하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
IPAddress dstIP = null;
IPAddress srcIP = null;
PhysicalAddress dstMac = null;
PhysicalAddress srcMac = null;
 
dstIP = IPAddress.Parse("100.0.0.100");
dstMac = PhysicalAddress.Parse("AA-AA-AA-AA-AA-AA");
srcIP = IPAddress.Parse("111.0.0.111");
srcMac = PhysicalAddress.Parse("BB-BB-BB-BB-BB-BB");
 
ARPPacket arp = new ARPPacket(ARPOperation.Response, dstMac, dstIP, srcMac, srcIP);
EthernetPacket eth = new EthernetPacket(srcMac, dstMac, EthernetPacketType.Arp);
arp.PayloadData = new byte[] { 00000000000000000000 };
eth.PayloadPacket = arp;
device.SendPacket(eth);
cs

- 1~4 줄 : IP, MAC 주소 입력을 위해 해당 형식의 변수 지정부분

- 6~9 줄 : 괄호 안의 문자열을 각 IP, MAC 변수에 맞는 형태로 저장하는 부분 (이 부분을 공격자가 임의로 조작하면 되겠지요?)

- 11 줄 : ARP 패킷을 만드는 부분입니다.

입력 순서대로

[ Opcode , TargetHardwareAddress , TargetProtocolAddress(IP) , SenderHardwareAddress , SenderProtocolAddress(IP) ]

Opcode 는 Request, Response(Reply) 둘중 선택할 수 있습니다.

- 12 줄 : 이더넷 프레임을 만드는 부분입니다.

입력 순서대로

[ SourceHardwareAddress , DestinationHardwareAddress , Type ]

ARP 패킷을 담아서 전송할 것이니 Type을 Arp 로 합니다.

- 14 줄 : 이더넷 프레임에 ARP 패킷을 담아줍니다.

- 15 줄 : 위쪽에서 Promiscuous 모드로 열었던 인터페이스로 이더넷 프레임을 전송합니다.

 

라. 결과

- 와이어샤크로 확인해보겠습니다. 

 

- 저는 for 문을 통하여 10개를 전송하였습니다.

- 사진 보시면 하드코딩하였던 내용들이 그대로 담겨서 와이어샤크에서 확인되었음을 볼수 있습니다.

 

 

Visual Studio 에서 C# 의 기본적인 조작법은 저도 아직 공부단계에 있기 때문에 작성하지 않았습니다.

 

솔직히 SharpPcap 사용하시려고 저희 블로그 와주실 정도라면 저보다 훨씬 잘하실게 뻔하기 때문이죠...ㅎㅎ;

 

아무튼 오늘 드디어 원하는 패킷을 만들어서 전송까지 해보았습니다.

 

저도 처음에 놓쳤던 부분이 ARP 패킷만 만들어서 device.SendPacket(arp); 이런식으로 바로 보내려고 하니까 안되어서 고생했습니다.

 

이제와서보면 너무 기초적인 부분을 놓쳤기 때문에 막혔던 것이 너무나 부끄럽네요.....ㅜ 이더넷...ㅠ

 

저와 같은 실수 하실분은 없으시겠지만, 만약을 위해 그리고 나중에 제가 또 같은 실수 하지 않길 바라지만.. 혹시나.. 기록하기 위해...ㅎㅎ

 

posted by Red_Seek

지난 포스팅에서 추가한 SharpPcap.dll 과 PacketDotNet.dll 에서

 

SharpPcap.dll 를 이용하면 현재 사용자 단말기의 네트워크 인터페이스 정보를 볼수 있습니다.

 

이번에는 콘솔 응용 프로그램으로 네트워크 인터페이스 정보를 확인해보겠습니다.

 

 

1. 프로젝트 생성

- 프로젝트 생성 시 "콘솔 응용 프로그램"을 선택하였습니다. 

- 프로젝트 생성 후 SharpPcap.dll 라이브러리 추가 해주세요. (이전 포스팅 참고)

 

 

2. 코드 작성

- 프로젝트 생성 시 초기 상태입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace sharppcapEX
{
    class Program
    {
        static void Main(string[] args)
        {
            
        }
    }
}
cs

 

- Main 함수 부분에 작성을 하면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace sharppcapEX
{
    class Program
    {
        static void Main(string[] args)
        {
            // SharpPcap 버전
            string sharppcapversion = SharpPcap.Version.VersionString;
            Console.WriteLine("SharpPcap {0} version", sharppcapversion);
            Console.WriteLine("========================================");
 
            // 네트워크 장치 검색
            var networkInterface = SharpPcap.CaptureDeviceList.Instance;
            if (networkInterface.Count < 1)
            {
                Console.WriteLine("No network interface.");
                return;
            }
 
            // 네트워크 장치 정보 출력
            foreach (var interfaceInfo in networkInterface)
            {
                Console.WriteLine("{0}", interfaceInfo);
                Console.WriteLine("========================================");
            }
        }
    }
}
 
cs

 

 

3. 결과

- 아래의 결과화면에서 스크롤 내리시면 검색된 인터페이스의 정보들을 확인하실 수 있습니다.

- 맨위 인터페이스 부터 인덱스 0 번 부터 시작한다고 생각하시면 됩니다. (말씀 드린 인덱스 번호는 다음 내용에 사용됩니다.)

 

 

 

저는 노트북을 사용중이며 무선 네트워크를 이용하여 ARP 패킷을 보낼 예정입니다.

 

이번 포스팅을 통하여 저의 무선 네트워크의 인덱스 번호가 1번임을 확인하고 넘어가도록 하겠습니다.

 

본인의 네트워크 인터페이스 순서는 다 다릅니다. 본인이 사용하실 인터페이스 번호 확인하시면 되겠습니다.

 

 

 

posted by Red_Seek

안녕하세요.

 

정보보안 교육을 마치고 그동안 ARP Spoofing Tool 을 만들어보면서 공부를 하였는데요.

 

코딩을 하면서 골치아팠던 부분 몇가지 공유하고자 합니다. (저만 골치아픈거일수도 있어요...)

 

 

먼저, ARP 스푸핑을 하기 위해 Request 와 Reply 패킷을 보내는 두가지 방법이 있는데 저는 Reply 패킷을 이용하여 코딩을 해보았습니다.

 

사용한 언어는 C# 입니다. Visual Studio 2015 사용하였습니다.

 

라이브러리는 스니핑에 많이 사용하시는 SharpPcap 을 사용하였습니다.  (첨부파일 확인)

https://sourceforge.net/projects/sharppcap/

SharpPcap-4.2.0.bin.zip

 

 

 

1. SharpPcap 라이브러리 추가하기 (for Visual Studio 2015)

가. 프로젝트 생성

- 저는 Windows Forms 으로 생성을 하였습니다. 

 

 

나. 상단 메뉴 [프로젝트] -> [참조 추가] 선택

 

 

 

다. 아래의 "찾아보기" 선택하여서 SharpPcap.dll 과 PacketDotNet.dll 을 추가해줍니다.

- 첨부파일 다운 받으시면 SharpPcap.dll 과 PacketDotNet.dll 있습니다.

- 아래 사진처럼 추가하신후 확인 버튼누르면 완료입니다.

 

 

 

 

posted by Red_Seek