안녕하세요. 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
2017. 5. 7. 18:29 :: 취약점

안녕하세요. Message 입니다.

오늘 포스팅 주제는 Java와 관련된 CVE에서 자주 등장하는 ysoserial 도구에 대한 내용입니다.

CVE-2015-4852 취약점 포스팅에 포함시키려 하였으나, 생각보다 분량이 많아져서 따로 작성하게 되었네요.

현재 이슈되고 있는 내용은 아니지만, 제가 모르는 내용이 많아서 배울것이 많은 주제였습니다. 

주요 골자는 아래 웹사이트에 있는 내용을 참고하였고, 필요한 개념과 디버깅 부분을 추가하여 포스팅을 진행하겠습니다.

 

OSINT :: Understanding the ysoserial's CommonsCollections1 exploit

http://opensources.info/understanding-the-ysoserials-commonscollections1-exploit/

 

 

 

  0x00  Ready

 

 1. Y SO SERIAL? - ysoserial.jar

 ysoserial은 해외 연구원들이 프레임워크상에서의 RCE 발생과 관련된 연구 결과를 입증하기 위해 제작한 개념증명 도구입니다.

 이 툴을 이용하면 Java 프로그램에서 임의의 사용자 명령을 실행할 수 있게 해주는 페이로드를 생성할 수 있습니다.

 Java의 직렬화(Serialization)/역직렬화(Deserialization)에서 발생하는 취약점을 다루다 보니 이름에 serial이 붙은걸로 추정되지만

 Github 대문에 조커와 비슷한 이미지가 있는걸로 보아 우리가 생각하는그 대사를 노린게 분명해보입니다 ㅎㅎ

 URL : https://github.com/frohoff/ysoserial

 

 

 

 2. Gadget Chain

 ysoserial은 직렬화/역직열화 과정에서 Commons-Collections 라이브러리의 InvokerTransformer를 악용합니다.

 만약 서버의 Java Class Path에 InvokerTransformer가 필수 가젯으로 포함된 응용 프로그램이 운영되고 있을 경우 

 공격자는 ysoserial을 이용하여 얻은 가젯체인(gadget chain)을 직렬화하여 서버로 전달할 방법을 고안할 것입니다.

 (여기서 가젯체인이라는 용어가 생소하지만, OSINT에서는 Method Sequence로 설명하고 있습니다.

 시퀀스의 각 메소드를 Gadget이라 하고, 사용자의 명령(ls, wget 등..)이나 InvokerTranformer등을 하나의 Gadget으로 봅니다.)

 데이터가 서버로 성공적으로 전달되어 역직렬화 되는 과정이 진행된다면, 결과적으로 Runtime.exec() 메소드를 호출하게 됩니다.

 아래 그림은 ysoserial의 gadget chain을 표현한 그림입니다. 지금은 이해가 안되더라도 한번 쓰-윽 살펴보고 넘어갑니다.

 

 

 

 3. 필요 배경지식

 ysoserial 도구는 아래의 클래스들과 디자인 패턴등을 이용하여 페이로드를 생성합니다.

 만약 이들에 대한 이해도가 부족하다면 ysoserial 페이로드가 실행되는 매커니즘을 완전히 이해하기 힘들 수 있습니다.

 1) JDK : ① AnnotationInvocationHandler  ② Proxy  ③ Map  ④ Override  ⑤ InvocationHandler  ⑥ Runtime

 2) Commons Collections : ① LazyMap  ② Transformer  ③ ChainedTransformer  ④ InvokerTransformer

 3) ETC.. : ① Java Serialization and Deserialization  ② ObjectInputStream - readObject()

 

 

 

   0x01  Payload only Execute

 

 1. RCE 예제 실행

 OSINT의 첫번째 예제를 실행시켜보겠습니다.

 해당 코드는 CommonsCollection1 옵션으로 생성한 페이로드의 핵심 부분이며, 여기에 20~30%만 덧붙이면 온전한 페이로드가 됩니다.

 CommonsCollection1 옵션은 ysoserial을 실행시키면 가장 앞에 있는 페이로드 생성 옵션입니다. (아래 그림은 ver.0.0.2 기준)

 해당 옵션은 CVE-2015-4852 취약점에서 사용되는 페이로드이기도 합니다.

 

 

 Commons-Collection 3.2.1 버전을 다운받아 라이브러리에 추가시킨 뒤 이클립스로 실행시켜보았습니다.

 아직 동작원리는 파악되지 않지만, String배열에 할당해준 사용자 명령어("calc.exe")가 정상 실행되는군요.

 다시 한번 상기해보면 ysoserial 도구는 Runtime.exec() 메소드 호출을 통해 InvokerTransformer를 악용하여 RCE를 발생시키는 도구이므로,

 아래의 코드 역시 결국 Runtime.exec() 메소드를 호출하는 구조일겁니다.

 

 

 위의 코드를 이해하려면 getMethod() + invoke() 메소드가 어떤 기능을 수행하는지 알아야합니다.

 아래 코드는 String.class에서 getMoethod 메소드로 length() 메소드 객체를 얻어와 invoke()로 실행시키는 간단한 예제입니다.

 invoke() 메소드는 클래스의 정보를 얻어와 동적으로 메소드를 실행시킬 수 있는 자바의 리플렉션(Reflection) 기능의 핵심입니다.

 리플렉션에 대한 자세한 설명은 아래쪽에서 하겠습니다. 일단 예제의 실행 결과는 length 메소드 실행 결과와 동일한 "9" 입니다.

 

 

 

 2. 예제 분석

 아래 사진은 위의 소스코드를 좀더 가독성 있게 정리한 결과입니다. 조금더 보기 수월해졌나요?

 일단 Transformer형 배열에 ConstantTransformer, InvokerTransformer를 할당하고 ChainedTransformer 객체에 인자로 넣어주고 있습니다.

 ConstantTransformer는 말그대로 상수 객체를 반환하며, InvokerTransformer는 invoke() 메소드를 이용한 결과값을 객체로 반환합니다.

 어떻게 invoke() 메소드를 이용하여 객체를 반환하는지는 아래에서 상세하게 보겠습니다.

 

 

 이어지는 코드에서는 LazyMap의 decorate 메소드를 호출하여 ChainedTransformer를 인자로 넣어준 뒤 LazyMap.get("문자열")을 호출합니다.

 LazyMap의 API 설명을 살펴보면, Map의 객체를 decorator 패턴으로 확장/생성할 수 있도록 설계된 메소드입니다.

 get 호출시 넘긴 key 값과 매칭되는 value값이 없으면 ChainedTransformer(factory)가 해당 인자를 이용하여 새로운 객체를 생성합니다.

 이때 ChainedTransformer안에 있는 Transformer들이 가지고 있는 값들(Runtime.class, getMethod, invoke...)을 이용합니다.

 

 

 실제로 get메 소드를 호출하며 넣어준 임의의 key값인 "message"는 매칭되는 value가 없으므로

 ChainedTransformer안에 들어 있는 4개의 Transformer들의 trnasform 메소드가 객체를 생성하기 위해 연쇄적으로 호출됩니다.

 아래 소스코드를 보면 map.cotainsKey(key) == false 일때 factory.transform 메소드가 실행됨을 알 수 있습니다.

 

 

 transform 함수는 메소드의 반환값을 다음 실행되는 transform의 인자로 넘기게 되는데

 Java의 decorator 디자인 패턴과 동일하게 아래와 같이 4단계로 확장되는 방식입니다.

 디자인 패턴까지 상세하게 다룰 수 없으므로, 관련된 블로그를 소개합니다 → 데코레이터 패턴 :: http://jdm.kr/blog/78

 ①  Runtime.class  // → ConstantTransformer

 ②  Runtime.class.getMethod("getRuntime", new Class[0])  // → InvokerTransformer

 ③  Runtime.class.getMethod("getRuntime", new Class[0]).invoke(new Class[]{ Object.class, Object.class},new Object[0]))

 ④  Runtime.class.getMethod("getRuntime", new Class[0]).invoke(new Class[]{ Object.class, Object.class},new Object[0])).exec("calc.exe");

 

 

 실제로 이렇게 일렬로 쭈욱 만들어진 코드를 실행해보면 계산기(calc.exe)가 진짜(?) 실행됨을 알 수 있습니다.

 여기 까지 진햄함으로서 우리는 LazyMap에 ChainedTransformer를 인자로 넣어주고 get 함수를 호출하면

 RCE가 발생할 조건이 성립됨을 알았습니다. 관건은 타겟 서버 LazyMap의 get 함수를 어떻게 호출시키느냐 일겁니다.

 그나마 희소식은 여기까지 잘 이해했다면 ysoserial gadget chain을 핵심부분을 이해했다고 볼 수 있습니다.

 

 

 

 

 

 

  0x02  Putting it all together

 

 1. CommonsCollections1 페이로드

 CommonsCollections1 옵션으로 생성한 페이로드 + 직렬화/역직렬화까지 포함한 풀버전(?)을 살펴보겠습니다.

 생각보다 길지만, 직렬화/역직렬화를 위한 파일입출력을 제외하면 위에서 분석한 예제의 내용이 상당수 포함되어 있습니다.

 

 

 

 

 2. Main

 main에서는 위에서 공부한 내용이 포함된 객체(evilObject)를 생성합니다.

 해당 객체에 대한 자세한 내용은 아래에서 다룹니다. 

 

 

 이후에 serializeToByteArray, derializeFromByteArray 메소드를 이용하여 evilObject 객체의 직렬화/역직렬화를 수행합니다.

 역직렬화를 하기 위한 deserializeFromByteArray 내부의 ObjectInputStream.readObject 메소드에서 RCE가 발생하게됩니다.

 

 

 getEvilObject의 상단 코드는 바로 위에서 본 내용입니다. 다만 RCE를 발생시키는 lazymap.get() 메소드가 없습니다.

 표면적으로는 없어졌지만, 다른 메소드를 타고 타고 넘어가다보면 결국 어디선가는 나올겁니다.

 lazymap.get() 대신 새롭게 추가된 코드를 살펴보면 AnnotationInvocationHandler, Constructor, InvocationHandler, Proxy 등이 보입니다.

 라인수는 얼마 안되지만 각종 디자인 패턴과 캐스팅이 많이 포함되어 있기 때문에 처음에는 무슨 내용인지 파악하기 힘들 수 있습니다.

 

 

 

 

 3. secondInvocationHandler :: 첫번째 Handler

 

 ① 리플렉션(Java Reflection)을 이용한 객체 생성

 새롭게 추가된 코드의 첫부분은 클래스의 이름 "sun.reflect.annotation.AnnotationInvocationHandler"을 이용하여 

 secondInvocationHandler 객체를 생성하는 부분이 나오는데, 이를 이해하기 위해서는 리플렉션(Java Reflection) 개념에 대해 알아야합니다.

 리플렉션은 동적인 방식을 이용하여 Java의 유연성을 높이는 기능이며, 프레임워크에서 자주 사용되는 방식입니다.

 Java는 아래의 java.lang.reflect 패키지를 통해 리플렉션 기능을 제공하고 있습니다.

 

 

 리플렉션에서 제공하는 동적 기능은 클래스의 이름만으로 해당 클래스의 다양한 정보를 얻어오고,

 중간에 보았던 invoke() 메소드 등을 활용하여 상황에 따른 메소드를 호출할 수 있는 기능등을 말합니다.

 이러한 동적 기능을 이용하게 되면, 클래스의 변경이 자주 일어나는 코드에서 일일이 소스코드를 수정해서 배포하지 않아도 되며

 상황에 따라 입력으로 받은 메소드 이름을 이용하여 로직의 변화 없이 새로운 기능을 제공해줄 수도 있습니다.

 보안과는 별개로 사용 방법에 따라 매우 편리한 기능이라 할 수 있습니다.

 

 리플렉션을 이용하면 클래스의 이름/제어자/정보/부모클래스/생성자/메소드/변수의 정보를 얻을 수 있으며

 스프링 프레임워크에서 자주 사용되어 친숙한 Annotation(주석의 일종, @RequestMapping 등) 객체도 얻을 수 있습니다.

 

 

 위에서 설명한 리플렉션의 기능을 원활하게 사용하기 위해서는 클래스의 정보를 얻을 수 있는 API가 필요합니다.

 이를 위해서 자바에서는 java.lang.Class라는 클래스를 제공해주고 있으며,

 일반적으로 아래 예제와 같은 형태로 객체의 정보를 얻어냅니다. (우리가 분석할 evilObject에서도 동일한 패턴이 사용됩니다)

 코드를 살펴보면, forName() 메소드에 String 클래스의 이름을 넣어주고 Class 객체를 얻었습니다.

 이후에 getDeclaredConstructors() 메소드를 이용하여 String 클래스에 정의된 생성자들을 Constructor<?> 객체 형태로 얻습니다.

 

 

 getDeclaredConstructors() 메소드는 배열 형태로 생성자(Constructor)들을 반환해주며,

 배열 인덱스([0], [1], [2]...)를 이용하면 원하는 생성자만 얻어낼 수 있습니다. (오버로딩 때문에 생성자가 여러개일 수 있으므로)

 이때 결과값은 다양한 생성자를 고려하여 모든 타입을 수용하는 <?> 제네릭(Generics) 형태입니다.

 어떤 생성자들이 반환됐는지 출력해보면 아래와 같습니다. 개인적으로 제가 원했던 new String("문자열")의 형태는 [12]에 있었네요.

 

 

 

 ② 본문코드 분석

 이제 evilObject에서 AnnotationInvocationHandler 객체를 리플렉션으로 생성하는 부분을 살펴보겠습니다.

 Class.forName() 메소드로 AnnotationInvocationHandler 객체를 얻고, getDeclaredConstructors()[0] 메소드로 첫번째 생성자를 얻었습니다.

 이후에 newInstance를 이용하여 생성자에 lazyMap을 인자로 전달하면서 취약점을 발생시키는 객체를 생성합니다.

 lazymap을 인자로 전달하는것으로 보아 최종적으로 RCE가 발생하는 부분은 secondInvocatinHandler에서 발생할 것으로 추정됩니다.

 첫번째로 생성됨에도 불구하고 변수명이 second로 시작되는 이유는 해당 핸들러가 다른 객체의 인자로 들어감으로써

 두번째로 생성되는 invoacationHandlerToSerialize 핸들러 보다 RCE 흐름상 뒷부분에 위치하기 때문입니다.

 

 

 lazymap을 인자로 받는 AnnotationInvocationHandler 클래스의 생성자를 살펴보면 Map<String, Object> 타입의

 memberValues 파라미터를 두번째 인자로 받고 있습니다. 이것은 ysoserial에서 굳이 AnnotationInvocationHandler를 타겟으로 하여

 도구를 제작한 이유와 무관하지 않아보입니다. 아마도 LazyMap과 ChainedTransformer의 조합으로 취약점이 발생하는것을

 발견하고 Map을 인자로 받는 클래스를 찾지 않았을까요.

 

 

 

 

 4. invocationHandlerToSerialize :: 두번째 Handler

 

 ① Proxy를 이용한 객체 생성

 두번째로 생성되는 invocationHandlerToSerialize 객체 생성 코드를 분석하려면 프록시(Proxy) 디자인 패턴에 대한 이해가 필요합니다.

 일단 프록시 디자인 패턴에 대한 원리는 쉽게 설명해 놓은 글이 있어서 아래의 블로그 주소를 소개합니다.

 프록시 패턴 :: http://devbox.tistory.com/entry/DesignPattern-%ED%94%84%EB%A1%9D%EC%8B%9C-%ED%8C%A8%ED%84%B4

 

 내용을 요약하자면, 가벼운일은 대리인 PrinterProxy에서 처리하고, 무거운일(heavyJob)은 Printer를 호출해서 처리하는 로직입니다.

 이런 디자인패턴을 구현하게 되면 핵심기능의 앞 뒤로 원하는 추가 기능을 넣을 수 있을 뿐만 아니라,

 A/B/C 3개의 클래스가 있다고 가정했을 때, 프록시에서 1번만 구현하면 되기 때문에 코드 관리가 편리해집니다.

 

 하지만 우리가 분석중인 evilObject에서는 정적 프록시 패턴이 아닌, 리플렉션의 다이나믹 프록시(Dynamic Proxy)를 사용합니다.

 사실 기존의 정적 프록시는 A/B/C 클래스가 단일 프록시로 구현되어 있을 때 새로운 D/E/F 클래스가 추가되야 한다고 가정하면 

 매번 추가되는 신규 클래스의 인터페이스를 프록시에 구현해야 하는 번거로움이 있습니다.

 또한 부가기능/접근제어와 같은 코드는 자주 활용되는 것들이 많기 때문에 유사한 중복 코드가 빈번하게 발생하기도 합니다.

 

 다이나믹 프록시는 이러한 정적 프록시의 단점을 극복하고자 리플렉션의 특징을 도입하여 설계되었습니다.

 이때 사용하는 리플렉션의 특징은 바로 위에서 학습했던 InvocationHandler의 invoke() 메소드입니다.

 프록시 객체 생성시 인자로 전달되는 InvocationHandler에 부가기능을 한번만 구현하여 중복코드 문제를 해결합니다.

 또한 A/B/C 클래스의 앞뒤로 원하는 출력이나 기능을 덧붙이는 기능도 그대로 유지할 수 있습니다.

 프록시는 클라이언트에게 요청받은 내용을 핸들러에게 넘기고, 핸들러에서는 invoke()에서 처리한 후 결과값을 반환해줍니다.

 아래에서 디버깅을 진행할 때에도 프록시를 이용한 호출이 있다면, 프록시가 가지고 있는 핸들러의 invoke() 함수를 눈여겨 봐야합니다.

 아래 그림은 InvocationHandler를 이용하여 다이나믹 프록시를 설명하는 다이어그램입니다.

 

 그림출처 : https://jsdom.wordpress.com/2011/09/05/java-reflection-in-action-study-note-5/

 

 

 예제로 우리가 분석중인 다이나믹 프록시에 인자값으로 전달되는 AnnotationInvocationHandler 클래스를 살펴봅시다.

 클래스의 기본 설명은 Annotation의 Dynamic Proxy 구현을 위한 InvocationHandler 라고 되어 있습니다.

 프록시 패턴에 대해 잘 아시는 분이라면 다이나믹 프록시에 등록하여 사용하는 핸들러임을 바로 파악하지 않았을까 싶네요.

 InvocationHandler, Serializable 인터페이스를 구현했기 때문에 invoke/readObject 메소드가 있습니다.

 

 

 프록시를 통해 toString을 실행할 경우 toStringImp() 메소드를 이용하여 annotation의 형태의 문자열을 출력합니다.

 이런식으로 invoke()를 통해 처리하지 않고, 인터페이스를 구현한 메소드는 가벼운일에 속한다고 봐야할것 같습니다.

 ex) @("Original toString()")

 

 

 사실 이부분까지 알필요는 없지만, 취약점의 핵심이 되는 AnnotationInvocationHandler 클래스는 도대체 어디에 쓰이는걸까요.

 검색을 해봐도 별다른 결과가 나오지 않아서 주로 프레임워크에서 사용되지 않을까 싶어 JDK 라이브러리에 검색을 했습니다.

 여러 결과가 나왔으나, AnnotationParser 클래스가 눈에 띕니다.

 

 

 실제로 코드를 살펴보면 annotationForMap을 호출하면서 타입과 Map을 넘겨주면 AnnotationInvocatioanHandler를 리턴해줍니다.

 이렇게 사용하면 사용자는 annotationForMap을 사용하면서 자동으로 프록시의 기능을 이용하게 될것으로 보입니다.

 

 

 실제로 활용 용도나 인자값을 고려하지 않고 실행되는것에 초점을 맞춘 예제를 작성하여 돌려보면 

 마치 프록시처럼 문자열 "@" "(" 등을 덧붙이는 부가기능이나 타입을 반환하는 메소드는 annotationForMap 본인이 처리하고 있습니다.

 

 

 

 그외의 무거운일(HeavyJob)인 equals 메소드는 invoke()를 통해 해결합니다.

 

 

 

 ② 본문코드 분석

 본론으로 돌아와서, Proxy를 어떻게 사용하고 있는지 살펴봅니다.

 newProxyInstance 메소드로 아래의 인자를 넣어주면서 다이나믹 프록시의 객체(evilProxy)를 생성하고 있습니다.

 인자는 프록시 클래스를 정의하기 위한 클래스로더, 클래스가 구현해야할 인터페이스, 실제 작업을 처리하는 핸들러입니다.

 또한 이렇게 얻은 객체(evilProxy)를 InvocationHandler 객체를 생성하면서 인자로 넣어주는것을 볼 수 있습니다.

 

 ① 첫번째 인자 :: 클래스로더(ClassLoader)

 동적으로 클래스의 정보(Class.forName), 생성자(getConstructor)를 얻거나 정의(defineClass)하는데 사용

 ② 두번째 인자 :: 프록시가 구현해야할 인터페이스 리스트

 해당 리스트를 이용하여 generateProxyClass 메소드에서 인터페이스를 구현한 .class 파일을 FOS스트림으로 생성

  

 ③ 세번째 인자 :: 프록시가 호출하는 Handler

 바로 위에서 살펴본 AnnotationInvocationHandler이므로, 어떤 기능을 수행하는지는 생략

 

 프록시의 객체(evilProxy)를 생성하면서 재밌는 점은 구현해야할 인터페이스인 두번째 인자 Map.class 입니다.

 왜하필 Map.class를 상속받았는지 의문점이 있었으나, 바로 아래에 두번째 Invocationhandler 객체를 생성할때 의문이 풀리게됩니다.

 AnnotationInvocationHandler 클래스의 생성자는 Map<String, Object> 형태의 두번째 인자를 받습니다.

 따라서 Map.class를 구현한 evilProxy 객체는 이 타이밍에서 Map.class로 업캐스팅되겠죠.

 Map 인터페이스를 구현하지 않으면 RCE를 발생시키지 못하니 구현한것인지,

 혹은 원래 이렇게 사용하는 핸들러인지는 잘 모르겠으나, 어쨋든 Map 인터페이스를 구현한 것이 이유가 있는것은 확실합니다.

 

 

 핸들러의 invoke 함수 내부를 살펴보면 RCE가 발생하는 주요 포인트인 memberValues.get(member) 코드가 존재합니다.

 여기서 memberVlues는 방금 바로 위에서 본 proxy 객체입니다. 따라서 proxy.get(member) 라고 바꿔도 무방합니다.

 또한 proxy는 Map 인터페이스를 구현하면서 Map.class의 get() 메소드가 이미 존재합니다.

 이후는 우리가 Payload only Execute에서 살펴본대로 가젯 체인이 연쇄 호출되며 사용자 명령어(calc 등..)가 실행될것입니다.

 

 

 

 5. invocationToSerialize의 구조

 디버깅을 하면서 가장 헷갈렸던 부분은 핸들러 안에 핸들러가 들어가고, 중간에 프록시까지 들어가는 복잡한 구조입니다.

 정리하지 않고 무작정 디버깅으로 넘어가면 머릿속에서 핸들러의 미로(?)에 빠질수도 있습니다.

 일단 객체 생성코드 부분을 다시 체크하고 구조를 살펴보겠습니다.

 ① secondInvocationHandler는 Lazymap을 인자로 넣어주면서 생성↓↓

  InvocationHandler secondInvocationHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);

 ② invocationHandlerToSerialize는 Map 인터페이스를 구현한 Proxy를 인자로 넣어주면서 생성 ↓↓

  InvocationHandler invocationHandlerToSerialize = (InvocationHandler) constructor.newInstance(Override.class, evilProxy);

 

 이렇게 생성된 invocationToSerialize 객체의 구조를 이클립스로 살펴보면 아래와 같습니다.

 invocationHandlerToSerialize 인스턴스를 생성하면서 인자로 넣어준 ProxymemberValues로 선언되어 있고,

 Proxy 인스턴스를 생성하면서 넣어준 두번째 AIHandlerh(handler)에 선언되어 자리를 잡았습니다.

 두번째 AnnotationInvocationHandler는 인스턴스를 생성시 LazyMap을 인자로 전달했기 때문에 핸들러 변수(h)에 있습니다.

 

 

 이렇게 이해하기 어려운 구조로 객체를 생성해야 하는 이유는 사용자 명령을 서버단에서 실행하기 위해서입니다.

 간단하게 아래처럼 Serializable 인터페이스를 구현한 객체를 만들고 readObject() 내부에 사용자 명령어를 넣어주고 전달하면 끝아닌가?

 라는 생각을 할수도 있지만, 이러한 로직이 성립하려면 서버(or 응용프로그램)에서 동일한 MyObject 객체를 가지고 있어야합니다.

 (실제로 테스트를 해보면 MyObject를 찾을 수 없다면서 java.lang.ClassNotFoundException 오류가 발생할겁니다.)

 객체를 공유할 수 있는 기능이 제공되지 않는 이상 발생하기 희박한 상황이겠지요.

 

 

 하지만 ysoserial 페이로드는 Java Class Path Commons Collection 라이브러리가 포함되어 있고, 역직렬화를 수행하면

 같은 객체를 공유하지 않더라도 서버에서 실행될 수 있습니다.

 

 

 

 

  0x03  Gadget Chain Debugging

 

 이제 마지막으로 가젯체인에 있는 순서대로 RCE가 발생하는 포인트까지 디버깅을 진행하겠습니다.

 

 1. Gadget Chain :: ObjectInputStream.readObject()

 ObjectInputStream의 readObject 메소드가 호출되면 직렬화된 데이터를 읽어들이기 위해 역직렬화 과정을 거치게 됩니다.

 역직렬화가 진행되면 ObjectInputStream이 읽어들인 직렬화된 클래스 내부의 readObject를 호출하게 됩니다.

 이때 readObject 메소드 호출 역시 Reflection 기능을 이용하여 readObject를 invoke 메소드로 호출하는 로직입니다.

 만약 Serializable 인터페이스를 구현한 객체가 readObject 메소드를 가지고 있다면 해당 readObject가 호출됩니다.

 AnnotationInvocationHandler의 경우 Serialize와 Map 인터페이스를 구현했기 떄문에 readObject()를 가지고 있습니다.

 

 

 

 2. Gadget Chain :: readObject() + Map.entrySet()

 참고로 Deserialize가 진행되면서 readObject() 메소드는 second 핸들러 → toSerialize 핸들러 순으로 총 2번 호출됩니다.

 (정확한 이유는 모르겠으나 serialize 인터페이스를 구현한 클래스가 중첩되었을 경우 각각 직렬화/역직렬화 과정을 진행하나봅니다)

 second 핸들러에서는 RCE가 발생하지 않으며, 두번째로 호출되는 toSerialize 핸들러의 AnnotationInvocationHandler에서 RCE가 발생합니다.

 

 proxy를 가지고 있는 toSerialize의 역직렬화를 살펴보겠습니다. 

 AnnotationInvocationHandler는 readObject() 메소드를 실행할때 생성자를 통해 받은 Map의 entrySet() 메소드를 호출합니다.

 그리고 entrySet() 메소드를 실행하는 순간 evilProxy가 가지고 있는 InvocationHandlerinvoke() 메소드로 실행흐름이 넘어갑니다.

 쌩뚱맞게 갑자기 invoke() 메소드로 흐름이 넘어가는 부분에 대해서 나름대로 분석을 해보면..

 ① memberValues = Proxy이므로, 실행되는 메소드는 evilProxy.entrySet으로 볼 수 있음

 ② 요청(entrySet)은 프록시가 가진 핸들러(AnnotationInvocationHandler)가 처리 (프록시 다지안 패턴)

 ③ 이때 Proxy는 Map 인터페이스를 구현(Imp)하였으므로, entrySet 메소드를 호출하여도 에러가 발생하지 않음

 ④ InvocationHandler는 통칭 무거운일(Heavy Job)을 invoke() 메소드로 처리하므로 invoke() 메소드로 흐름이 넘어감

 

 

 

 3. Gadget Chain :: invoke() → Payload only Execute

 proxy를 생성할때 넣어준 AnnotationInvocationHandler의 invoke() 함수로 진입했습니다.

 디버깅을 진행하다보면 중간 부분에 memberValues.get(member) 메소드를 호출하는 부분이 나옵니다.

 

 

 이때 memberValues는 main 에서 첫번째 AnnotationInvocationHandler 객체를 생성할때 넣어준 LazyMap입니다.

 생성할때와 동일하게 factory에 ChinedTransformer를 가지고 있고, map은 HashMap입니다.

 해당 코드를 실행하게 되면 Payload only execute 챕터에서 보았던 RCE가 정상적으로 실행됩니다.

 

 

 단순히 툴을 사용하면 하나의 직렬화된 객체가 짠 하고 나오지만, 그 중간과정은 매우 복잡하네요.

 디버깅을 끝으로 Ysoserial - CommonsCollection1 Exploit 페이로드 분석을 마칩니다.

 

 

 

 

  0x04  Debugging/Error Tip

 

 1. UnsuppoertedOperationException : Serialization support for org.apache.commons..

 InvokerTransformer의 보안 때문에 오류가 난다면 Common-Collections 3.2.1 Ver 이하로 라이브러리로 바꿔주어야 합니다.

 (저의 경우에는 3.2.1 Ver을 설치했다고 착각했지만 확인해보니 3.2.2 Ver을 사용하여 오류 발생....)

 

 

 

 2. IncompleteAnnotationException : java.lang.Override msiing element entrySet

 프로젝트 설정에서 JDK 1.8 → JDK 1.7 Ver 으로 변경하여 실행 (여전히 오류는 발생하지만 RCE는 발생함)

 (미리 1.7 버전을 다운로드 받아서 이클립스 build path에 별도로 추가해주어야 탭으로 선택할 수 있습니다)

 

 

 

 

 

 

  0x05  마치며..

 

 포스팅을 진행하면서 디버깅 할일이 많아지다 보니, 마치 개발(?)을 하는듯한 느낌이 들더군요.

 물론 개념만 알면 그냥 넘어갈 수 있는 부분도 많았고, 응용이 필요하진 않았지만

 프레임워크에서 사용하는 각종 디자인 패턴과 자바의 개념들이 머리속에서 뒤엉키는 경우가 많았습니다.

 내용이 길어지고 내용들이 얽히고 섥히다 보니 오타나 흐름이 이상한 부분이 있을 수 있습니다.

 틀린 내용이나 바로 잡아야할 내용이 있을 시 댓글 남겨주시면 감사하겠습니다.

 긴 글 읽어주셔서 감사합니다.

 

 

posted By Message

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

redScreen.tistory.com Blog :(

 

 

':: 취약점' 카테고리의 다른 글

[CVE-2014-6332] OLE 자동화 배열 취약점 분석  (1) 2016.07.24
FCKeditor 취약점 + 간단실습  (0) 2016.04.20
posted by Red_Message
2017. 1. 21. 16:17 :: 웹 보안

안녕하세요. Message 입니다.

오늘은 SQL Injection, Union Select 테스트를 위해

ORACLE 기반의 테스트 환경을 구축하고 DB 쿼리문을 정리하는 글을 포스팅하려 합니다.

쿼리문은 인젝션 포인트에서 Union Select 구문을 날리는 순서대로 진행하고자 합니다.

 

 

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

0x00 준비

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

 

 

ORACLE 공식 홈페이지에서 ORACLE Database Express Edition 11g Release2 (XE) 를 다운받습니다.

무료(OTN License)이며, 기본으로 내장된 테이블만으로도 각종 실습이 가능합니다.

URL : https://www.oracle.com/index.html

 

이후에 Next 쭉쭉~ 진행하시면 되며

중간에 입력하는 패스워드는 데이터베이스 접속 시 사용됩니다.

 

 

 

설치가 완료되었다면 구동시켜봅니다.

시작버튼을 누르고 Oracle 설치 폴더에서 Start Database를 실행시켜서 아래와 같은 창이 뜨는지 확인합니다.

 

이후에 동일 폴더에서 Get Started 버튼을 누르면 아래와 같은 ORACLE HOME 메인 페이지로 접속됩니다.

이때 루프백주소(127.0.0.01) 8080 포트로 접속되므로,

각종 프록시 도구나 기타 서버 프로그램과의 포트 충돌에 주의해야 합니다. 

 

 

 

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

0x01 실행

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

 

메인 페이지에서 Application Express 으로 이동합니다.

로그인창에서는 SYSTEM(유지보수용, DB생성 권한 없음) 계정으로 접속하며, 위에서 설정한 ID/PW를 입력합니다.

참고로 계정은 SYS(오라클 Super), SYSTEM(유지보수용), SCOTT(sample ID, 실습용), HR(sample ID, 실습용) 등이 있습니다.

 

Application Express 탭에서 Create New 항목을 이용하여 계정을 생성합니다.

계정 생성이 완료되었다면, 우측 Getting Started의 Login Here 버튼을 클릭하여 로그인창으로 넘어갑니다.

 

방금 생성한 계정 또는 기존의 계정으로 로그인합니다.

 

SQL Workshop을 눌러 생성한 계정의 DB를 살펴봅니다.

 

이제 SQL Commands 항목으로 이동하면 쿼리문을 날릴 수 있는 페이지가 나옵니다.

 

 

 

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

0x02 테스트 & 실습

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

 

1. DB종류 판별

실제로 진단을 수행할 경우에는 어떤 DB인지 알 수 없으므로 아래 쿼리문을 이용하여 확인합니다.

일차적으로는 주석으로 MYSQL 또는 MSSQL/ORACLE 등의 여부를 판단합니다.

- "#" → MYSQL

- "--" → MSSQL, OACLE

 

결과값을 통해 주석을 "--" 문자로 사용하는 DB라는 결과가 나온다면

특수문자를 어떻게 받아들이느냐로 간편히 DB를 구분할 수 있습니다.

MSSQL의 경우에는 "||" 문자를 OR 연산자로 인식하지만, ORACLE은 'AB' || 'CD" 를 문자열 'ABCD'로 인식합니다.

즉, 인젝션 포인트에서 아래의 쿼리문이 참으로 판단되어 결과값이 잘 나온다면 ORACLE 입니다.

 

 

2. 전체 테이블 확인 + 로그인 유저의 테이블 확인

초반부에서 잠깐 언급했지만, 일단 ORACLE 설치가 완료되면 별도의 테이블 생성 없이

기본적으로 제공되는 테이블들이 있기 때문에 바로 실습이 가능합니다.

DB에 어떤 테이블들이 존재하는지 ALL_TABLES 테이블명으로 질의할 수 있습니다.

제가 설치한 버전에서는 89개의 테이블이 나왔습니다.

 

만약 특정 DB 계정을 얻는데 성공했다면, 로그인한 계정이 소유한 테이블이 무엇인지 확인할 필요가 있습니다.

이럴경우에는 아래와 같이 USER_TABLES 테이블명으로 질의할 수 있습니다. 18개 테이블이 나왔습니다.

 

그중에 제가 사용한 테이블은 EMPDEMO_USERS 테이블입니다.

아무래도 각종 DB 서적에서 자주 사용되는 테이블이다보니 친숙하기도 했고,

특히 EMP 테이블은 Union Select 쿼리를 날릴때 DATE 속성이 있던 케이스가 있어서 선택했습니다.

 

 

3. 컬럼 개수 확인

Union Select를 이용한 Injection 공격 수행을 할 경우, 질의하는 테이블의 컬럼 개수를 알아내야 합니다.

왜냐하면 Union Select 자체가 앞선 Select 구문과 합쳐져서 나오기 때문이지요.

이럴때 Order By + 숫자(컬럼) 질의를 이용하면 컬럼의 개수를 알아낼 수 있습니다.

아래와같이 숫자를 1로 할 경우, EMPNO 컬럼의 오름차순으로 정렬되지만,

숫자를 9로 지정할 경우, 해당되는 속성이 없어서 결과값이 반환되지 않습니다. 즉 컬럼 개수를 알 수 있죠.

 

 

4. 컬럼 타입 확인

Union Select의 경우 컬럼의 타입도 맞춰 주어야 검색이 가능합니다.

일반적으로는 아래 질의문과 같이 컬럼 개수만큼 숫자 + 문자를 넣어주며 공격 쿼리문을 날립니다.

하지만 앞선 SELECT 문에서 검색되는 컬럼들의 타입과 일치하지 않으면 에러가 나지요.

SELECT * FROM EMP WHERE EMPNO > 7500 UNION SELECT 1, '2', 3, '4', 5, '6', 7, 8 FROM EMP;

 

하지만 컬럼의 타입이 위에서 언급한 DATE일 경우도 있으므로, 숫자 + 문자 + DATE 등의 경우의 수가 너무 복잡해집니다.

이럴때는 컬럼의 개수만큼 NULL을 넣어주고, 맨 앞에서부터 하나씩 숫자와 문자 등을 넣어주며 타입을 맞춰주면 됩니다.

 

이때 DATE 속성의 경우에는 아래와 같이 타입을 캐스팅 해주지 않으면 에러가 발생합니다.

 

 

5. 컬렴명 확인

이젠 UNION SELECT 테이블명에 원하는 테이블을 넣고, 동일 타입에 해당 테이블 컬럼을 넣어주어 DB 데이터를 뽑아내면 됩니다.

테이블명은 ALL_TABLES를 이용하여 알아내면 되지만, 우리는 아직 해당 테이블의 컬럼명을 모릅니다.

테이블의 컬럼명은 아래와 같이 ALL_TAB_COLUMNS 테이블명을 이용하여 얻어냅니다.

이때도, USER_TAB_COLUMNS 테이블명을 이용하면, 현재 계정의 권한으로 어떤 속성에 접근이 가능한지 선별할 수 있습니다.

 

 

6, BONUS. 출력 팁

깨알같은 팁이지만, UNION SELECT를 이용해 게시판 등에서 원하는 쿼리를 뽑을 경우

어떤 데이터가 무엇을 의미하는지 모르는 상황이 발생할 수 있습니다.

이럴때는 CONCAT 함수와 || 문자를 이용하여 깔끔한 출력값을 얻어낼 수 있습니다.

 

 

 

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

0x03 마무리

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

 

이것저것 쿼리를 날리다보면, 오타여서 오류가 발생하는지, 쿼리가 잘못된건지 기타 이유를 파악하는데 시간이 너무 오래 걸렸습니다.

그럴바엔 직접 DB에 쿼리를 날리면서 실습을 해보고자 포스팅을 하게 되었습니다.

덤으로 어느정도 DB에 대한 막연함이 사라진것 같기도 하네요!

읽어주셔서 감사합니다.

 

 

posted By Message.

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

 

posted by Red_Message
2017. 1. 8. 00:13 :: 시스템 보안

안녕하세요. Message 입니다.

오늘은 존더리퍼(John the Ripper) 도구를 이용한 패스워드 크래킹 실습을 포스팅합니다.

실습환경은 윈도우/리눅스 모두 가능하며, 저의 경우는 칼리리눅스로 진행했습니다.

 

 

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

0x00 준비

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

 

비밀번호 크래킹과 관련된 도구는 크게 온라인/오프라인 나뉩니다.

온라인 도구로는 Hydra, Medusa, 데이터베이스 기반의 Findmyhash 등이 있습니다.

오프라인 도구는 오늘 살펴볼 John the Ripper (JTR) 가 대표적입니다.

존더리퍼는 유닉스 패스워드 크랙 도구로서, 초당 백만개 이상의 패스워드를 비교할 수 있다고 합니다.

성능이 더 좋은 도구들도 있지만, 크랙에 대한 배경지식을 쌓기에 유용한 도구라고 생각합니다.

 

윈도우 기반으로 사용하실분은 공식 홈페이지에서 Windows 버전으로 다운받으시면 됩니다.

URL : http://www.openwall.com/john/

 

존더리퍼는 칼리리눅스 05 - Password Attack 항목에 있으며, Johnny는 GUI기반입니다.

John의 경우 각종 옵션을 알아야 사용할 수 있으므로, 

Johnny를 통해 존더리퍼와 친숙해진 다음 John으로 넘어가겠습니다.

 

일단 존더리퍼로 크랙할 테스트 계정을 생성합니다.

저의 경우 아래와 같이 4개의 신규 계정을 추가하고, 비밀번호를 할당해주었습니다.

 test1 : test

 test2 : test!@#

 test3 : gksrmf -> 한글을 영어로 타자침

 test4 : gksrmf!@#

 

할당이 완료되었다면, shadow 파일에 패스워드가 해쉬되어 저장되었는지 확인합니다.

 

존더리퍼를 사용하기 이전에, 위에서 나온 shadow 파일과 구조에 대해서 간략히 알 필요가 있습니다.

일단 shadow 파일은 /etc/passwd 파일에 있는 패스워드 부분을 /etc/shadow에 두고 root만이 읽을 수 있는

400 권한으로 설정해두어 보안을 강화하기 위한 목적입니다.

shadow파일은 9개 항목으로 구성되며, 우리가 존더리퍼를 사용하면서 알아야할 부분은 암호가 해쉬되어 저장된 encryped 항목입니다.

구조 : [ $Hashid $Salt $Hash Value ]

 

Hashid는 첫번째 $ 뒤에 있는 문자로서, 아래 그림과 같이 Identifier에 따라 Hash Function과 Salt length 등이 변경됩니다.

 

Salt는 레인보우테이블을 이용한 단순 복호화를 방지하기 위한 값으로서, 두번째 $ 뒤에 있는 값입니다.

위에 있는 shadow 파일은 Identifier가 $6 이므로, SHA-512로 해쉬되어 2byte의 Salt 값을 가집니다.

Hash Value는 Hashid에 따른 해쉬방법과 Salt 값을 이용하여 Hash Function을 수행한 결과입니다.

 

 

 

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

0x01 기능파악

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

 

이제 어느정도 배경지식이 쌓였다면 Johnny를 통해 shadow 파일을 로드하여 살펴봅니다.

Shadow 파일의 내용이 User, Password, Hash 값 등으로 분리되어 있군요.

디폴트 모드로 바로 시작하려면 Start Attack 버튼을 누르면 되며, 결과값이 실시간으로 Password 항목에 채워집니다.

 

말이 나온김에 일단 Start Attack 버튼을 클릭해봅니다.

클릭한지 1초만에 root, test1 계정의 비밀번호가 바로 크랙되어 UI에 표기되었습니다.

gksrmf, test!@# 패스워드도 조금만 기다리면 될거라 예상했지만, 2시간이 넘어도 크랙되지 않았습니다.

하단에는 패스워드 크랙이 진행중인 계정의 개수와 함께 진행도가 %로 표시됩니다.

 

0~1초 크랙의 의미는, "크래킹 속도 매우 빠름" or "기본 사전파일에 해당 문구 있음" 정도로 유추할 수 있습니다.

존더리퍼 설정파일인 john.conf 파일의 내부를 살펴보면, Default wordlist를 사용하고 있음을 알 수 있으며

기본 wordlist에는 대부분이 문자열, 숫자열이고 특수문자는 거의 존재하지 않습니다.

 

Output 탭으로 이동하면 콘솔모드의 john에 표기되는 문구들을 볼 수 있습니다.

Identifier가 $6 이었기 때문에 sha512crypt 알고리즘으로 인지하였고, 4 OpenMP threads 모드로 크랙이 진행되는군요.

 

실제로 존더리퍼가 어떤 작업을 수행하고 있는지는 로그 파일을 살펴보면 됩니다.

John의 로그파일이 생성되는 경로는 환경마다 다를 수 있지만, 저의 경우 /root/.john/ 폴더에 생성되었습니다.

Johnny 로그파일은 동일 경로 jhonny 폴더 내부에 생성됩니다. 3개가 생성되어 있네요.

 

default.log 파일을 살펴보면 session의 시작부터 각종 rule이 적용되는 부분이 보입니다.

root와 test1 계정의 패스워드는 0초만에 크랙되었습니다.

 

이후 타임라인이 1시간이 지나도록 별다른 성과는 없었습니다.

 

 

이제 실행도 시켜 보았으니 존더리퍼를 좀더 유용하게 사용하기 위한 옵션을 알아봅니다.

Options 탭으로 이동하면 존더리퍼의 기능 5가지가 나옵니다.

Default behavior

   기본적인 모드로서, "Single crack"  → "wordlist" → "incremental" 순으로 크랙이 진행됩니다.

   순차적으로 실행되는 모드들은 기본으로 실행됩니다.

"Single crack" mode

   존더리퍼에서 가장 빠른 크래킹 기능입니다.

   계정명 + other information + word mangling(혼합)을 이용한 "Single" rule을 사용합니다.

   여기서 other information은 존더리퍼의 나름 자체적인 알고리즘이 아닐까 싶습니다.

"Wordlist" mode

   가장 많이 사용될만한 기능입니다. 말그대로 사용자가 사전파일을 대입하여 크랙을 진행할 수 있습니다.

   use rules 옵션을 지정하지 않으면, 사전파일에 있는 문구만 대입합니다.

   use rules 옵션을 지정하면, 대입한 사전파일 단어에 각종 알고리즘을 적용합니다. (사전파일이 클수록 많은 시간이 소요됨)

  

  

 

   하지만 Johnny 에서 use rules를 체크하거나, John에서 -rules 옵션을 지정해주면

   아래와 같이 mangling 옵션등을 활성화하여 룰을 적용합니다.

  

 

"Incremental" mode

   가장 강력한 기능을 가진 크래킹 옵션입니다. 모든 가능한 조합을 시도합니다.

   하지만 그만큼 시간이 필요하므로, 침투테스트 시간이 길지 않다면 사용하기 힘든 옵션입니다.

"External" mode

   사용자의 코드를 이용하여 크래킹을 flexible 하게 시도할 수 있습니다.

 

 

 

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

0x02 크래킹의 시작 + 마무리

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

 

이제 어느정도 기능 파악이 되었으니, 주어진 환경에서 크랙을 시도해볼일만 남았습니다.

침투테스트 기간이 항상 길다는 보장이 없으므로, 아마 사전파일을 이용한 크랙이 주를 이룰거라 생각됩니다.

사전파일을 만드는 방법은 이미 작성된 파일을 구하거나 직접 유추하는 방법이 있으며,

crunch,  cupp, 워드하운드, BruteScrape 등의 도구를 사용하는 방법도 있습니다.

 

여기서는 도구를 사용하는 방법보다는 기존에 작성된 파일을 이용한 크랙을 시도합니다.

제가 사용한 사전파일 다운로드는 아래 URL에서 가능합니다. 10억개의 사전파일이 있다는군요.

URL : https://dazzlepod.com/uniqpass/

 

또한 HackerPlaybook 책에서도 소개된 brutescrape 도구를 이용하여

침투테스트를 수행하고 있는 웹사이트의 정보를 모아 사전파일을 만들어 추가시키는것도 큰 도움이 될 수 있습니다.

GitHub : https://github.com/cheetz/brutescrape

 

아래 명령어는 콘솔모드인 John에 사전파일(passwords.txt)을 대입하여 크랙을 시도하는 명령어입니다.

진행도는 다른 콘솔을 열어서 -show 옵션을 주면 체크할 수 있습니다.

크랙 명령어 :  john -w:passwords.txt -rules shadow

진행도 확인 :  john -show shadow

 

이제 기다리는 일만 남았습니다.

개인적으로 패스워드 크래킹을 공부하면서, 시간 + 사전파일(+노하우)이 중요하다는 느낌이 듭니다.

시간이 제한적이라면 노하우를 통해 조금이라도 시간을 줄여야겠죠..

포스팅을 마칩니다. 읽어주셔서 감사합니다.

 

posted By Message.

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

 

posted by Red_Message
2017. 1. 1. 18:43 :: 문제풀이/WebHacking,kr

안녕하세요. Message 입니다.

Webhacking.kr Lv10 문제풀이입니다.

 

 

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

0x00 문제분석

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

 

10번 문제로 접속합니다.

"buy lotto" 라는 문구가 보이며, 별다른 특징은 없어보입니다.

 

소스코드를 살펴봅니다.

"hackme" 라는 id값을 가진 a태그 외에는 주목할만한 점이 보이지 않습니다.

 

<a> 태그에 몇몇가지 이벤트가 존재합니다.

onmouseover, onmouseout 이벤트로 인해 O라고 되어 있는 문자에 마우스를 가져가면 모양이 변하며,

onclick 이벤트 안에 하드코딩 되어 있는 this.style.posLeft+=1; 스크립트로 인해 글자를 클릭시 오른쪽으로 이동합니다.

 

 

 

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

0x01 문제풀이

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

 

<a> 태그에 있는 onclick 이벤트를 좀더 자세히 살펴보겠습니다.

아무래도 아래의 스크립트가 이번 문제를 해결하는 열쇠인듯 합니다.

if(this.style.posLeft=800) this.href='?go='+this.style.posLeft

 

posLeft 속성이 800이 되면 특정 go 변수에 800을 할당하고, 해당 URL로 이동합니다.

그럼 go 속성에 800을 할당해서 새로고침을 누르면 되는구나 싶어 시도해보았더니

아래쪽에 "nohack" 이라는 문구가 생깁니다. 단순히 숫자만 바꿔서는 해결이 안되는 케이스인듯 합니다.

 

그래서 일단 go 변수에 799 까지 할당한 다음 직접 클릭하여 어떤 패킷이 들어가는지 확인해보기로 했습니다.

이때 hackme.style.posLeft에 799를 할당하는 방법은 여러가지가 있는데,

제가 주로 사용하는 방법은 IE의 개발자 - 콘솔모드 입니다만,

DOM탐색기 - 스타일에서 속성을 변경하면 정확한 타겟에 오타없이 지정할 수도 있습니다.

이후에 문구를 한번 클릭해주면 클리어입니다.

 

자..그렇다면 직접 800을 URL로 입력했을 경우와 799까지 입력해주고 클릭했을 경우

어떤 차이가 있길래 클리어가 갈리는지 알고싶어집니다.

일단 패킷을 떠서 비교해보니 직접 URL을 만들어서 접속한 경우 Referer 속성이 누락되는군요.

 ← 직접클릭

 ← URL작성

 

확실히 그 차이인지 확인해보기 위해서, 직접 URL을 작성한 상황에서 Referer 속성을 추가하여 보내봅니다.

 

이에 대한 응답은 Congratulation 입니다.

특정 페이지에서만 접근할 수 있도록 접근제어 옵션이 설정되어 있다고 보면 될 것 같습니다.

 

 

posted By Message.

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

 

posted by Red_Message
2016. 12. 31. 14:31 :: 리버싱

 

안녕하세요. Message 입니다.

오늘은 오랜만에 리버싱과 관련된 2가지 주제로 포스팅을 하려 합니다.

① UPack PE 헤더 상세 분석

② 디버깅 - OEP 찾기

 

<리버싱 핵심 원리 - 이승원님> 책에 있는 내용을 베이스로 하지만,

공부하는 입장에서 진행과정을 조금 더 보강하거나

당연한 내용이기에 자연스럽게 책에서 생략된(저는 모르는..) 내용들도 추가하려 합니다.

얼마전 실행압축으로 인해 분석을 보류했던 악성코드가 있었습니다. 각종 도구들을 이용해도 탐지가 안되더군요.

이번 공부가 패킹된 악성코드와 PE구조를 이해하는데 좋은 밑거름이 되었으면 좋겠습니다.

 


 

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

0x00 준비

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

 

홈페이지가 변경되어, 책에 있는 링크가 아닌 아래의 링크에서 UPack 0.39 final 버전을 다운받습니다.

Windows 버전도 있는듯 하지만, CMD 버전으로 다운로드 받았습니다.

URL : http://www.geocities.jp/dwingj/mycomp.htm

 

명령어도 매우 간단합니다. UPack으로 notepad.exe 파일을 패킹합니다.

사실 저는 처음에 OK! 문구 하나만 확인하고 그냥 창을 꺼버렸습니다만,

UPack이 어떻게 패킹을 수행하는지 대략적인 흐름을 알 수 있는 문구들이 있습니다.

지금은 그냥 읽어보고 넘어가세요. 분석 중간중간에 해당 사항들을 언급하겠습니다.

 - Rebuilding import table [00C8]

 - Recompiling resuorce [008304]

 - Remocing debug data [001C]

 - Transforming code

 - Compressing data <Use 128KB dict>

 - Building new PE data

 

파일의 원본과 UPack 으로 패킹된 프로그램의 헤더를 살펴봅니다.

섹션헤더는 모두 사라졌고, NT_Header 에서 중요한 OPTIONAL_Header 역시 찾아볼 수 없습니다.

또한 IMAGE_DOS_Header에서 KERNEL32.DLL을 비롯한 API 이름까지 보이고 있습니다.

확실히 정상적인 PE구조와는 확연히 다름을 알 수 있습니다.

 

 

 

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

0x01 헤더분석

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

 

1. 헤더 겹쳐쓰기(=e_lfanew 조작)

 

DOS_Header에서는 2가지 멤버가 중요합니다.

그 외는 프로그램 실행에 아무 의미가 없습니다. 위치상으로는 첫번째와 마지막 멤버입니다.

① e_magic : DOS Signature (4D5A, "MZ")

② e_lfanew : NT_header의 시작주소(파일에 따라 가변적)

 

정상적인 프로그램의 e_lfanew 값이 계산되는 방식은 통상적으로 아래와 같습니다.

e_lfanew = DOS_Header(40) + DOS_Stub(A0, 가변) = E0(224)

하지만 Upack에서는 e_lfanew 값이 10(리틀엔디안) 입니다.

 

PE 스펙에 어긋나진 안지만 스펙 자체의 허술함을 이용한 것이며,

NT_Header를 의미하는 COFF_File_Header를 클릭하면 두번째 라인부터 시작되며,

헤더의 크기 측면으로 보면 E0 → 10 으로 변경되면서 D0(208) 의 공간이 절약되었습니다.

 

 

 

2. IMAGE_FILE_HEADER.SizeOfOptionalHeader 조작

 

UPack은 NT_Hedaer 안에 있는 FILE_Header에서 SizeOfOptionalHeader 값을 조작합니다.

이 값을 변경하여 헤더 안에 디코딩 코드를 삽입하기 위한 목적입니다.

원래 SizeOfOptionalHeader 값의 의미는 말그대로 뒤따르는 OPTIONAL_Header의 구조체 크기입니다.

원래 정상적이라면 E0(224) 값을 가져야 하지만, Upack은 148로 변경합니다.

 

그렇다면 왜 UPack은 SizeOfOptionalHedaer 의 값을 변경할까요?

SECTION_HeaderOPTIONAL_Header 바로 뒤에 위치하는게 당연할거라고 생각되지만

OPTIONAL_Header의 시작주소(Offset)SizeOfOptionalHedaer 값을 더한 위치에서 시작합니다.

즉, 간단하게 헤더의 시작위치에서 헤더의 크기를 더하는 공식에서 헤더의 크기인 SizeOfOptionalHedaer 값을 조작한거죠.

아래 그림은 원본파일(notepad.exe)의 1st 섹션(.text)이 계산된 과정을 보여줍니다.

 

UPack은 이러한 특성을 이용하여 PE 헤더를 꽈배기처럼 꼬아놓고

남는 공간에 디코딩에 필요한 코드를 적절히 끼워 넣는 특성을 가집니다.

SECTION_Header1D8이 아닌 170에서 시작하고 있습니다.

 

결과적으로, 아래 그림처럼 OPTIONAL_Header의 뒷부분에 존재하는 DATA_DIRECTORY가 끝나는 108(264) 부터

SECTION_Header가 시작되는 28(OPTIONAL_Header Offset) + 148(SizeOfOptionalHedaer) = 170(368) 사이에

68(104)의 공간이 생겼습니다. byte 길이 계산하실때 실수를 방지하기 위해 HxD 툴을 사용하면 편리합니다.

 

 

 

3. IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes 조작

 

Upack은 OPTIONAL_Hedaer 안에 있는 NumberOfRvaAndSizes 값 역시 변경합니다.

이 값의 의미는 바로 뒤에 이어지는 DATA_DIRECTORY 구조체 배열의 원소 개수입니다.

해당 값을 변경하는 이유는 방금전 위에서 확보했던 68(104)만큼의 공간에 추가적인 공간을 확보하여

자신의 코드를 삽입하기 위한 목적입니다.

 

정상적인 파일에서는 DATA_DIRECTORY 구조체 배열의 원소 개수는 이미 10(16) 이지만,

UPack에서는 A(10) 으로 변경됩니다. 따라서 TLS_Table 원소 뒤에 존재하는 6개의 원소는 무시됩니다.

사실, DATA_DIRECTORY에서 중요한 원소는 EXPORT, IMPORT, RESOURCE, TLS Directory 이므로,

UPack이 TLS_Directory 원소 이후의 원소를 무시하는것은 자연스러운 결과일지도 모르겠습니다.

 

결국, 위에서 SizeOfOptionalHedaer의 값을 변경하여 얻은 68 크기의 공간에서

NumberOfRvaAndSizes 값 변경으로 DATA_DIRECTORY 6개 원소 크기 30을 더해 98 만큼의 공간이 생겼습니다.

 

UPack에서는 해당 공간에 아래와 같은 디코딩 코드를 삽입합니다.

처음엔 NT_HedaderSECTION_Header 사이에 코드를 삽입해서 어떻게 사용할건지 의문이 들었습니다.

해당 영역은 메모리에 올라가면 010000D8 주소에 있을테니까요.

 

아래 부분을 이해하려면 바로 아래 파트의 "섹션 겹쳐 쓰기"를 먼저 공부해야합니다.

하지만 SECTION_Header에 섹션의 시작 오프셋을 가리키는 PointerToRawData10임을 주목해야 합니다.

책에 있는 그림 18.17 [UPack의 겹쳐쓰기 특징] 처럼 PE 헤더 영역에 속한 D8 주소에 있는 데이터가

1st 섹션 영역에서 동일하게 나타남을 의미합니다. 아래 그림처럼 해당 섹션의 VirtualOffset(1000)이 추가된 영역에서 말이죠.

눈치 빠르신 분은 010010D8 주소에서 PointerToRawData10을 뺀 주소가 아니라 의아함을 느끼실 수 있습니다만,

RVA to RAW 파트에서 해당 부분에 대해 자세하게 살펴보겠습니다. 어쨋든 동일한 코드가 존재한다는게 중요합니다.

  헤더영역

  섹션영역

 

사실 저같은 경우, 분석을 하면서 NumberOfRvaAndSizes 값이 변경되었다는 사실을 단번에 파악하기엔

초보운전자가 도로상황을 전부 파악하기 힘든것과 일맥상통하다고 생각합니다.

하지만 시각적인 단서가 있다면 좀더 빨리 알아차릴 수 있겠죠.

아래 그림처럼 CFF에서 살펴보면, UPack과 원본파일의 DATA_DIRECTORY 구조체 개수와

Invalid 항목등을 통하여 NumberOfRvaAndSizes 값 변경을 통한 패킹을 의심해볼만 합니다.

 

 

 

 

4. 섹션 겹쳐쓰기

 

UPack은 SECTION_Header에서도 프로그램 실행시 사용되지 않는 항목에 자신의 데이터를 기록합니다.

Stud_PE를 이용해서 UPack의 SECTION_Header를 살펴보면 수상한 점이 2개입니다.

 

첫번째 수상한 점은 1st 섹션 & 3st 섹션이 겹쳐있다는 점입니다.

Stud_PE를 보면 RawOffset, RawSize 두가지 요소가 각각 10(16), 1F0(496)으로 동일합니다.

(참고로 오프셋은 10(16)은 아까 보았던 NT_Header가 시작되는 영역입니다.)

UPack은 동일한 파일 이미지로 각각 다른 위치/크기의 메모리 이미지를 만들 수 있는 헛점을 이용했습니다.

 

두번째 수상한 점은 1st 섹션 & 3st 섹션과는 달리, 매우 큰 2st 섹션의 크기입니다.

2st 섹션의 크기는 무려 AE28(44,584) 이며, 파일의 대부분을 차지하고 있습니다.

또한 1st 섹션의 VirtualSize 역시 파일의 RawSize에 비해 매우 큰 14,000(81,920) 입니다.

일반적으로 SECTION_HeaderVirtual SizeSize of Raw Data보다 월등히 크다면 패킹을 의심합니다.

 

Upack이 이러한 특성을 보이는 이유는 2st 섹션에 원본파일(notepad.exe)이 압축되어 있기 때문이며,

두번째 섹션이 메모리에 로딩될때 압축이 풀리며 1st 섹션에 기록하기 때문입니다.

VirtualSize 멤버 값은 메모리에서 섹션이 차지하는 크기를 의미하는데,

원본파일(notepad.exe)이 메모리에 로딩될때의 사이즈인 OPTIONAL_Header - SizeOfImage 값과 동일합니다.

따라서 AE28(44,584) 크기로 압축된 2st 섹션이 메모리에 로딩될 때 온전하게 1st 섹션에 기록될 수 있습니다.

또한, 원본파일(notepad.exe)의 이미지가 통째로 풀리기 때문에 프로그램이 정상적으로 실행됩니다.

 

  

 

5. RVA to RAW

 

각종 PE 유틸리티들이 UPack으로 패킹된 PE를 만나서 강제 종료되었던 이유는

RVA → RAW 변환에 어려움을 겪었기 때문이라고 합니다.

UPack의 제작자는 많은 테스트를 통해서 Windows PE 로더의 버그를 알아낸 후 이를 UPack에 적용합니다.

아래는 책에 기재되어 있는 RVA → RAW 변환 공식입니다.

RAW - PointerToRawData = RVA - VirtualAddress

RAW = RVA - VirtualAddress + PointerToRawData

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

*PointerToRawData : 파일에서 섹션의 시작 위치

*VirtualAddress : 메모리에서 섹션의 시작주소 = RVA

 

위 공식대로 UPack 샘플에서 EP의 RAW(파일오프셋)를 계산해봅니다.

위의 공식대로 RAW를 계산하려면 3가지를 알아야 합니다.

RVA

VirtualAddress

PointerToRawData

 

RVA 값은 OPTIONAL_HedaerAddressOfEntryPoint 1018(4,120) 입니다.

 

VirtualAddress 값은 RVA값이 속해 있는 섹션의 메모리 오프셋입니다.

1018(4,120) 값은 Stud_PE에서 VirtualSize의 값과 VirtualOffset을 고려하여 판단합니다.

첫번째 섹션 영역이 1000 ~ 14FFFF 이므로, 해당 주소는 첫번째 섹션에 속합니다. 

따라서 VirtualAddress = 1000(4096) 입니다.

자연스럽게 PointerToRawData 값은 첫번째 섹션의 RawOffset = 10(16) 입니다.

 

구한 값들을 바탕으로 공식을 적용하면 아래와 같습니다.

RAW = 1018 - 1000 + 10 = 28

Hex editor로 살펴보면, 뭔가 이상합니다. 코드가 아니라 문자열이 존재하기 때문이지요.

이것은 UPack의 특성중 하나인, PointerToRawData 값을 이용한 트릭입니다.

 

일반적으로 섹션 시작의 파일 오프셋을 가리키는 PointerToTawData 값은 FileAlignment의 배수입니다.

UPack의 FileAlignment의 값은 NT_Header - OPTIONAL_Header에서 200으로 명시되어 있으며, 

일반적으로도 PointerToTawData 값은 0, 200, 400 등의 값을 가진다고 합니다.

 

하지만 UPack으 PointerToRawData 값이 10으로 지정되어 있습니다.

FileAlignment(200)의 배수가 아니기 때문에 PE로더는 강제로 FileAlignment 배수에 맞춰서 인식합니다. (이경우는 0)

이것이 바로 UPack 파일이 정상적으로 실행은 되지만, PE 유틸리티에서 에러가 발생한 이유입니다.

따라서 이것을 적용한 공식은 아래와 같습니다.

RAW = 1018 - 1000 + 0 = 18

 

그렇다면 이제 디버거를 통해 메모리에 올라간 이미지와(1018) 파일의 오프셋(18)에 있는 데이터를 비교하여

코드가 동일하다면, RVA → RAW 변환이 잘 이루어진 것입니다. (아래그림 참조)

 

 

 

6. Import Table(IMAGE_IMPORT_DESCRIPTOR array)

 

① 원본파일 Import Table

UPack의 IMPORT_Table 역시 매우 특이하게 구성되어 있습니다.

차이점을 알기 위해서 원본파일(notepad.exe)의 정상적인 구조부터 살펴보겠습니다.

PE파일은 자신이 어떤 라이브러리를 임포트하는지 IMAGE_IMPORT_DESCRIPTOR(IID) 구조체에 명시합니다.

IID는 이번 소단원의 제목과 같이 IMPORT_Directory_Table(IDT)이라는 용어로 부르기도 합니다.

아래 그림은 VIsual Studio 에서 확인한 IMAGE_IMPORT_DESCRIPTOR(IID) 구조체입니다.

 

IID or IDT는 NT_Header 내부의 OPTIONAL_Header에 위치하며,

DATA_DIRECTORY 구조체의 두번째 멤버 IMPORT_Table 항목에 RVA, Size 값이 명시되어 있습니다.

 

해당 주소값을 찾아가보면, IMPORT_Table이 존재하는 곳은 PE 헤더가 아닌 PE 바디입니다.

PE View 도구에서는 IMPORT_Directory_Table 항목으로 명시해주고 있으며,

20byte 단위의 IMAGE_IMPORT_DESCRIPTOR(IID) 구조체 목록이 존재함을 확인할 수 있습니다.

마지막은 NULL 구조체로 구성됨을 체크하고 넘어갑니다.

 

IID에서는 API 목록 RVA 값을 가지는 멤버 2개가 존재합니다.

OriginalFirstThunk = INT(Import Name Table)

FirstThunk = IAT(Import Address Table)

해당 멤버들이 실제 PEView에서 아래와 같은 위치에 표현되고 있음을 체크합니다.

 

실제로 INT(Import Name Table), IAT(Import Address Table) 내부를 들여다보면

HintName이 존재하는것은 동일하지만, IAT는 VA, INT는 RVA값이 존재하고 있습니다.

 

마지막으로 해당 IMAGE_IMPORT_DESCRIPTOR(IID) 구조체 목록을 파일 RAW에서도 살펴봅니다.

20byte의 구조체 목록임을 확인하고, IMPORT_Table이 NULL 구조체로 끝나는지 확인합니다.

START RAW = 7604 - 1000 + 400 = 6A04

END RAW = 76C8 - 1000 + 400 = 6AC8

 

 

② UPack Import Table

정상적인 원본파일(notepad.exe)의 IMPORT_Table을 살펴보았으니, 이제 UPack의 차례입니다.

UPack에서는 PEView가 정상적으로 동작하지 않으므로, 직접 파일의 RAW에서 값을 살펴봅니다.

방법은 동일하게 DATA_DIRECTORY에서 RVA를 얻습니다. 값은 271EE 입니다. (리틀엔디안 주의)

 

271EE가 속하는 섹션은 3st 섹션의 VirtualOffsetVirtualSize를 통해 27000 임을 알 수 있습니다.

PointerToRawData의 경우 10 이지만, 위에서의 트릭에 유의하여 0으로 수정합니다.

이를 이용한 계산 결과는 아래와 같습니다.

RAW 271EE 27000 - 0 = 1EE

 

Hex Editor를 통해 해당 주소값을 살펴보면 아래와 같습니다.

구조체의 대부분이 0 이기 때문에, 각 멤버의 값을 분별하기 어렵지만 구조체를 보면서 값을 채워봅니다.

 

4byte씩 끊어서 보면 아래와 같으며, Name 멤버는 KERNEL32.DLL을 나타냅니다.

계속 섹션에서 RVA → RAW 계산 하느라 정신없지만, 헤더영역은 RVA와 RAW값이 동일합니다.

- INT :: OriginalFirstThunk = 0000 0000  (0일 경우 IAT값 참조)

- TimeDataStamp = 0000 0000

- FowarderChain = 0000 0000

- Name = 0000 0002 (KERNEL32.DLL)

- IAT :: FirstThunk = 0000 11E8 (Little Endian)

 

하지만, 문제는 그 다음 구조체입니다.

Name 멤버가 가르키는 문자열도 없으며, IMPORT_Table의 끝을 나나태는 NULL 구조체도 아닙니다.

- INT :: OriginalFirstThunk = 0000 0000 (0일 경우 IAT값 참조)

- TimeDataStamp = 0000 0000

- FowarderChain = 0000 0000

- Name = 0003 0008

- IAT :: FirstThunk = 0005 0000 (Little Endian)

 

이것은 PE스펙에 어긋난 듯이 보이지만, 섹션이 로딩될때의 헛점을 노린 UPack의 트릭입니다.

다시한번 Stud_PE에서 3st 섹션의 멤버값들을 확인해보겠습니다.

3st 섹션의 크기는 1F0이며, 시작지점은 10이므로, 로딩되는 영역은 10 ~ 1FF 입니다.

정확한 크기값은 Hex Editor로 체크하면 정확합니다.

 

하지만 RawOffset은 PE로더에 의해 10 0 으로 강제 변환 되므로

결론적으로 VA 27000 위치에 파일의 0 ~ 1FF 영역이 로딩되며, 길이는 200입니다.

실제로 3st 섹션의 시작부분인 VA 27000을 살펴보면, DOS_Header의 시그니처인 "MZ"로 시작되고 있습니다.

 

또 염두해두어야 할 부분은 3st의 VirtualSize1000 이므로, 271FF 영역 이후부터는

NULL 값으로 채워집니다. 즉, IMPORT_Table의 마지막 멤버가 NULL 구조체가 아니어도 정상 실행됩니다.

 

그렇다면 마지막으로 UPack이 KERNEL32.DLL에서 어떤 API를 임포트하는지

실제로 IAT를 따라가서 확인해 보겠습니다. IAT의 RVA 값은 11E8 이었으므로, RAW값은 아래와 같습니다.

RAW = 11E8 - 1000 + 0 = 1E8

 

1E8 주소로 가보면, INT가 있습니다.

INT, IATIMAGE_IMPORT_BY_NAME 구조체를 가리키는 Name_Pointer(RVA) 배열이며, 끝은 NULL입니다.

따라서 UPack의 경우 2개의 API를 임포트하고 있음을 알 수 있습니다.

 

IMAGE_IMPORT_BY_NAME 구조체는 2byte Hint 멤버와 Name 배열 멤버로 구성됩니다.

 

첫번째 주소는 28이며, Hint0B01과 함께 LoadLibraryA API 이름이 존재합니다.

두번째 주소는 BE이며, Hint0000과 함께 GetProcAddress API 이름이 존재합니다.

 

 

 

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

0x02 디버깅 - OEP 찾기

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

 

1. OllyDbg 실행 에러 

Upack은 PE헤더를 독특하게 변경하는 것이 문제가 되고, 안티 디버깅 기법은 없습니다.

일단 OllyDbg를 이용해 패킹된 파일을 실행시키면 에러 메시지가 출력된다고 합니다.

하지만 OllyDbg 201 버전에서 실행하면 오류는 커녕 EP까지 잘 찾아주는 현상(?)이 발생했습니다.

 

크리티컬한 에러가 아니라고 저자분께서 적어 주셨기 때문에, 그냥 넘어가도 될것 같지만.. 

디버깅을 연습도 할겸 버전을 낮추어 실행해보기로 했습니다.

OllyDbg 110 버전에서 실행시키면 아래와 같이 오류창이 뜹니다.

에러의 원인은 UPack이 OPTIONAL_Header에서 NumberOfRvaAndSizes 값을 10(16)  A(10)

변경했기 때문에 OllyDbg의 초기검증 과정에서 에러가 발생합니다.

 

또한 위와 같은 에러로 인하여 OllyDbg는 EP로 가지 못하고 아래와 같이 ntdll.dll 영역에서 멈춥니다.

OllyDbg의 버그 또는 엄격한 PE체크 때문에 발생하는 현상입니다.

 

그렇다면 직접 EP를 설정해주기 위해 Stud_PE를 통해 기본 정보를 수집합니다.

EntryPoint의 RVA값은 1018이며, ImageBase01000000입니다.

따라서 EntryPoint의 VA값은 01001018입니다.

 

OllyDbg의 Code창에서 01001018로 이동한후

"New Origin here" 명령을 이용하여 강제로 EIP를 변경합니다.

이미 EP로 설정된 곳에서 우클릭하면 해당 메뉴가 나타나지 않으니 주의하세요.

 

 

 

2. BP걸고 달리기

 

모든 패커에는 디코딩(Decoding Loop) 루프가 존재한다고 합니다.

압축/해제 알고리즘 자체가 많은 조건 분기와 루프로 구성되어 있다보니 필연적입니다.

이러한 디코딩 루프를 디버깅할 때에는 조건 분기를 적절히 건너뛰어서 루프를 탈출해야 합니다.

레지스터를 잘 보면서 어떤 주소에 값을 쓰고 있는지 잘 살펴야 하지만.. 많은 경험이 필요합니다.

 

Upack은 두번째 섹션에 압축된 원본 데이터가 존재하고,

이 데이터를 디코딩 루프를 돌면서 첫번째 섹션에 압축해제 합니다.

그럼 EP코드부터 디버깅을 시작합니다!

 

처음 두명령은 010011B0 주소에서 4byte를 읽어서 EAX에 저장하는 명령어입니다.

이는 원본 notepad의 OEP(Original Entry Point) 입니다만, 아직은 모른다는 가정하에 진행합니다.

LODS 명령어는 ESI가 가르키는 주소에서 4byte를 읽어서 EAX 레지스터에 저장하는 기능이며, ESI 값을 증가시킵니다.

 

EAX에 저장된 명령어가 OEP인줄 알고 있다면, BP를 걸고 달리면 된다고 합니다.

해당 주소로 가보면 NULL로 채워져 있는 공간 뿐이어서 당황했습니다.

 

하지만 BP를 걸고 달리면 없던 코드가 생겨나면서 OEP로 추정되는 코드가 나타납니다.

패킹되지 않은 원본파일의 OEP와 비교해보니, 오른쪽의 설명을 제외한 코드가 모두 일치했습니다.

이로서 "BP를 걸고 달린다"의 의미를 어느정도 알 것 같네요.

  UPack      정상

 

 

 

 

3.압축해제, 디코딩을 위한 초기 세팅(?)

이어서 트레이싱을 계속 진행합니다.

OEP와 LoadLibrary 함수를 스택에 PUSH한 이후에 JMP 명령어로 010010A0 주소로 이동합니다.

GetProcAddress 함수와 01013FFF 주소에 있는 값을 동일하게 PUSH 합니다.

해당값들은 추후에 IAT를 새롭게 구성할때 POP 하여 사용합니다.

 

이후에는 아래 명령어를 실행하게 되는데, 결론부터 말하면 3st 섹션 → 2st 섹션으로 복사하는 코드입니다.

REP 명령어로 27(ECX) 횟수 만큼 반복문을 수행하기 위해 EDI, ESI레지스터에 주소값을 저장합니다.

EDI 레지스터에는 2st 섹션의 0101FE28 주소값이 저장되며, 어떤 영역인지 계산해보면

15000(VA) + AE28(RawSize) = 1FE28 이므로, 2st 섹션 끝에 채워진 NULL(0) 영역임을 알 수 있습니다.

REP MOVS DWORD PTR ES : [EDI], DWORD PTR DS : [ESI]

 

ESI 레지스터에는 3st 섹션의 010270F0 주소값이 저장되며, 구조상 1st 섹션과 동일하므로

정상적이라면 F0 주소에는 Delay_Import_Descriptor 값이 존재해야 하지만,

위에서도 살펴보았듯이 UPack이 OPTIONAL_Header에서 NumberOfRvaAndSizes 값을 10(16)  A(10)

변경하여 본인의 디코딩 코드를 삽입하기 위해 만들었던 공간 영역입니다.

 

다만, 해당 영역의 모두를 복사하고 있지 않으며, JMP 구문 이후에 영역을 복사합니다.

아마 디코딩 코드들을 한군데에 몰아넣고, 필요한 부분만 꺼내 쓰는 모양입니다.

 

아래 사진을 보시면, 위에서 공백이었던 0101FE28 주소에 4byte씩 데이터가 복사되고 있음을 알 수 있습니다.

PE헤더 분석에서 2st 섹션에는 원본파일이 압축되어 있다고 분석한바 있습니다.

왜 3st 섹션에 있는 명령어 코드를 2st 섹션 빈공간으로 복사하고 있는지는 모르겠지만, 일단은 넘어가기로 했습니다.

 

반복문이 종료된 이후의 코드를 계속 살펴봅니다.

PUSH 명령어로 DS : [ESI+4] = 190 주소가 가리키는 값 132를 Stack에 저장합니다.

3st 섹션은 1st 섹션과 동일하므로, 1st 섹션에서 190이 어떤 값인지 살펴보면 Relocations Number 입니다.

이후에 무슨 이유인지는 모르겠지만 EAX 값을 FFFFFFFF로 설정합니다.

이것이 문자열의 길이를 알아낼때 반복문의 최대치를 설정한 것인지, -1로 설정한 것인지 아직 파악이 되지 않습니다.

 

JMP 명령을 하기전에 ES : [EDI] = 0101FEC4 주소값에 EAX레지스터 값을 저장합니다.

해당 주소값은 3st 섹션에 있는 데이터를 2st 섹션에 복사한곳의 바로 뒷부분, NULL(0) 공간입니다.

 

JMP 010010D8 명령은, 방금 위에서는 뒷부분만 복사했던 디코딩 코드의 처음으로 이동합니다.

INC EAX 명령은 EAX값을 0으로 되돌리는 목적으로 보이며,

STOS 명령으로 EAX값을 저장하는데, EAX값이 0이므로 헤딩 주소의 NULL(0) 값이 유지됩니다.

 

이후에 REP명령어로 Write할 값 EAX = 1 을 저장하고, 반복횟수 ECX = 4 를 저장한뒤

아래 그림과 같이 16byte에 EAX값을 Write 합니다. (리틀엔디안 주의)

 

다음 명령어 역시 동일한 방법으로 EAX = 400, ECX = 1C00(7,168) 를 레지스터에 저장한뒤 

아래 그림과 같이 0101FEDC 주소부터 01026ECB 까지 1C00(7,168) 횟수만큼 EAX값을 Write 합니다.

이후에 "MZKERNEL32.DLL" 문자열과 ECX(=0) 레지스터를 스택에 PUSH합니다.

 

 

 

4. decode()

 

JMP 0101FD13 명령어를 통해 해당 주소로 넘어옵니다.

세번째 라인을 보면, CALL DWORD PTR DS : [ESI] 명령이 존재합니다.

이때 ESI 레지스터의 값은 0101FCCB 이며, 이게 바로 decode() 함수의 주소입니다.

 

책에서 decode() 함수라고 적혀 있어서 미리 알게되었지만,

어떤 행동을 하는지 한눈에 파악이 안됩니다만, 계속 트레이싱을 하다보면 굉장히 자주 호출됨을 알 수 있습니다.

 

잘 파악이 안될때에는 일단 실행 결과를 보면서 파악하곤 합니다.

일단 해당 함수를 CALL 하기 전에 특정 인자를 PUSH 한것은 아니지만,

레지스터로 인자를 전달하는 함수도 있으므로, 바로 전에 EDX에 할당한 주소를 체크합니다. (아래 그림 파란색)

그리고 F8 트레이싱을 하게되면, 0101FEC4 주소의 4byteFFFFFFFF 7FFFFC00 으로 변경됩니다.

정확히 어떤 메커니즘인지는 모르겠지만, 일단 2st 섹션의 특정 값을 변동시킵니다.

 

해당 작업이 다 이루어지고 나면 아래와 같이 대부분의 데이터가 채워집니다.

아마 어떤 방식으로든 채운 데이터를 활용하여 1st 섹션에 원본파일을 기록할 것으로 생각됩니다.

 

 

 

5. 디코딩 루프의 끝 + IAT 세팅

 

UPack의 디코딩 어셈블리 코드를 세세하게 분석하기엔, IDA Hex-ray에서 제공해주는 코드조차 매우 난잡합니다.

하지만 패킹의 경우, 특히 UPack은 메모리상에서 2st 섹션의 압축 원본이 1st 섹션이 풀리므로

디버깅의 초점을 섹션간의(3st 2st 또는 2st 1st) Write 행위로 바꾸었습니다.

그렇게 진행하다보면 책의 저자분이 책에 기재해놓으신 코드들 위주로 눈에 들어오게 됩니다.

 

먼저 1st 섹션에 데이터가 Write 되는 순간을 포착하기 위해 Hardware Breakpoint를 설정합니다.

이때 단일 Data size 에만 Breakpoint를 설정하면 그냥 넘어가는 케이스가 발생하여

동일주소에 3가지 Data size를 모두 설정하였습니다.

 

Breakpoint를 설정하고 F9를 눌러 첫번째 트레이싱을 진행하면 STOS BYTE OTR ES: [EDI] 코드에서 처음으로 멈춥니다.

EDI 주소에 EAX 레지스터의 값을 BYTE 단위만큼 저장하는 명령어입니다.

실제로 해당 주소에 가보면, Breakpoint를 걸었던곳의 1byte가 NULL(0)로 채워진것을 볼 수 있습니다.

반복적으로 트레이싱을 하면, UPack이 1st 섹션의 데이터를 지울때 항상 해당 명령어로 1byte를 먼저 지우는 패턴을 보입니다.

 

두번째로 멈추는곳은 몇줄 위에 있는 0101FE57 주소입니다.

REP 명령어로 ECX 레지스터에 할당된 횟수만큼 반복하며 EDI 레지스터가 가리키는 주소에 값을 씁니다.

 

F8 트레이싱으로 REP 명령어를 실행시키면

아래와 같이 1st 섹션이 NULL(0) 값으로 채워짐을 볼 수 있습니다.

UPack은 2st 섹션의 데이터를 1st 섹션에 압축해제하기 이전에 이와 같이 NULL(0) 값으로 덮어씁니다.

 

트레이싱을 계속 진행하면 NULL(0) 값으로 써진 1st 섹션에 새로운 데이터가 써집니다.

원본파일(notepad.exe)의 1st 섹션인 SECTION.text 값과 바이너리를 비교해보면 초반부는 동일합니다.

 

OllyDump와 같은 플러그인을 이용해서 원본파일(notepad.exe)의 메모리값과 비교해보면 완벽하게 일치하지 않습니다.

압축해제 되는 과정에서 약간의 변화 또는 손실이 일어나는것으로 보이지만, 실행되는데 문제는 없습니다.

첫번째로 원본과 틀린 부분은 1350 주소부터 시작하는 IMAGE_DEBUG_DIRECTORY 입니다. 

  원본 IDD

  원본 IDD

   UPack Dump

 

IMAGE_DEBUG_DIRECTORY의 경우에는 UPack으로 실행압축을 하는 과정에서

"Removing debug data [001C]" 라는 출력 문구를 통해, 실행압축 과정에서 삭제되었음을 유추할 수 있습니다.

001C의 경우 원본파일 IMAGE_DEBUG_DIRECTORY 사이즈 값과 동일한데, 사이즈를 출력해준것으로 판단됩니다.

 

두번째로 원본과 틀린 부분은 7604 주소의 IDT, INT, Hints/Names&DLL_names 입니다.

해당 멤버들은 하나같이 Import_Table과 관련된 항목들인데, NULL(0)로 채워져 있습니다.

하지만 해당 부분들이 아예 사라진것은 아닙니다. IAT세팅에서 다시 들여다 보겠습니다.

  원본 IDT

  원본 IDT

  UPack Dump

 

이부분도 역시 실행압축 과정에서 출력된 문구에서 리빌딩, 리컴파일로 언급되어 있는 사항들입니다.

대괄호로 출력된 16진수는 원본파일 IDT, RDT 항목들 사이즈와 일치합니다.

 

REP 명령어는 0101FE5E 주소에 있는 CMP 명령어가 만족될때까지 루프를 돌며 여러번 수행되며

[ ESI + 34 ] 주소가 가리키는 값은 0102718C + 34 = 010271C0 으로서, 4byte 01014B5A 값입니다.

해당값은 1st 섹션이 끝나는 주소인 15000(VA)과 매우 밀접합니다.

 

 

 

6. IAT 세팅 

 

Upack을 포함한 일반적인 패커는 디코딩 루프가 끝나면 원본 파일에 맞게 IAT를 새롭게 구성합니다.

위에서 분석했다 싶이, UPack은 kernel32.dll 하나만을 임포트하고 있으며,

 

IAT를 따라가 보면 LoadLibraryA, GetProcAddress 두개의 API 만이 존재하고 있음을 확인했습니다.

두개의 함수를 호출하여 0101FEAC 주소에 있는 STOS 명령으로 1st 섹션에 IAT를 채웁니다.

 

 

이때 참고하는 영역이 원본 파일에는 없는 01014000 주소 영역입니다.

원래 원본파일은 OllyDbg, Dump 등을 살펴보면 010133F0 주소 근처가

PADDING으로 채워진 이미지의 마지막 영역입니다.

 

하지만 UPack은 IAT를 재구성 하기 위해 01014000 주소 영역에

원본파일(notepad.exe)의 IDT 리스트 순서대로 DLL이름과 API 이름들을 저장해두었습니다.

이것을 읽어들이면서 1st 섹션의 IAT를 복원합니다.

 

 

 

7. BONUS - IDA에서 살펴보기

 

OllyDbg를 이용하여 디버깅을 진행하다보니

복잡한 압축해제 알고리즘을 Hex-ray로 보면 좀 더 쉬워질까? 라는 생각이 들었습니다.

결과적으로 도움이 되지는 않았지만, 다른 케이스에서는 도움이 되길 바라는 마음으로 남깁니다.

 

IDA로 패킹된 notepad.exe 를 Open하면 정상적인 PE구조가 아니기 때문에

아래와 같은 오류창이 3~4개 정도 연달아 뜹니다. 당황하지 말고 모두 OK 눌러줍니다.

 

"Can't find translation..." 관련 오류창만 짚고 넘어가겠습니다. (다른 케이스에 종종 나타나므로..)

해당 오류가 발생하는 곳은, DATA_DIRECTORY 영역으로서, Relocation Directory RVA, Size 멤버가 위치합니다.

CFF에서 Invalid 값으로 표현해주고 있군요.

 

지금까지 UPack을 분석하면서의 짧은 경험으로도, 해당 값은 RVA로 사용되기엔 너무 큽니다.

차근차근 분석해보면...일단 UPack은 1st 섹션의 시작 위치를 10으로 잡기 때문에, B0 주소의 데이터는

메모리에 로딩되면 1st 섹션 RVA(1000) + ImageBase(01000000) = 010010B0에 로딩됩니다.

주소에 찾아가보면 REP MOVS DWORD PTR ES : .... 명령어 코드가 존재하며

 

위에서 분석했던 "디코딩, 압축해제를 위한 초기 세팅" 부분의 코드임을 알 수 있습니다.

UPack이 NT_Header 영역 또한 일부 무시하고 디코딩 코드를 삽입했기 때문에 이러한 현상이 발생하는 것이고

이와 비슷한 오류창이 뜬다면, 해당 영역이 소스코드로 사용되는 패킹일 수 있음을 염두해둡니다.

 

말이 나온김에, 이와 같은 사실을 이용해서 PE헤더 영역에서 코드로 사용된 부분을 다시 살펴보면

아래 그림의 영역까지 포함하여 총 2개(1018 ~ 1023, 10A0 ~ 10BB) 영역입니다.

 

해당 영역을 노란색으로 그려보면 헤더 분석시에 그렸던 그림이 아래와 같은 그림으로 변경됩니다.

 

결과적으로 OPTIONAL_HeaderDATA_DIRECTORY에서 아래에 표시된 상당수의 변수가 무시되고, 

UPack의 디코딩 코드가 삽입 되었음을 알 수 있습니다. 아마 무시되도 실행에는 문제가 없는 멤버겠지요.

 

다시 본론으로 돌아와서...

로딩이 완료되면 Function window에 start 함수 달랑 1개만 존재함을 볼 수 있습니다.

이때 F5를 눌러 Hex-ray를 호출하면, 어느정도 복잡한 코드가 생성되지만 온전한 코드는 아닙니다.

사실 지금 생성된 코드만 해도 수십개의 변수가 존재하며, 코드도 엄청 복잡합니다. (스샷엔 2/5 정도만 담김)

 

온전한 코드가 생성되지 않는 정확한 이유를 단정지어 설명드리기 어렵지만,

개인적인 생각으로는 IDA가 UPack이 생성한 바이너리를 소스코드로 인식하지 않아서 발생하는 문제로 보입니다.

아래 스샷은 0101FCCB 주소로 Jump 했을때 이동된 장소이며, 위에서 보았던 decode() 함수입니다.

어셈블리 코드로 해석되지 않고, 16진수 데이터로 표현되어 있습니다.

 

디버깅을 진행하다보면 실행 흐름에 따라 16진수 데이터가 새로운 어셈블리 코드로 변경됩니다.

이러한 점을 이용하여 IDA에서 어느정도 트레이싱을 진행한 다음 Hex-ray를 호출하면 됩니다.

보통 OllyDbg에서 OEP를 걸고 디버깅 하듯이, IDA에서도 동일하게 진행합니다.

일단 Debugger의 종류를 Local Win32 debugger로 설정하고 Start합니다.

 

보통 OllyDbg에서 Ollydump 플러그인을 이용한 언패킹을 진행할때에는

책에서 학습한대로 OEP에 브레이크 포인트(F2)를 걸고 달리게 되지만,

Upack의 경우 원본파일이 우리가 관찰해야될 1st 섹션의 코드 부분에 압축해제 됩니다.

디코딩 코드를 관찰하기 위해 1st 섹션에 데이터를 쓰는 0101FE57 주소의 명령어가

실행되기 전의 주소에(근처) BP를 걸고 달립니다. 

 

어느정도 트레이싱이 진행되었으므로, Debugger - Take memory snapshot을 실행합니다.

저장할 세그먼트는 무엇을 해도 상관없지만, all segments를 선택할 경우 너무 많은 함수목록이

Function window에 나타나서 번잡스럽게 느껴졌습니다.

이후 정지버튼 또는 Debugger-Terminate process 기능을 이용해 디버깅을 종료시킵니다.

 

이후에 Options - General - Analysis 옵션에서 Reanalyze program을 실행하고서

Hex-ray를 실행하면 더 길고 복잡한 온전한(?) 코드를 얻을 수 있습니다.

처음으로, 차라리 OllyDbg를 이용한 트레이싱이 더 낫겠다...라는 생각을 하게 만들더군요. 

 

 

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

0x03 마무리

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

 

언젠간 해야지 해야지...하고 미뤄두기만 했었던 실행압축에 대해 공부했습니다.

모르는 부분을 겸사겸사 계속 추가했더니 내용이 또 길어졌네요.

그래도 이번 기회에 PE구조에 대해 좀더 익숙해졌고, 리버싱에 대한 막연함이 많이 사라진것 같습니다.


개인적으로..

그냥 공부했다면 너무 어려웠을 내용을 책으로 쉽게 내주신 이승원 저자분에게 감사드립니다.

잘못된 부분이나 오타는 댓글 남겨주세요.

 

 

posted By Message.

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

 

':: 리버싱' 카테고리의 다른 글

[정보 보안 개론] Chapter 06 악성코드  (0) 2016.11.07
Red_Seek :: 스택 프레임  (0) 2014.01.29
[Red_Seek] abex' crackme #1 (크랙미 #1)  (0) 2014.01.23
[Red_Seek] 스택  (0) 2014.01.22
[Red_Seek] IA-32 Register  (0) 2014.01.14
posted by Red_Message

안녕하세요. Message 입니다.

<실전 악성코드와 멀웨어 분석> 책의 실습 문제 9-1을 분석합니다.

점점 분량이 늘어나고 있습니다...

스크롤 압박이 다소 있지만, 중간중간 필요한 서브루틴을 적절히 검색하시면 편리합니다~

 

※ 분석환경 : WindowsXP, Windows7, Vmwre 12.1.0 build, IDA 6.1 and 6.6

 

 

 

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

0x00 기초 동적 분석

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

문제풀이에 앞서 간단한 동적 분석을 진행합니다.

 

1. PE구조 & 패킹

별도의 패킹이 되어 있지 않습니다.

 

2. DLL

KERNEL32, ADVAPI32, WS2_32 등은 자주 보아왔던 DLL이었지만

SHELL32 ShellExecuteA 함수가 눈에 띄었습니다.

어디선가 보았다 싶어서 찾아봤더니 OLE 취약점 분석 포스팅에서 VBScript에 있던 함수명이네요.

해당 취약점에서는 ShellExecute 함수를 이용하여 cmd.exe 파일을 실행시키는 용도로 사용하였는데

이번 케이스는 어떻게 사용될지 궁금합니다.

 

3. String

1) .data 섹션

레지스트리 경로, URL, system32 폴더, CMD 명령어, Service 관련 문자열들이 눈에 띕니다.

네트워크 통신과 더불어 시스템에 영향을 미치는 기능을 수행하는 악성코드로 예상됩니다. 

 

2) .rdata 섹션

대부분이 프로그램 생성시 포함되는 소스코드 또는 Import DLL 목록으로 판단됩니다.

R6016~R6028로 시작하는 문자열들이 많이 등장하고 있습니다.

프로그램 로드 또는 실행시 CRT에 의해서 발생하는 C Runtime Error 관련 에러 문자열입니다.

이번 문제 역시 Lab06-01 문제와 동일하게 printf 를 WIndows API로 명명해주지 않기 때문에

내부에서 사용되는 함수/문자열들이 Hex-ray를 통해 소스코드로 변경되면서 수면 밖으로 나온것으로 판단됩니다.

(해당 문자열들을 XREF로 추적하면 sub_402E7E(printf) 에서 사용되고 있습니다.)

MSDN : https://msdn.microsoft.com/en-us/library/6f8k7ad1.aspx

 

Tip. String Window에서 .data vs .rdata 섹션

 IDA를 이용한 초기 동적분석에서 항상 살펴보는 곳이 String Window입니다.

 그리고 String 값들은 대게 data/rdata 섹션에 존재했습니다. 어떤 차이일까요?

 

 data Section

 섹션헤더 Chracteristics 필드를 살펴보면 READ/WRITE 속성을 모두 가지고 있습니다.

 저장되는 변수는 초기화된 전역변수(global)와 정적변수(static) 등 입니다.

 

 ② rdata Section

 CONST변수, 하드코딩(printf 등) 문자열, 소스코드에 포함된 문자열 등이 저장됩니다.

 또한 Import 되는 DLL 목록이 저장되기로 하므로, API들과 DLL 이름등이 저장됩니다.

 

 ③ Test

 VS2015로 global, static, local 타입의 포인터/배열을 선언하여 테스트해보았습니다.

 테스트 환경은 학습을 위해 release 모드로 빌드후 XP에서 실행하였습니다.

   LOCATION : 선언된 변수의 위치,   VALUE : 문자열 위치,   STR : 문자열

 

 

 위 결과를 기반으로 정리하면 아래와 같습니다.

 ① ptr String : .rdata 섹션

 ② arr String : 전역변수  data섹션 (const 제외) , 지역변수  Thread Stack (static 제외)

 포인터 스트링은 모두 .rdata 섹션이며, 배열 스트링은 상황에 따라 복잡합니다.

 배열이라 하더라도 CONST는 상수이므로 자연스럽게 rdata 섹션에 저장되며,

 나머지는 data + Thread Stack 영역에 저장됩니다. 

 

 <참고>

 주소값들을 살펴보면 스택이 PE보다 낮은 주소에 있습니다.

 처음에는 오류인줄 알고 헷갈렸습니다. 왜냐하면 스택은 높은곳에서 거꾸로 자라며,

 계속 자라면 Heap하고 충돌...등등 어디서 주워들은 얘기로만 기억하고 있었고,

 구글링을 해보아도 같은 얘기를 하는게 맞나 싶을 정도로

 다양한? 메모리 구조 그림들이 나왔습니다.

 (아마 Unix계열과 Windows계열이 섞여서 돌아다녀서 그런가봅니다.) 

 

 Vmap으로 메모리를 힙/스택 모두 PE보다 상단(낮은주소)에 있었으며

 이것에 맞는 메모리 구조를 표현한 그림은 아래와 같았습니다.

  Thread Stack : 0x00030000 ~ 0x0012FFFF

 

 

 *그림출처 : http://www.securitysift.com/windows-exploit-development-part-1-basics

 

 

 

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

0x01 정적 분석

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

문제풀이를 위한 정적 분석을 진행합니다.

 

 

1. main 함수 - 인자가 1개인 경우

main 함수에 진입하면, __alloca_probe 함수를 통해 스택을 0x182C 만큼 할당한뒤

전달된 인자의 개수가 1인지 체크합니다. 만약 결과값이 1이라면 sub_401000 함수를 호출합니다.

__akkica_probe 함수의 경우 지난 문제에서 상세 분석을 마쳤으므로, 무난하게 패스합니다.

 

 

sub_401000 함수를 살펴보면,

RegOpenKeyExA 함수를 이용하여 빨간색으로 배경이 지정된 문자열의 레지스트리 키를 Open하고

"Configuration" 항목값을 가져옵니다.

 

가끔 IDA에서 String 값을 제대로 못읽어와서 위에처럼 빨간색으로 처리되는 경우가 종종 발생하는데

(불행하게도 아직 해결하는 방법을 몰라요)

함수에 들어가는 인자값들은 OllyDbg를 통해 좀더 직관적이고 빠르게 분석이 가능합니다.

레지스트리키는 HKLM하이브의 "SOFTWARE\Microsoft\WPS" 이며, 해당 키에서 "Configuration" 항목값을 가져옵니다.

※ sub_401000 정리 : 레지스트리 존재 여부 체크

 

레지스트리 open에 실패하거나, "Configuration" 항목값이 존재하지 않을 경우 sub_402410 서브루틴을 호출합니다.

sub_402410 함수의 경우, 아래 그림에서 보다싶이 main에서 매우 빈번하게 호출되는 함수입니다.

해당 함수의 내부를 살펴보면, GetModuleFileNameA, GetShortPathNameA 함수를 이용하여

Full/Short Path를 얻으며, 이후에 "/c del" 문자열을 Parameters 변수에 복사합니다. 

 

 

이후 이어지는 코드는 IDA에서는 비교적 복잡해 보이지만

이전 문제에서 한번 분석해본 내용으로서, do-while 문으로 문자열의 길이를 구하는 내용입니다.

현재 szLongPath 변수에 Short Path가 들어있으니, Short Path의 문자열 길이를 얻게 됩니다.

이러한 do wile문은 버전이 낮은 Hex-rays에서 문자열이 들어가는 함수가 나올경우 자주 등장하는

패턴입니다. 버전을 높이면 생략되더군요.

 

 

첫번째 탈출 조건은 변수 v0에 0xFFFFFFFF(-1)을 할당한 후 0이 될때까지 -1을 하여서 0이 되면 break 합니다.

두번째 탈출 조건은 문자열이 담긴 v1 변수에서 문자열을 끝을 나타내는 null 문자 발견입니다. 

처음에는 아래의 소스코드가 헷갈렸는데,

*v1++ == 0 조건이 성립할때 v2에 *v1++을 할당하는 내용으로 해석하면 될 것같습니다.

 ex) v2 = *v1++ == 0;   if( 변수 또는 계산식 == 0 ) v2 = 변수;

 

이후에 v4 = ~v0; 코드로 값을 반전하여 글자수를 얻습니다. 글자수는 8자리네요.

 

이어지는 코드는 비슷하게 Parameters 변수에 저장된 "/c del " 문자열의 길이를 동일 로직으로 얻습니다.

특이하게 v7 변수에 &v1[-v4] 주소값을 할당하는 부분이 있습니다.   v2 = *v1++ == 0;  

위 코드에서 *v1++ 로 인해 증가된 v1의 의 주소값을 다시 "/c del " 문자열을 가리키는 주소로 보정하기 위함입니다.

 

이후에 memcpy 함수를 이용하여 v7에 저장된 문자열을 (void *)(v6 -1)

주소값에 복사하여 아래의 문자열을 완성합니다.

경로값을 고려해서 del 문자열 뒤에 한칸 스페이스가 삽입되어 있는 부분을 체크합니다.

 

역시 동일한 방식으로 ">> NUL" 문자열의 글자수를 계산하고

memcpy 함수를 통해 최종문자열 "/c del c:\단축경로\Lab09.01.exe >> NUL" 문자열을 완성시킵니다.

해당 문자열의 의미는 스스로 파일을 삭제함과 동시에, 에러 출력을 버리는 의미를 가집니다.

완성된 문자열은 ShellExecuteA 함수를 통하여 CMD 명령어로 사용됩니다.

 

저는 몰랐지만, OllyDbg로 실행하고 있기 때문에 윈도우가 파일삭제를 허용하지 않는다고 하네요.

(사실 실수로 몇번 브레이크 포인트를 지나서 실행해버린 바람에 가슴철렁 했었는데...)

이후 exit 함수로 프로그램을 종료합니다.

※ sub_402410 정리 : 레지스트리 체크 + 디스크에서 악성코드 삭제

 

sub_401000 서브루틴에서 레지스트리가 존재할 경우, 프로그램이 종료되지 않고 sub_402360 서브루틴을 호출합니다.

해당 서브루틴의 내부 로직을 살펴보면, 원하는 조건이 충족될때까지 무한 루프를 수행합니다.

정확한 행위를 분석하려면 while문 내부의 sub_401280sub_402020 함수의 분석이 필요합니다.

 

sub_401280 서브루틴을 살펴보면,

sub_401000 서브루틴에서도 보았던 "\\XPS" 레지스트리를 Open하고 "Configuration" 값을 한번더 체크합니다.

계속 중복체크하는것을 보니, 분명 악성코드의 중요 행위중에 하나로 보여집니다.

만약 해당 레지스트리가 존재하지 않을 경우 함수의 인자로 받은 a1, a3, a5, a7 변수에

"Configuration" 항목에서 받아온 내용을 글자수만큼 주소값을 이동하며 복사합니다.

하지만 아직은 어떤 문자열이 해당 변수에 저장되는지는 알 수 없습니다.

※ sub_401280 정리 : 레지스트리에서 "Configuration" 항목값을 얻어와서 변수에 할당 

Tip. Hex-ray에 주석남기기

 이번 문제와 같이 함수에 함수가 꼬리를 무는 경우, 헷갈리기 시작하면서

 분석시간이 길어질 수 있습니다.

 이럴땐 단축키 "/" 를 눌러서 아래와 같이 주석을 남길 수 있습니다.

 사실 간단한 부분인데...이런 노하우가 늘어날수록 분석 시간이 조금이라도 단축되겠죠.

 

 

다시 while 문으로 돌아갑니다.

위에서 분석한 sub_401280 서브루틴으로 "Configuration" 항목값에 있는 값들을 변수에 담아 왔으니

이후에 실행되는 sub_402020 서브루틴에서는 해당 값들을 활용할 것으로 예상됩니다.

 

 

 

2. 명령어 제어 프로토콜(sub_402020) - Part1(명령어수신)

서브루틴 내부를 살펴보면, 가장 먼저 실행하는 sub_401E60 서브루틴을 비롯한 여러 문자열들이 보입니다.

"SLEEP", "UPLOAD", "DOWNLOAD", "CMD" 등의 문자열이 무엇을 의미하는지 알아볼 필요가 있습니다.

 Tip. Hex-ray 옵션 - Strings Generate names

 IDA 버전을 6.6으로 업그레이드 했습니다.

 Hex-ray 코드가 좀더 간결해 졌지만, 문자열이 아래와 같이

 변수명으로 표현되어 가독성이 떨어집니다.

 

 IDA 옵션에서 한참 삽질을 하다보니.. Hex-ray 옵션이 따로 있더군요.

 IDA 설치경로의 cfg 폴더에서 hexrays.cfg 파일로 옵션을 설정합니다.

 Analysis 옵션의 HEXOPTIONS 값에서 read-only만 literal로 출력하는

 HO_CONST_STRING 옵션 비트 조정합니다. ex) 0x01FF → 0x01BF

 

 이제 아래와 같이 Hex-ray 코드에서 문자열이 표현됩니다.

 

 

먼저  sub_401E60 서브루틴을 살펴봅니다.

저같은 경우 sub_401E60 서브루틴 내부에 있는 함수들이 단발성(Xref에서 하나의 상호 참조만 있는) 함수들이

많은데다가, 전체적인 기능 파악이 안되어 있는 상태에서 하나만 계속 파고드는 느낌이 들어서

바로 분석하기 부담을 느꼈습니다. 그래서 처음엔 pass 하고 가장 마지막에 분석하였습니다.

이건 아마 분석하시는 분들의 성향에 따라 달라질듯하네요.

 

일단 가장 먼저 실행되는 서브루틴은 sub_401420 서브루틴으로서, 내부를 살펴보면

바로 위에서 분석한 sub_401280 서브루틴의 축소판입니다. 함수 자체가 BOOL 타입이지만,

포인터를 활용하여 인자로 전달된 &name 변수에 레지스트리에 저장된 문자열을 가져옵니다.

아래쪽에서 레지스트리를 등록하는 부분이 분석되어 있지만, 미리 컨닝을 하자면 아래의 URL을 가져옵니다.

※ sub_401420 정리 : 등록된 레지스트리에서 URL 값을 가져옴, sub_401280의 축소판 

 

두번째로 실행되는 서브루틴은 sub_401470 입니다.

해당 서브루틴 역시 내부에서 sub_401280 서브루틴을 호출하고 있으며,

인자로 전달된 a1 변수에 아래와 같이 포트번호로 보이는 문자열 "50(=80)" 값을 저장합니다.

레지스트리에 등록되는 값이 문자열이기 때문에 atoi 함수를 이용하여 정수값으로 변환합니다.

※ sub_401470 정리 : 등록된 레지스트리에서 포트번호를 가져옴, susub_401D80b_401280의 축소판 

 

세번째로 실행되는 서브루틴은 sub_401D80 입니다.

눈에 띄는 부분은 time 함수와 왠지 복잡하게 보이는 여러개의 for문입니다.

먼저 time 함수값이 어떻게 사용되는지 살펴보면,

sub_4030E0 서브루틴에 전달되어 단순하게 dword_40C180 변수에 값을 할당합니다.

왜 이런 함수가 있을까 궁금해서 Xref로 검색해보니 rand 함수 내부에서 해당 변수가 사용되고 있습니다.

 

rand 함수 내부를 살펴보면 time(NULL) 값을 바탕으로 난수값을 return 하고 있습니다.

rand 함수는 srand 함수를 미리 호출하지 않으면 동일한 난수값을 발생시키므로,

time(0) + sub_4030E0 서브루틴의 조합은 srand(time(NULL)) 과 동일한 기능으로 판단됩니다.

 

for문의 경우 포인터의 주소값을 1씩(BYTE 단위) 증가시키며 sub_401D10 서브루틴의 값을 저장합니다.

중간중간 구분자로 보이는 47("/"), 46("."), 0(NULL) 값을 넣어주는군요.

 

어떤값을 채우느냐가 관건입니다. sub_401D10 서브루틴을 살펴봅시다.

do-while 문으로 조건이 충족안될때까지 실행을 반복합니다.

while( (v1 < 48 || v1 > 57) && (v1 < 97 || v1 > 122) && (v1 < 65 || v1 > 90) )

실제로 찍어보거나, 엑셀로 계산하거나, 암산을 하시거나... 확인 방법은 자유입니다.

결과는 영어대소문자 + 숫자 아스키코드 범위이며, 결과값은 아래와 같습니다.

※ sub_401D80 정리 : 특정 구분자가 포함된 문자열 반환 

 

네번째로 실행되는 서브루틴은 sub_401AF0 입니다.

초반부에서 sub_401640 서브루틴을 호출하고 있습니다. 먼저 분석하고 되돌아옵니다.

 

sub_401640 서브 루틴을 살펴보면,

Lab07-03 문제에서 나왔던 패턴이 보입니다. 윈속으로 통신을 수행하기 위해서

WSAStartup → gethostbyname → socket → connect 순서로 연결을 시도합니다.

※ sub_401640 정리 : 인자로 전달된 도메인(=name)에 winsock으로 connect 수행 

 

다시 sub_401AF0 서브루틴으로 돌아갑니다.

strcpy + stcat 함수를 이용하여 buf 변수에 "GET VuHD/2tCo.tjd HTTP/1.0\r\n\r\n" 문자열을 생성합니다.

이후 connect로 연결한 원격지에 send 함수를 이용하여 통신을 수행합니다. 

 

만약 정상적으로 서버와 연결이 되었다면,

서버로부터 변수 v6 에 512 길이만큼 데이터를 전송받습니다.

수신된 데이터는 qmemcpy 함수를 통해 a4 배열에 차곡차곡 복사합니다.

반복 루틴이 실행될때마다 strstr 함수를 이용하여 "\r\n\r\n" 문자열이 존재하면 break 합니다.

 

 Tip. qmemcpy 

 qmemcpy 함수는 msdn에 검색해도 나오지 않았습니다.

 하지만 Hex-rays Home에서 6.6 버전을 릴리즈하며 게시한 내용에

 간략한 설명이 있어서 올립니다.

 왜 앞에 "q" 문자가 붙었는지는 모르겠지만

 중요한건 memcpy와 사용방법이 동일하며, byte by byte 방식으로

 낮은 주소에서 높은 주소로 복사를 한다는 점이겠죠.

 

 

 

recv 함수는 정상적인 데이터 전송이 이루어졌다면, 수신한 바이트의 길이를 리턴합니다.

하지만 리턴값이 0 이하일 경우(데이터 못읽음, 세션종료, 오류 등) 또는

"누적된 데이터 길이 + 방금 받은 데이터 길이" 가 인자로 넣어준 변수 a5(=4096)의 크기를 초과할 경우

sub_401740 서브루틴을 호출하여 shutdown, closesocket, WSACleanup 등의 세션종료 함수를 호출합니다.

 Tip. Windows 함수에서의 ErrorCode

 위에서 등장한 recv, shudown 등의 windosck 함수들을 호출하고나서 결과값이 0 미만일 경우

 result 값을 1로 반환하여, 정상적인 진행이 되지 않았을 경우의 실행 코드가 존재합니다.

 0 미만의 값이 무엇을 의미하는지 MSDN에서 살펴보려고 했습니다만,

 아래와 같은 WSAGetLastError 함수와 함께 에러코드들의 링크가 존재하더군요.

 그런데 문제점은 모두가 하나같이 0 이상의 상수값이라서 헤멨습니다.

 (분명 0 미만일 경우의 코드들이 존재했는데, 왜 모두 0 이상이지? 라는 생각과 함께요)

 

 자세하게 설명을 읽어보지 못한 저의 불찰입니다만,

 GetLastError 또는 WSAGetLastError 함수들은 에러가 발생한 이후에

 별도로 호출하여 에러코드를 얻어내는 함수들입니다. 

 recv 함수는 정상적이라면 수신한 바이트 길이를 리턴하며, shutdown 함수는 정상적인 경우

 0(zero)을 반환합니다. 그래서 보통 0 미만의 값을 가지면 실패 또느 오류로 인지하는것이죠.

 

sub_401740 서브루틴에서도 원하는 결과를 얻지 못했다면, 마지막 else 문으로 진입합니다.

strstr 함수로 " `'`'` " 문자열의 위치를 검색하는 루틴입니다.

문자가 헷갈려서 간단하게 찍어보니 16진수값 0x27(" ' "), 0x60(" ` ") 이 나왔습니다.

교재에서는 백틱, 어포스트로피라고 되어 있는데...백틱이란 문자는 생소하네요.

만약 해당 문자열들이 둘다 검색된다면,

qmemcpy 함수를 이용하여 v5 변수가 가지고 있는 주소값의 내용을 a1 변수로 복사합니다.

그 다음줄은 마지막 라인에 0(null)값을 넣어주면서 문자열의 끝을 나타내는것으로 생각됩니다.

변수 v10 에는 sub_401AF0 서브루틴에서 GET 으로 얻어온 파일이 담겨있습니다.

서버에서 가져온 파일 또는 문서에서 특정 내용을 a1 변수에 반환하여

위에서 잠시 살펴보았던 "SLEEP", "UPLOAD" 등의 명령어일 경우 해당되는 함수들을 호출합니다.

※ sub_401E60 정리 : 등록된 레지스트리에서 항목값을 가져오거나, URL로부터 명령어 수신 

 

 

 

 

3. 명령어 제어 프로토콜(sub_402020) - Part2(명령어별 동작)

이제 명령어를 수신하였으니, 명령어별로 어떤 동작을 수행하는지 살펴봅니다.

v14 변수에 담긴 문자열과 명령어들과의 비교를 통해 일치할 경우, strotok 함수를 이용하여 문자열을 분리합니다.

strtok 함수의 특성상, 처음에만 주소값을 넣어주고 이후에는 Null 을 넣어줌으로서 지속적인 분리를 수행합니다.

이렇게 분리하는 이유는, 위쪽에서 strncmp 함수를 이용하여 문자열의 앞부분에서 특정 길이만큼만 체크한뒤

그 뒤에 "(공백)" " ' "  구분자로하여 이어지는 다른 명령을 파싱하기 위해서일겁니다.

그래서 각 명령어마다 서로 다른 sub_401790 / sub_401870 / sub_4019E0 함수 등을 호출하는것으로 보입니다.

 

① "SLEEP"

해당 명령의 경우 말그대로 Sleep 함수를 실행시킵니다.

함수 인자의 단위는 밀리초(=ms, milliseconds, 1/1000초) 이므로,

"1000 * v3" 계산에서 v3 변수가 몇초 동안 sleep 상태로 대기할 것인지를 결정합니다.

 

 "UPLOAD"

해당 명령의 경우 sub_4019E0 서브루틴을 실행합니다.

 

sub_4019E0 서브루틴의 내부 동작을 살펴보겠습니다.

CreateFileA 함수로 파일을 생성(또는 open) 하고 있으며,

실패할 경우 sub_401740 서브루틴으로 winsock 연결을 해제(shutdown, closeconnect) 합니다.

 

하지만 성공하였을 경우에는, 

방금 얻어낸 handle + recv 함수+ WriteFile 함수로 파일의 로우 데이터를 입력합니다.

여기서 의아했던 점은 분명 "UPLOAD" 인데 서버에서 데이터를 수신하고 있다는 점입니다.

뒤쪽에서 알게되었지만, "DOWNLOAD" 역시 반대로 서버에 데이터를 송신합니다.

이것으로 미루어보아, 명령어의 주체가 감염된 PC가 아닌 서버 or 악성코드 제작자임을 알 수 있습니다.

 

서버와의 연결이 성공적으로 종료되면, sub_4015B0 + sub4014E0 서브루틴을 호출하여

특정 파일 생성/복사 등을 수행합니다. 자세한 내용은 아래쪽의 "-in" 기능을 분석에 있습니다.

(내용이 길어지고 함수가 반복적으로 쓰이다보니 왔다갔다하네요..^^;)

 

 

"DOWNLOAD"

위에서 어느정도 파악을 했다싶이, 실제로는 감염된 PC에 파일이 다운로드 되어지는 루틴입니다.

 

호출되는 sub_401870 서브루틴을 살펴봅니다.

CreateFile 함수를 통해 파일을 오픈하는 과정은 들어가는 몇개의 인자값을 제외하고는 동일합니다.

다운로드의 경우 업로드와는 달리 "dwCreationDisposition" 인자값을 EXCREATE_ALWAYS OPEN_EXISTING

설정하였기 때문에 덮어쓰지 않고 기존에 존재하는 파일을 오픈합니다.

이후에 ReadFile 함수 + send 함수를 이용하여 서버 또는 악성코드 제작자에게

파일에서 읽어온 데이터(&Buffer)를 송신합니다.

 

 

 "CMD"

해당 함수는 말그대로 CMD 기능을 사용하는 명령어이며,

_popen 함수를 이용하여 cmd에서 명령어를 실행하고 있습니다.

"rb" 옵션의 경우 프로세스가 결과값을 볼 수 있음(=r) + 이진 데이터 반환(=b)을 의미합니다.

이후에 sub_401790 서브루틴을 호출합니다.

 

sub_401790 서브루틴의 경우 _popen 함수의 결과값을 인자로 받고 있으며,

fread 함수를 이용하여 결과값을 읽은 후에 send 함수로 서버에 송신하는 역할을 수행합니다.

 

 

4. main 함수 - 인자가 여러개인 경우

메인함수로 되돌아옵니다.  사실 저희는 아직 main의 6라인 밖에 분석을 못했습니다.

얼마 남지 않았음을 믿어 의심치 않으며...달려봅시다.

else문을 살펴보면, 프로그램 실행시 받은 마지막 인자를

v10 변수에 할당하고 sub_402510 서브루틴을 호출하는군요.

 

sub_402510을 살펴봅니다.

매개변수 int achar* 형으로 캐스팅한 뒤 문자열의 길이를 체크합니다.

만약 길이가 4가 아닐 경우에는 바로 result = 0 을 리턴하게 되며, 이럴경우

메인으로 돌아가서 본인 삭제 기능을 가진 서브루틴 sub_402410을 호출됩니다.

앞으로도 어떤 조건을 충족시키지 못하면 sub_402410을 호출하는 횟수가 빈번해집니다.

이는 프로그램 실행시 특정 조건을 반드시 맞춰야 하는 비밀번호와 같은 역할입니다.

문자열의 길이가 4라고 가정하고 다음으로 넘어갑니다.

 

int형 매개변수 a1을 _BYTE * 형으로 캐스팅한뒤 97(=a) 과 비교하여 일치하면 아래의 연산을 수행합니다.

 v2 = *(_BYTE *)(a1 +1) - *(_BYTE *)a1

이때 a1은 int형이므로 "[]" 연산이 불가능하며, a1이 가리키는곳의 문자열은 총 4자리임을 기억해야합니다.

*(_BYTE *)(a1+a1)이 의미하는 바는 a1에 저장되어 있는 숫자(주소값)에 +1을 더하여 값을 가져오므로 

a1이 가리키는 문자열 중에 두번째 문자를 의미합니다.

따라서 v2에는 (두번째 문자) - (첫번째 문자) 연산 결과가 할당됩니다.

v2 == 1이 되려면 자연스럽게 두번재 문자는 98(=b) 가 되며,

v3 = 99* v2 로 인해 세번째 문자는 99(=c) 입니다.

 

위와같은 로직으로 계산하게 되면, 이후에 실행되는 코드도 해석이 쉬워집니다.

변수 v3와 a1이 가리키는 문자열 중에 세번째 문자를 비교하는 코드입니다. 

 if( v3 == *(_BYTE *)(a1+2) ) 

 

만약 일치한다면, 세번째 문자와 네번째 문자가 일치하는지 확인하고, 일치할 경우 result에 저장합니다.

여기서 (char)(v3+1) 은 v3 주소값에서 +1을 하는것이 아니라,

말그대로 v3값(99)에서 +1을 해서 (char)형으로 캐스팅을 한겁니다. 즉, 네번째 문자는 100(=d) 입니다.

 result = (char)(v3+1) == *(_BYTE *)(a1 + 3) 

 

다시 main으로 돌아갑니다.

이후에 _mbscmp 함수를 이용하여 argv[1]에 있는 변수와 "-in" "-re" "-c" "-cc" 문자열을 비교합니다.

_mbscmp 은 strcmp 의 멀티바이트 문자 버전으로서, MBCS기반의 문자 비교를 수행합니다.

각 문자열 별로 비교하는 부분을 살펴봅니다.

 

① "-in"

일치 O : 인자의 개수에 따라 else if 또는 else 문 진입  sub_4025B0, sub_402600 서브루틴 실행

일치 X : IF문 내부로 진입 (_mbscmo : 문자열이 동일한 경우 0 반환)

 

"-in" "-re" "-cc" 의 공통점은 인자가 3개일때 sub_4025B0 서브루틴을 호출하는 부분입니다.

sub_4025B0를 살펴보면, 내부에서 getModuleFileNameA 함수를 호출하는것 외에는

특별한 기능을 수행하지 않습니다. _splitpath 함수는 디렉토리를 분리하는 함수로서

드라이브명, 디렉토리명, 확장자 등을 분리하여 각 변수에 담을 수 있는 함수입니다.

세번째 인자 a1 변수에 파일명이 분리되어 담길것입니다. 이후 0을 반환하고, main에서 sub_402600을 호출합니다.

sub_4025B0 정리 : 인자로 넣어준 변수의 파일명 반환

 

sub_402600를 살펴보면,

strcpy + strcat을 이용하여 "%SYSTEMROOT%\\system32\\서비스명(=파일명).exe" 문자열을 만듭니다.

사실상 여기서 서비스명은 sub_4025B0 에서 반환한 파일명입니다.

이후 OpenSCManagerA + OpenServiceA 함수를 이용해 서비스를 실행합니다.

 

만약 서비스가 제대로 Open 되었다면,

ChangeServiceConfigA 를 실행하고 서비스 핸들을 닫거나 or 그냥 닫아버립니다.

dwServiceType과 dwErrorControl 인자값이 0xFFFFFFFF 이어서 어떤 의미인지 헤메다가

MSDN의 "Changing a Service's Configuration" 예제를 발견하여 참고했습니다.

URL : https://msdn.microsoft.com/ko-kr/library/windows/desktop/ms681987(v=vs.85).aspx 

 

서비스가 정상적으로 Open 되지 않았을 경우,

서비스명 "Lab09-01", 서비스 표시 이름 "Lab09-01 Manager Service" 으로 서비스를 생성합니다.

6번째 인자를 통해서 SCManager 가 Setup 될 때 자동으로 시작되는 서비스임을 알 수 있습니다.

들어가는 인자들은 Xref로 어셈블리 코드를 추적한 다음 Symbolic Constant로 변경하면 편리합니다.

 

이후에 ExpandEnvironmentStringA 함수를 호출하여 Src변수에 문자열로 저장해 두었던

환경변수 %SYSTEMROOT%을 확장한 문자열을 얻습니다. 이러한 절차는 설치되는 PC마다 환경변수가 다르기 때문에

오류를 최소화하기 위한 작업으로 보입니다. 다음에 또 해당 코드와 마주친다면 의도파악이나 분석 속도가 빨라지겠죠.

그아래에서는 파일을 다른 경로를 복사하기 위해 GetModuleFIleName 함수와 CopyFile 함수를 호출합니다.

이후에 sub_4015B0 함수 결과에 따라 sub_401070 함수의 호출 여부가 결정됩니다.

 

sub_4015B0 서브루틴을 살펴보면,

GetSystemDirectory + strcat 함수를 이용하여 아래와 같은 문자열을 만듭니다.

이후 sub_4014E0 함수의 결과가 0이 아니면 result로 반환합니다.

 

sub_4014E0 서브루틴을 살펴보면,

인자로 넘어온 2개의 경로를 이용하여 파일을 2개 생성합니다.

① a1 : C:\Windows\system32\Lab09-01.exe (GENERIC_WRITE)

② lpFileName : C:\Windows\system32\kernel32.dll (GENERIC_READ)

CreateFile 함수의 인자값들은 OllyDbg에서 파악하기 쉽습니다.

 

여기서 염두해둘 부분은 kernel32.dll 파일은 이미 존재할 것이므로, OPEN_EXISTING 옵션에 의하여

핸들값을 가져오고 난 이후에 system32 폴더에 생성된 Lab09-01.exe의 타임설정을 kernel32.dll과 동일하게 설정하는 점입니다.

만들어진 Lab09-01.exe의 등록정보를 살펴보면 제가 캡쳐를 위해 접근한 시간인 액세스 날짜를 제외하고

나머지 일자가 동일함을 알 수 있습니다.

sub_4014E0 정리 : 함수에 넘어온 인자를 이용하여 "Lab09-01.exe" 파일 생성, "kernel32.dll" 파일과 동일하게 타임 설정

sub_4015B0 정리 : "시스템 디렉토리 경로 + kernel32.dll" 문자열 생성, sub_4014E0 호출 

 

 

다시 sub_402600으로 돌아가서, sub_401070을 살펴보겠습니다.

함수에 "ups" "http://...." "80" "60" 인자들을 넣어주고 sub_401070 서브루틴을 호출합니다.

 

아마 이부분이 가장 궁금했던 "SOFTWARE\\Microsoft \\XPS" 레지스트리에

어떤 내용이 저장되길래 자꾸 체크하는지 알 수 있는 부분입니다.

함수 호출시 전달된 인자들을 주소값 연산을 통해 &Data 변수에 주르륵 할당합니다.

 

이후 RegCreateKeyExA 함수를 이용하여 "SOFTWARE\\Microsoft \\XPS" 서브키로 레지스트리를 생성하고

RegSetValueExA 함수를 이용하여 "Configuration" 항목값을 Data 변수에 있는 내용으로 채웁니다.

 

regedit을 이용하여 생성된 레지스트리르 체크해보면,

MicroSoft 문자열 뒤에 스페이스가 추가된 서브키가 추가로 생성되어

사용자가 악성코드로 인해 생성된 레지스트리인지 쉽게 인지할 수 없게 되어있습니다.

 

이것으로 길고 길었던 "in" 옵션에 대한 분석이 끝났습니다.

이후의 옵션들은 위에서 분석한 함수들의 내용이 중첩되는 부분이 많아서 좀더 수월할것으로 보입니다.

sub_401070 정리 : 레지스트리 생성 및 등록 

"in" 커맨드라인 옵션 정리 : 시스템 디렉토리에 악성코드 복사, 서비스 생성, 레지스트리 생성 

 

 

② "-re"

일치 O : 인자가 3개일 경우 sub_4025B0, sub_402900 호출, 4개인 경우 sub_202600 호출, 그외 sub_402410(삭제+종료) 호출

일치 X : IF문 내부로 진입 (_mbscmo : 문자열이 동일한 경우 0 반환)

 

인자가 3개 혹은 4개일 경우, "in" 옵션과 동일하게 sub_4025B0(파일명반환) 서브루틴을 호출합니다.

이후 sub_402900 서브루틴을 호출합니다. 해당 함수를 분석합니다.

 

가장 먼저 눈에 들어오는 부분이 OpenSCManagerA + OpenServiceA 입니다.

해당 함수들이 눈에 익어서 어디서 호출했는지 Xref로 확인해보면 sub_402600 서브루틴입니다.

sub_402600 서브루틴에서는 새로운 서비스를 생성하는데 초점이 맞춰져 있었습니다.

 

하지만 분석중인 sub_402900 서브루틴에서는 DeleteService + DeleteFileA 함수를 통해

악성코드가 생성한 서비스와 파일을 삭제를 수행합니다. 해당 명령어는 "remove"를 의미하는것 같네요.

 

sub_402600 서브루틴의 분석이 선행되어서 그런지 "re" 옵션에 대한 분석이 빨리 끝났습니다.

"re" 커맨드라인 옵션 정리 : 서비스 삭제, 시스템 디렉토리에 복사한 악성코드 삭제 

 

 

③ "-c"

일치 O : 인자가 7개인 경우 sub_401070 호출, 아닌 경우 sub_402410(삭제+종료) 호출

일치 X : IF문 내부로 진입 (_mbscmo : 문자열이 동일한 경우 0 반환

 

해당 명령어는 인자가 7개가 아니면 바로 삭제를 수행합니다.

위에서 이미 분석한 sub_401070(레지스트리 생성 및 등록) 서브루틴을 호출합니다.

해당 함수에 인자가 모두 argv인것으로 보아 레지스트리 값만 직접 등록하고 싶을때 사용하는 명령어입니다.

아마도 최초에는 "in" 명령어를 이용하여 설치할것이고, 향후 변동사항이 있을때 사용할것으로 예상됩니다.

"c" 명령어는 "configuration" 정도가 어울릴까요?

"c" 커맨드라인 옵션 정리 : 인자값을 이용한 레지스트리 직접 생성 및 등록 

 

④ "-cc"

일치 O : 인자가 3개가 아닌경우 sub_402410(삭제+종료) 호출, 그외 sub_401280, sub_402E7E 호출

일치 X : sub_402410(삭제+종료) 호출

 

"cc" 커맨드 옵션은 간결합니다.

sub_401280 서브루틴을 호출하여 레지스트리의 데이터를 얻어옵니다.

이후 sub_402E7E 서브루틴을 호출하는것을 볼 수 있는데, 

해당 함수 내부의 _stbuf _ftbuf 함수는 printf 내부 동작에 사용되는 함수이기도 합니다.

함수 호출시 전달된 인자 "k:%s h:%s p:%s per:%s\n" 의 형태가 화면에 해당값들을 출력하기 위한

용도로 보이기 때문에 직접 실행해봅니다.

"cc" 커맨드라인 옵션 정리 : 현재 레지스트리에 등록되어 있는 Config 값 출력 

 

 

 

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

0x02 문제풀이

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

1. 어떻게 하면 악성코드가 자신을 설치하게 할 수 있는가?

  옵션 "-in" 으로 패스워드를 입력해서 프로그램 자체를 초기설치할 수 있습니다.

  OllyDbg를 이용하여 패스워드 검증 부분을 뛰어넘을수도 있습니다.

 

2. 이 프로그램의 커맨드라인 옵션은 무엇인가? 패스워드 요건은 무엇인가?

   "-in" "-re" "-c" "-cc" 4가지 옵션이 있으며, 패스워드는 "abcd" 입니다.

   각 옵션의 기능과 패스워드를 구하는 방법은 위쪽의 분석을 참고하세요.

 

3. 이 악성코드가 특수 커맨드라인 패스워드를 요구하지 않게 영구 패치하려면 OllyDbg로 어떻게 해야 하는가?

   교재에서는 0x402510 주소에서 항상 참을 반환하게 함수의 첫번째 바이트를 변경하라고 되어 있지만

   코드를 패치할 수 있는 포인트는 더 다양할 것 같습니다.

 

4. 이 악성코드의 호스트 기반 지표(indicator)는 무엇인가?

   레지스트리키 생성, "파일명 Manager Service" 생성,

   원격 명령어 프로토콜을 이용한 백도어 기능, 파일 생성/복사 등이 있습니다.

 

5. 이 악성코드가 네트워크를 통해 수행할 수 있는 다른 행동은 무엇인가?

  악성코드는 네트워크를 통해 "SLEEP", "UPLOAD", "DOWNLOAD", "CMD", "NOTHING" 다섯 명령어 중

  하나를 실행하게 명령어를 받습니다.

 

6. 이 악성코드에 대한 유용한 네트워크 기반 시그니처가 있는가?

  기본적으로 악성코드는 http://...(중략)...analysis.com URL으로 통신하지만, 변경가능합니다.

  비컨은 xxxx/xxxx.xxx 형태로 HTTP/1.0 GET 리소스 요청을 합니다.

  여기서 x는 무작위로 구성한 문자와 숫자 ASCII입니다.

 

 

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

0x03 마무리

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

이번 포스팅은 IDA 소스코드만 보고 분석하기에도 정신없을 정도로 분량이 많았습니다. 

그러다보니 값을 검증하기 위해 올린 Ollydbg 화면 외에는 어셈블리어를 거의 살펴보지 못한 것 같습니다.

모르는게 많아서 포스팅 속도가 느리네요.

부족한글 봐주셔서 감사합니다.

 

 

posted By Message.

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

 

posted by Red_Message