열받는 공인인증서 비밀번호 자동 입력기를 만들어보자

Alpha, Orderly·2024년 10월 11일
0

뻘짓거리

목록 보기
1/4


도대체 아직도 공인인증서를 왜 쓰는걸까

공인 인증서 자체는 2020년 12월경 사라졌지만, 정말 끈질기게도 은행들은 계속 쓰더라..

하루에도 수십번 공인인증서 로그인을 해야 하는 사람들이라면 로그인 절차는 정말 고역일 것이다.

한번 자동화 해보자!


어떻게 할건데?

  • 처음엔 하드웨어 기반 매크로로 동작하는 키보드를 몇개 찾아봤다,
    생각보다 소프트웨어 없이 문장을 길게 입력할만한 제품이 많이 안보여서 패스..
  • 잘 하면 아두이노 레오나르도로 되지 않을까? 하는 생각이 번뜩 들었다!
    근데 이때까지만 해도 보안프로그램에서 잡아내지 않을까 하는 걱정이 들긴 했다.
    • 근데 사실 한국의 보안 프로그램? 들은 사실 보안이 없다고 봐도 괜찮기에 괜찮을지도?

그래서 뭘로 만들었는데?

아두이노 레오나르도

  • USB 지원 관점에서, Arduino Uno는 별도의 USB-Serial 변환기를 사용하지만, Leonardo는 마이크로컨트롤러 자체에서 USB 기능을 지원해 키보드나 마우스처럼 인식된다!

  • 고로 이때 계획한 것은 다음과 같았다.

  1. 아두이노 레오나르도의 HID 입력장치를 로지텍 K120으로 속이자!
  2. 진짜 사람이 타이핑 하는것처럼 보안 프로그램을 속이자!

그래서 어떻게 관리할건데?

  • 이게 제일 난관이였다, 비밀번호 바꿀때 마다 새로 컴파일하고 업로드 할수는 없잖아!
  • 다행히도 아두이노에는 EEPROM이라고 저장공간이 있다! 여기에 한번 넣어보기로 했다.
  • 1024kb의 용량을 가지고 있기 때문에 32글자의 비밀번호 16개를 저장시 512바이트, 즉 절반을 사용해 저장할수 있다.
  • 이를 시리얼 통신을 이용해 변경하면 끝!

4x4 버튼

  • 언제 샀는지 모르지만 일단 엄청 많은 아두이노 부품상자에 3개나 있더라
    아마 까먹고 여러번 산듯 ㅋㅋ
  • 코드에서 후술하겠지만, 이 버튼 덕분에 이 프로젝트의 구현 난이도가 꽤나 낮아졌다.
    한번 눌렀을때 한번만 실행되는게 이번 프로젝트의 고비가 될것 같았는데 다행이였다.

코드

  • 코드는 일단 먼저 전부 적어놓고 하나하나씩 뜯어보자
#include <EEPROM.h>
#include <Keypad.h>
#include <Keyboard.h>

const byte ROWS = 4;    // 경(rows) 개수
const byte COLS = 4;    // 열(columns) 개수

int d = 70;

char keys[ROWS][COLS] = {
  {'G','F','E','D'},
  {'C','B','A','9'},
  {'8','7','6','5'},
  {'4','3','2','1'}
};

byte rowPins[ROWS] = {6, 7, 8, 9};   // R1, R2, R3, R4 단자가 연결된 아두이노 핀 번호
byte colPins[COLS] = {5, 4, 3, 2};   // C1, C2, C3, C4 단자가 연결된 아두이노 핀 번호

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

// 비밀번호 저장 공간
const int passwordLength = 32;
char passwords[16][passwordLength];

// EEPROM 주소 지정
int eepromAddresses[16] = {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 480};

void loadPasswordsFromEEPROM();
void savePasswordToEEPROM(int index, const char* password);
int getPasswordIndex(char key);
void processCommand(String command);
void typeWithPressRelease(String str, int delayTime = 150);

void setup() {
  Serial.begin(9600);
  Keyboard.begin();
  loadPasswordsFromEEPROM();
}

void loop() {
  if (Serial.available()) {
    String command = Serial.readStringUntil('\n');
    processCommand(command);
  }

  char key = keypad.getKey();
  if (key) {
    Serial.println(key);
    int index = getPasswordIndex(key);
    if (index != -1) {
      typeWithPressRelease(passwords[index], d);
    }
  }
}

// 비밀번호를 EEPROM에서 불러오기
void loadPasswordsFromEEPROM() {
  for (int i = 0; i < 16; i++) {
    for (int j = 0; j < passwordLength; j++) {
      passwords[i][j] = EEPROM.read(eepromAddresses[i] + j);
    }
  }
}

// 비밀번호를 EEPROM에 저장하기
void savePasswordToEEPROM(int index, const char* password) {
  for (int i = 0; i < passwordLength; i++) {
    EEPROM.write(eepromAddresses[index] + i, password[i]);
  }
}

// 키팩 입력에 따른 비밀번호 인덱스 반환
int getPasswordIndex(char key) {
  if (key >= '1' && key <= '9') {
    return key - '1';
  } else if (key >= 'A' && key <= 'G') {
    return key - 'A' + 9;
  }
  return -1;
}

// 시리얼 통신을 통한 명령 처리
void processCommand(String command) {
  if (command.startsWith("SET")) {
    int index = command.substring(4, 5).toInt();
    String newPassword = command.substring(6);
    newPassword.toCharArray(passwords[index], passwordLength);
    savePasswordToEEPROM(index, passwords[index]);
    Serial.println("PASSWORD SET");
  } else if (command.startsWith("GET")) {
    int index = command.substring(4, 5).toInt();
    Serial.println(passwords[index]);
  } else if (command == "RESET") {
    for (int i = 0; i < 16; i++) {
      const char* defaultPassword = "default";
      savePasswordToEEPROM(i, defaultPassword);
    }
    Serial.println("PASSWORD RESET");
  } else if (command == "PING") {
    Serial.println("PONG");
  }
}

// 키보드 입력 함수
void typeWithPressRelease(String str, int delayTime) {
  for (int i = 0; i < str.length(); i++) {
    Keyboard.press(str[i]);    // 키 누르기
    delay(delayTime / 2);      // 약간의 대기 
    Keyboard.release(str[i]);  // 키 떼기
    delay(delayTime);          // 약간 더 대기
  }
}

키패드 등록

const byte ROWS = 4;    // 경(rows) 개수
const byte COLS = 4;    // 열(columns) 개수

char keys[ROWS][COLS] = {
  {'G','F','E','D'},
  {'C','B','A','9'},
  {'8','7','6','5'},
  {'4','3','2','1'}
};

byte rowPins[ROWS] = {6, 7, 8, 9};   // R1, R2, R3, R4 단자가 연결된 아두이노 핀 번호
byte colPins[COLS] = {5, 4, 3, 2};   // C1, C2, C3, C4 단자가 연결된 아두이노 핀 번호

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
  • 키패드가 4x4 구조이기 때문에, 하나하나의 키에 char 변수를 등록해준다.
  • 이후 입력된 키는 keypad.getKey() 를 통해 가져올수 있다!
  • 핀의 위치는 아래 참조!
    바로가기

EEP 주소에 비밀번호 저장하기

int eepromAddresses[16] = {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 480};
  • 아두이노에서 EEPROM에 저장, 읽기 위해선 바이트별로 값을 저장해주어야 한다!
  • 그러므로, 1~16번 비밀번호를 저장할 시작 주소를 저장해둔다.
// 비밀번호 저장 공간
const int passwordLength = 32;
char passwords[16][passwordLength];

// 비밀번호를 EEPROM에서 불러오기
void loadPasswordsFromEEPROM() {
  for (int i = 0; i < 16; i++) {
    for (int j = 0; j < passwordLength; j++) {
      passwords[i][j] = EEPROM.read(eepromAddresses[i] + j);
    }
  }
}

// 비밀번호를 EEPROM에 저장하기
void savePasswordToEEPROM(int index, const char* password) {
  for (int i = 0; i < passwordLength; i++) {
    EEPROM.write(eepromAddresses[index] + i, password[i]);
  }
}

// 키팩 입력에 따른 비밀번호 인덱스 반환
int getPasswordIndex(char key) {
  if (key >= '1' && key <= '9') {
    return key - '1';
  } else if (key >= 'A' && key <= 'G') {
    return key - 'A' + 9;
  }
  return -1;
}
  • 나는 버튼에 1~9, A~G 의 문자를 등록해 두었기에, 이를 인덱스로 변환하는 절차가 필요했다
  • 이 후 저장할때는 위 주소를 참조해 저장하고, 읽어온건 전역변수에 불러오는 식으로 구현했다!
  • 또한 setup 과정에서 비밀번호를 바로 불러왔다.

시리얼 통신 구현

void processCommand(String command) {
  if (command.startsWith("SET")) {
    int index = command.substring(4, 5).toInt();
    String newPassword = command.substring(6);
    newPassword.toCharArray(passwords[index], passwordLength);
    savePasswordToEEPROM(index, passwords[index]);
    Serial.println("PASSWORD SET");
  } else if (command.startsWith("GET")) {
    int index = command.substring(4, 5).toInt();
    Serial.println(passwords[index]);
  } else if (command == "RESET") {
    for (int i = 0; i < 16; i++) {
      const char* defaultPassword = "default";
      savePasswordToEEPROM(i, defaultPassword);
    }
    Serial.println("PASSWORD RESET");
  }
}
  • 명령어 SET, GET, RESET으로 구성되어 있다.
  • SET은 특정 번호의 비밀번호 설정
  • GET은 확인하기 위해 비밀번호 가져오기
  • RESET은 모든 비밀번호를 default로 바꿔 버린다!
  • C++으로 이런거 구현하는건 거의 처음이라 엄청 고생했다..
    • 명령어를 가져올때 고정된 위치에 원하는 값이 위치하도록 하는것이 중요했다!

키보드 입력

  • 이 부분이 제일 중요했다, 원래는 키보드 라이브러리의 write 기능을 써서 한번에 입력되도록 했는데, 세상에 너무 빨라서 보안 프로그램이 알아채 버렸다 ㅠ
// 키보드 입력 함수
void typeWithPressRelease(String str, int delayTime) {
  for (int i = 0; i < str.length(); i++) {
    Keyboard.press(str[i]);    // 키 누르기
    delay(delayTime / 2);      // 약간의 대기 
    Keyboard.release(str[i]);  // 키 떠기
    delay(delayTime);          // 약간 더 대기
  }
}
  • 그래서 이런식으로 사람이 실제 키보드를 누르는것을 흉내내도록 했다.
  • 키를 누르고, 다시 떼는 과정을 하도록 하니 보안 프로그램이 진짜 사람인줄 아는듯 했다 ㅋㅋ

키보드 흉내내기

  • 이게 사실 제일 쉬웠는데, 아두이노 IDE 폴더의 boards.txt를 열어 VID, PID 등을 수정해주면 된다!
VID : 벤더 ID, 장치의 제조사
PID : 장치 ID, 장치별 식별값
  • /Users/{사용자이름}/Library/Arduino15/packages/arduino/hardware/avr/1.8.6
    여기에 들어가면 boards.txt 파일이 있는데 이걸 열고
leonardo.build.vid=0x046D
leonardo.build.pid=0xC31C
leonardo.build.usb_product="Logitech K120"
  • 이 세게를 이렇게 바꿔주자! 이제 이녀석은 아두이노가 아니라 로지텍 K120이 되었다!

설정 프로그램

  • 그러면 이걸 설정해줄 프로그램이 또 필요하다!
  • 이건 PyQt와 Serial-tool 라이브러리를 통해 간단하게 구현해보았다.
import sys
import serial
import serial.tools.list_ports
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QComboBox, QTextEdit, QSpinBox

class PasswordManager(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.serial_connection = None

    def initUI(self):
        # 메인 레이아웃
        layout = QVBoxLayout()

        # 시리얼 포트 선택 섹션
        port_layout = QHBoxLayout()
        self.port_label = QLabel('시리얼 포트 선택:')
        self.port_combo = QComboBox()
        self.port_refresh_btn = QPushButton('포트 새로고침')
        self.port_refresh_btn.clicked.connect(self.refresh_ports)
        port_layout.addWidget(self.port_label)
        port_layout.addWidget(self.port_combo)
        port_layout.addWidget(self.port_refresh_btn)

        # 연결 버튼
        self.connect_btn = QPushButton('연결')
        self.connect_btn.clicked.connect(self.connect_serial)
        port_layout.addWidget(self.connect_btn)

        layout.addLayout(port_layout)

        # 비밀번호 설정 섹션
        password_layout = QHBoxLayout()
        self.password_input = QLineEdit(self)
        self.password_input.setPlaceholderText("새 비밀번호 입력")
        self.password_index_spinbox = QSpinBox(self)
        self.password_index_spinbox.setRange(1, 16)
        self.password_set_btn = QPushButton("비밀번호 설정")
        self.password_set_btn.clicked.connect(self.set_password)

        password_layout.addWidget(QLabel("비밀번호 인덱스:"))
        password_layout.addWidget(self.password_index_spinbox)
        password_layout.addWidget(self.password_input)
        password_layout.addWidget(self.password_set_btn)

        layout.addLayout(password_layout)

        # 비밀번호 불러오기 버튼
        self.password_get_btn = QPushButton("비밀번호 불러오기")
        self.password_get_btn.clicked.connect(self.get_password)

        layout.addWidget(self.password_get_btn)

        # 비밀번호 초기화 버튼
        self.password_reset_btn = QPushButton("비밀번호 초기화")
        self.password_reset_btn.clicked.connect(self.reset_passwords)

        layout.addWidget(self.password_reset_btn)

        # 상태 메시지 출력 창
        self.status_output = QTextEdit(self)
        self.status_output.setReadOnly(True)

        layout.addWidget(self.status_output)

        # 메인 레이아웃 설정
        self.setLayout(layout)
        self.setWindowTitle('아두이노 비밀번호 관리자')
        self.refresh_ports()

    # 시리얼 포트 검색
    def refresh_ports(self):
        self.port_combo.clear()
        ports = serial.tools.list_ports.comports()
        for port in ports:
            self.port_combo.addItem(port.device)

    # 시리얼 포트 연결
    def connect_serial(self):
        selected_port = self.port_combo.currentText()
        try:
            self.serial_connection = serial.Serial(selected_port, 9600, timeout=1)
            self.status_output.append(f"{selected_port}에 연결되었습니다.")
        except Exception as e:
            self.status_output.append(f"연결 실패: {str(e)}")

    # 비밀번호 설정
    def set_password(self):
        if self.serial_connection:
            new_password = self.password_input.text()
            index = self.password_index_spinbox.value() - 1
            if len(new_password) > 0:
                command = f"SET {index} {new_password}\n"
                self.serial_connection.write(command.encode())
                self.status_output.append(f"인덱스 {index + 1}에 비밀번호가 설정되었습니다.")
            else:
                self.status_output.append("비밀번호를 입력해주세요.")
        else:
            self.status_output.append("시리얼 연결이 없습니다.")

    # 비밀번호 불러오기
    def get_password(self):
        if self.serial_connection:
            index = self.password_index_spinbox.value() - 1
            command = f"GET {index}\n"
            self.serial_connection.write(command.encode())
            response = self.serial_connection.readline().decode().strip()
            self.status_output.append(f"인덱스 {index + 1}의 비밀번호: {response}")
        else:
            self.status_output.append("시리얼 연결이 없습니다.")

    # 비밀번호 초기화
    def reset_passwords(self):
        if self.serial_connection:
            command = "RESET\n"
            self.serial_connection.write(command.encode())
            self.status_output.append("모든 비밀번호가 초기화되었습니다.")
        else:
            self.status_output.append("시리얼 연결이 없습니다.")

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = PasswordManager()
    ex.show()
    sys.exit(app.exec_())
  • 아두이노에서 설정했던 시리얼을 통해 통신하게 된다!

코딩 후기

  • 너무 잘된다.. 이상하리만치 너무 잘됨
  • 보안 프로그램은 진짜 허구에 가까운것이라는걸 엄청 느꼈다,
    그곳은 도대체 어떤곳일까?

하드웨어 제작

아쉽게도 회사에 두고왔기에 여기서 이만..

profile
만능 컴덕후 겸 번지 팬

0개의 댓글