6 분 소요

지난 이야기

학교 도서관앱이 노후화가 심각해 입장용 QR 생성이 아주 느리고 오류가 많다.
자주 사용하는 QR생성 기능만 분리해보려고 했다.

네트워크 모니터링을 키고 앱 내에서 QR생성을 해서 QR생성하는 API주소를 찾았고 필요한 query 값들도 파악했다.

  • l_user_id: 아이디(학번)
  • l_user_pass: 비밀번호(홈페이지 비밀번호)
  • l_user_phone_uid: 사용자 휴대폰 고유번호
  • l_user_phone_type: 휴대폰 종류 (Android / iOS)

문제는 query 값들이 특정 알고리즘에 의해 암호화되어서 전송되기 때문에 사용자 입력값으로 API요청을 보내려면 이 암호 알고리즘을 내가 알아야한다.

그래서 우선 암호문 단독 공격을 시도해보기로 했다.

목적

독자의 이해를 위해 자세히 다시 설명하자면,

우선 앱에 내 계정의 아이디비밀번호가 저장되어있다. (자동로그인)

  1. 앱 내에서 QR생성버튼을 누르면
  2. 앱 내의 어떤 함수/코드(알고리즘)에 의해서 내 학번을 어떤 암호문으로 변환한 후
  3. l_user_id에 담겨 API요청이 서버로 전송된다.

구체적으로는 내 학번 202002502AiZdoAGnNMyVmibRH2zuA0JL5JLb2xz0NMy0Db2iZd0TJ2Kwr0zuHvEDbxxKwrtyHTJLbxzu&로 변경되는데,

내가 지금 하고자 하는 것은 어떤 규칙으로 이렇게 변경되는지를 알아내려고 하는 것이다.

암호문 단독 공격 (Cipertext-Only Attack)

공격자가 암호문만 가지고 있는 상황에서 공격하는 유형이다.
공격자는 암호문 이외의 어떤 정보도 가지고 있지 않기 때문에 공격자 입장에서 가장 어려운 공격 방법이다.

무차별 대입 공격 (Brute-force)

오라오라
돌격!!!!

말 그대로 무차별적으로 가능한 모든 값을 대입해 패턴을 찾아내는 것이다. (한 글자로 노가다)
암호문에 쓰인 문자의 빈도수 분석이나 문장의 특성 등을 추정하여 해독하는 방법이다.

전략

나의 경우에는 API요청에 보내는 l_user_id, l_user_pass, l_user_phone_uid, l_user_phone_type 이 네가지 변수가 암호 생성에 영향을 끼친다고 가정했다.

우선 l_user_pass에 해당하는 학교 계정 비밀번호를 제외한 나머지는 상수로 두고, 비밀번호만 계속 변경해가면서 API요청을 보내, l_user_pass(암호문) 값의 변화를 분석하는 전략을 세웠다.

1. 평문 후보셋 생성

비밀번호 변경 후보 리스트는 ChatGPT의 도움을 받아 생성했다.


ChatGPT:

표준 최소 표본대로 ‘ak2897117@’을 기준으로 비밀번호 변경 후보 리스트를 만들어줘

응답:
아래 목록을 그대로 써서 순차적으로 요청을 날리면, “표준 최소 표본” 25 개가 완성돼. (각 열은 샘플 번호 · 비밀번호 문자열)

구분 샘플 PW
◎ 베이스라인 1. ak2897117@ ← ① 첫 요청
2. ak2897117@ ← ② 같은 PW로 재요청
① 끝자리만 변주
(특수문자 10종)
3. ak2897117#
4. ak2897117$
5. ak2897117%
6. ak2897117^
7. ak2897117&
8. ak2897117*
9. ak2897117(
10. ak2897117)
11. ak2897117-
12. ak2897117_
② 위치 스캔
(왼→오 1글자씩)
13. bk2897117@  (a→b)
14. al2897117@  (k→l)
15. ak3897117@  (첫 2→3)
16. ak2997117@
17. ak2307117@
18. ak2898117@
19. ak2897217@
20. ak2897127@
③ 길이 실험 21. ak2897117@!
22. ak2897117@!!
23. !ak2897117@
④ 랜덤 컨트롤
(비트 충돌/중복)
24. ak2897117@# ← ①·③ 복합
25. ak2897117@ ← ③ 이후 다시 원본 (토큰 재현 여부 확인용)

사용 방법

  1. 각 비밀번호를 앱(또는 캡처 스크립트)로 로그인·로그아웃 1회씩 호출.
  2. l_user_pass=… 토큰을 모두 저장.
  3. 나에게 “PW–토큰” 쌍을 번호별로 던져주면, diff 패턴을 분석해 암호 구조를 추론해줄게.

(ChatGPT 응답 종료)


2. 실행 (요청 전송해 후보셋에 대응하는 암호문 수집)

이짓거리를 위해서 학교 계정 비밀번호를 1시간동안 26번이나 변경했다.

차단 안 당한거 보니 학교 계정 보안시스템이 따로 없나보다.

26가지의 비밀번호로 전송한 API 전송 26개를 각각의 txt파일로 정리했다.

응답 txt 파일 예시:

GET https://lib.kangnam.ac.kr/Account/RequestClickerSmartCardInformation?l_user_id=AiZdoAGnNMyVmibRH2zuA0JL5JLb2xz0NMy0Db2iZd0TJ2Kwr0zuHvEDbxxKwrtyHTJLbxzu&l_user_pass=AiZdoAGnNMyVmibRH%40xzu7iZ1HTJ1bx7bxz2rt8xzu9uAnAiZaxz%21oAGHvEDbxxKwrtyHTJLbxzu&l_user_phone_uid=AiZdoAGnNMyVmibRH8Lbx2Lb6rty0nNCLbxFtyBrtyBMy0vEDDHT9RHK3Lb-xzuCyH6AGnCJLAHTJ-yH2MyV8xxEvED4NM-JLb9xz4HKwALbBmib-Gn9bxz4Kw9bRHCHK4Vmi1ty2RHKERHHvEDbxxKwrtyHTJLbxzu&l_user_phone_type=AiZdoAGnNMyVmibRHeGnNnNMoxKwhyHpdoAiTJHvEDbxxKwrtyHTJLbxzu HTTP/2.0
user-agent: NewClicker/20210614 (iPhone; iOS 18.4; Scale/3.00)
accept: */*
accept-language: ko-KR;q=1, en-KR;q=0.9, vi-KR;q=0.8, ja-KR;q=0.7
accept-encoding: gzip, deflate
content-length: 0

이것으로 GPT가 요청한 표준 최소 표본을 확보했다! 이제 이 모든 파일을 ChatGPT에게 넘겨주고 변이 위치 분석암호 방식 추론을 요청했다.

lethimcook!
(LET HIM COOK!!)

ChatGPT:

이제 네가 요청한 표준 최소 표본을 모두 확보했어. 이제 변이 위치 분석과 암호 방식 추론을 진행해줘.

응답:

1 단계 ― 토큰 구조 “눈으로” 살펴보기
관찰 근거(예시) 의미
고정 머리말 AiZdoAGnNMyVmibRH 모든 26 개의 토큰이 동일한 17-byte 스트링으로 시작 서버가 미리 심어 둔 정적 식별자(매직 바이트). 암호 알고리즘과는 무관하지만 토큰 판별용 시그니처로 보인다.
토큰 길이 = 74 byte len(l_user_pass) 전부 74 (17 byte 머리말) + ( 57 byte 블록 ) → 57 byte는 48 byte(암호문) + 9 byte(랜덤/메타) 같이 ‘블록 암호 + IV’ 구성을 떠올리게 한다.
첫 3 byte가 URL 인코딩된 특수문자 @ → %40, # → %23, $ → %24, … ) → %29 토큰 안에 비밀번호의 “첫 특수문자”가 그대로 삽입된다. (URL 인코딩으로만 감싼 채)
나머지 54 byte가 매번 달라짐 같은 PW ① ak2897117@ 토큰들 비교 → 세 번 모두 완전히 다름 토큰 생성 시마다 난수가 들어간다. → 순수 해시나 단순 치환이 아님.

결국 “토큰 = 《고정헤더》 + 《PW 메타(3 byte)》 + 《난수 + 암호문 54 byte》”라는 3-층 레이아웃이 드러난다.

2 단계 ― 난수성 검증
비밀번호요청 ① (test)요청 ② (textre)
ak2897117@%40EDb7Gn5…%40AiZ7xx5…
  • 머리말과 %40만 같고 이후 51 byte 전부 불일치
  • => 토큰에는 무작위 IV / Nonce 가 들어가며, 동일 PW라도 토큰이 바뀜 → 단방향성 보장, 리플레이 방지.
3 단계 ― 바이트 정렬·패딩 패턴
  • 17 byte(헤더) + 57 byte = 74 → 57 ≡ 0 (mod 16) 이 아니다. 그러나 48 (= 16 × 3) + 9 byte(메타) 라고 보면 AES-128 CBC/CTR + (9 byte 난수) 설계와 딱 맞는다.
  • “첫 특수문자”만 평문으로 남겨둔 것은 클라이언트 쪽 비밀번호 규칙 검증용(특수문자 포함 여부 체크) 패턴으로 널리 쓰인다.
4 단계 ― 암호 방식 추론
단서 풀이
랜덤성이 있고 길이가 고정 블록 암호 + IV / Nonce 사용
특정 바이트만 평문으로 노출 규칙 검증·호환성 목적의 ‘Partial Plain’ 삽입
URL 인코딩 문자(%,0-9,A-F) 이외에 Base64 문자 집합과 유사 실제 바이트열을 Base64 → URL-safe 치환해 쿼리에 넣은 형태
헤더가 128 bit 정적 AES Key-ID 또는 버전 태그 가능성

따라서 가장 가능성 높은 설계

  1. AES-128 CBC (또는 CTR) 로 ‘암호화 영역’ 을 만들고,
  2. 선두에 9 byte 정도의 Nonce/IV + PW 첫 특수문자 를 붙여,
  3. 전부 Base64 → URL Safe(%, a-z, A-Z, 0-9, –, _) 로 바꾼 뒤
  4. 고정 헤더를 접두어로 달아 l_user_pass 에 삽입.
5 단계 ― 실용적 결론
  • 역산(복호화)은 사실상 불가

    • 난수(IV) + 대칭키가 서버에만 있고, 토큰마다 값이 달라서 rainbow-table도 막힘.
  • 패턴 채집을 더 늘려도 의미 없는 구간 전체 바이트 중 고정적으로 평문 노출되는 곳은 이미 다 파악했으므로, 추가 표본은 알고리즘 파악에는 기여하지 않는다.
  • 토큰 위·변조 역시 어려움 마지막 48 byte 암호문은 CBC MAC 역할도 겸하므로, 임의 수정 시 서버에서 ‘패딩 에러’ 혹은 ‘MAC 불일치’ 로 거절될 가능성이 높다.
다음 단계 제안
  1. 해당 앱 / 웹의 자바스크립트 or APK 디컴파일CryptoJS.AES.encrypt(…) 같은 호출 확인.
  2. 패킷 중 l_nonce, l_salt 류 파라미터 존재 여부를 추가 캡처로 확인하여 IV 길이를 확정.
  3. 필요 시 서버 측 공격면(Weak Key 관리, 인젝션 등) 을 별도로 검사 — 토큰 자체를 깨는 것보다 현실적이다.

요약하면: “고정 헤더 + 특수문자 평문 + (난수 ║ AES-CBC 암호문)” 구조이므로, 토큰 내부를 열어보려 하기보다는 클라이언트 코드나 키 유출 을 노리는 편이 훨씬 빠를 것이다.

(ChatGPT 응답종료.)


요약하자면

패턴파악이 불가능하다.

《고정헤더》 + 《PW 메타(3 byte)》 + 《난수 + 암호문 54 byte》

이런식으로 앞에 고정된 어떤 값이 붙고, 그 뒤에 비밀번호의 특수문자값이 그대로 들어가고, 그 뒤는 모르겠다. 이거다.

특수문자값이 그대로 들어간다고 특수문자만 빼내서 포함한다고 가정할 수도 없는게, 내 비밀번호에 특수문자가 하나밖에 없어서 그런 것일 수도 있다.

따라서 몇 시간동안의 삽질 끝에 얻은 것은 아무것도 없다.

다만 GPT의 마지막 말이 새로운 가능성을 제시해줬다..

‘해당 앱 / 웹의 자바스크립트 or APK 디컴파일’

thinkkk

APK 디컴파일..

디컴파일? 들어만 봤지 해본 적은 없었다.

디컴파일은 컴파일된 실행 파일(바이너리)을 원본 소스 코드로 되돌리는 과정입니다. 이는 컴파일과 반대되는 작업으로, 사람이 읽을 수 있는 고수준 언어 코드를 복원하며, 리버스 엔지니어링이라고도 불립니다. 완벽하게 원래 코드를 복원하기는 어려울 수 있지만, 소스 코드의 구조와 로직을 이해하는 데 사용됩니다

오! 리버스 엔지니어링은 들어봤다.

리버스 엔지니어링

간단히 설명하자면 실행가능한 앱을 분석해서 거꾸로 안에 들어있는 코드를 알아내는 것이다.

앱을 개발하는 순서는

  1. 코드로 프로그램 작성
  2. 컴파일로 실행파일화
  3. 사람들이 사용가능하게 앱 배포

크게 이렇게 진행되는데, 이 순서를 반대로해서 비법소스를 알아내는 것이다.

뭐랄까.. 유명 셰프의 음식을 먹어보고, 아 무슨 젓갈 썻네.. 간장 어디꺼 썼네.. 파스타 어디 회사꺼구만…

이런식으로 레시피를 알아내는 것, 혹은

기타 연주를 들으면서 아 퍼즈 썻네.. 리버브 스프링으로 되게 많이 걸었네… 하이 좀 깎은 거 같은데… 넥 픽업 쓰는 듯..

하며 톤 카피를 하는 것이라고 할 수 있다.

내가 할 수 있을까..?

리버스 엔지니어링이라는 것이 있다고 들어만 봤지 실제로 해본 적은 없어서 아예 문외한인 내가 할 수 있을까 고민이 되었…었지만

간단한 조사를 해봤는데 라이브러리를 이용해 명령어 몇줄로만 원본 JAVA 소스코드를 뽑아낼 수 있는 방법을 알게 되었다!

thinkBro
이젠 방법이 없다.. 리버스 엔지니어링은 감행하기로 했다.

왠지 모를 배덕감이 들고 있다.. 학교 앱을 리버스 엔지니어링 해도 될까?

다음 글에 이어서..