안녕하세요 오늘은 notepad.exe 후킹 코드를 작성해보도록 하겠습니다.
먼저 저희가 후킹하려는 목표 함수는 DispatchMessageW이고 DispatchMessageW안의 message가 WM_CHAR일 때의 wParam인자값을 후킹해야 합니다.
후킹하는 방법에 제가 공부했던게 세가지 있는데 Frida, IAT후킹, inline 후킹입니다.
이런 후킹 코드들은 지피티에게 물어봐도 알려주지 않기 때문에 제가 쓴 글들을 보면서 하나하나 천천히 해봐야되겠네요.
먼저 wParam을 후킹하기 위한 Frida 스크립트를 작성해왔습니다.
function hookfunc (name){
const addr = Module.getExportByName("user32.dll", name);
console.log("[*] Found", name, "addr is", addr)
Interceptor.attach(addr,{
onEnter(args){
const lpMsg = args[0];
if (lpMsg.isNull()) return;
const msg = lpMsg.add(0x04).readU32();
const wParam = lpMsg.add(0x08).readU32();
if (msg === 0x0102) {
console.log(`[${name}] msg = 0x${msg.toString(16)} wParam=${wParam}`);
}
}
});
}
hookfunc("DispatchMessageW");
notepad후킹을 연습하면서 처음으로 결실을 맺은 느낌이라 뿌듯하네요.
천천히 헷갈리는 포인트들을 짚어보겠습니다.
먼저 DispatchMessageW의 함수 원형에 대해 살펴보겠습니다.

const MSG 형식의 포인터를 가르키고 있습니다.

실제로 API 모니터로 볼때도 주소값 하나만 가지고 있습니다.
이 MSG형식의 포인터를 따라가보면 MSG형식의 구조체가 나오는데

구조체의 구성은 다음과 같습니다.
저희가 필요한 건 message값과 wParam값입니다.
여기서 중요한 포인트는 DispatchMessage가 가르키는 값이 주소라는 점과 그 주소에서 0x04 0x08만큼 더하면 message값과 wParam값이 나온다는 점입니다. (HWND와 UINT는 32비트 환경에서는 4바이트 고정이기 때문에)
그렇기 때문에 다음과 같이 message와 wParam을 읽어올 수 있었습니다.
const msg = lpMsg.add(0x04).readU32();
const wParam = lpMsg.add(0x08).readU32();
if (msg === 0x0102) {
console.log(`[${name}] msg = 0x${msg.toString(16)} wParam=${wParam}`);
}
또 msg안의 정수값에 따라 msg형식이 달라지는데 0x102일때 WM_CHAR을 담고있습니다.
그래서 if문을 사용해서 후킹을 진행해주었고 저장한 뒤 실행해보겠습니다./
function hookfunc (name){ //후킹함수 hook func
const addr = Module.getExportByName("user32.dll", name); //user32.dll안의 DispatchMessageW 후킹
console.log("[*] Found", name, "addr is", addr) //disaptchMessageW의 주소 출력
Interceptor.attach(addr,{
onEnter(args){ //실행되기전에
const lpMsg = args[0]; //lpMsg는 Dispatch의 첫번째 인자(Msg 포인터)(구조체 형식)
if (lpMsg.isNull()) return;//Null일시 종료
const msg = lpMsg.add(0x04).readU32(); //MSG 구조체에서 message는 상대주소 +0x04
const wParam = lpMsg.add(0x08).readU32(); //wParam은 상대주소 +0x08
if (msg === 0x0102) {//msg가 102이면 WM_CHAR
console.log(`[${name}] msg = 0x${msg.toString(16)} wParam=${wParam}`);
}
}
});
}
hookfunc("DispatchMessageW");
(*주석을 다 붙인 코드입니다)

위의 명령어로 코드를 실행하겠습니다.

성공적으로 붙었고 메모장에 입력해보겠습니다.

ascii 코드값이 정상적으로 출력되고 있는 모습입니다.

한글도 이렇게 출력이 되고요, 이제 이걸 다른 노트패드에 저장하는 코드를 짜면 될 것 같습니다.
파이썬으로 만들어서 해당 js코드를 실행하면서 파일을 열고 저장하는 파이썬 코드를 작성하겠습니다.
import frida
import pathlib
PID = 16768 # 대상 프로세스 PID 넣기
SCRIPT_PATH = "hooknotepad.js"
OUT = pathlib.Path("wparam_log.txt")
def on_message(message, data):
if message.get("type") == "send":
wparam = message.get("payload")
with OUT.open("a", encoding="utf-8") as f:
f.write(str(wparam) + " ")
elif message.get("type") == "error":
print("[error]", message.get("stack"))
def main():
session = frida.get_local_device().attach(PID)
script_code = pathlib.Path(SCRIPT_PATH).read_text(encoding="utf-8")
script = session.create_script(script_code)
script.on("message", on_message)
script.load()
input("Press Enter to quit .. \n")
if __name__ == "__main__":
main()
일단 1차로 스크립트를 작성했습니다
주석까지 다 넣었으니 편하게 보시면 될 것 같습니다.
pathlib을 처음 써보는데 좀 어렵네요 .. 프리다든 후킹이든 한번 해봤어도 꾸준히 안하면 명령어나 이런게 기억에 하나도 안남는 것 같습니다 ... 제가 쓴 글이라도 자주 봐야겠네요.
중요하다 생각했던 부분이 js에서 send로 넘길때 데이터가 딕셔너리와 비슷한 형태로

실행을 했고 메모장에 작성해보겠습니다.

이렇게 작성을 했고 wparam_log.txt가 생성되었습니다.

들어가보면 아스키코드값으로 들어가있네요.

이제 ascii 코드를 문자로 변환하는 코드만 추가하면 완성될 것 같습니다.
생각해보니까
여기 str만 char형식을 바꾸면 될 것 같았는데 한글도 깨지고 좀 여러가지 문제가 있어서 수정을 해봐야 될 것 같습니다. .
처음엔 잘 되다가 몇가지 수정하고 나니까 다시 처음코드로 돌려도 제대로 출력이 안되네요..
이상한 한자가 계속 출력되서 찾아보니까 UTF 호환이 안되서 그렇다고 합니다.
인코딩을 UTF-8로 지정해놨는데 wparam_log.txt가 두번째 실행할 때 UTF-16으로 읽어들여서 그런것 같습니다.
파이썬 파일에 BOM(UTF-8 서명)을 넣으면 해결된다해서 넣어줬습니다.
f.write부분도 char형식으로 바꿔주었습니다.

이렇게 자동으로 wparam_log에 문자가 저장이 됩니다.
중간중간 깨지는건 백스페이스같은 문자 입력할 때 깨지는데, 이건 나중에 예외처리를 한번 더 해줘야 될 것 같습니다.
더 깔끔하게 하려면 js에서 send를 할 때 한글자씩 보내는 것보다 한 문장씩 버퍼에 담아서 보내는게 나을 것 같네요.
전체 코드 올려드리겠습니다.
injector.py입니다.
//injector.py
import frida
import pathlib #경로를 다루는 라이브러리
PID = 19028 # 대상 프로세스 PID 넣기
SCRIPT_PATH = "hooknotepad.js"
OUT = pathlib.Path("wparam_log.txt") #로그를 기록할(후킹) txt이름
if not OUT.exists():
OUT.write_text("", encoding="utf-8-sig") # BOM 붙여서 생성
def on_message(message, data): #JS에서 보내는 로그를 받음
if message.get("type") == "send": #type이 send일 때
wparam = message.get("payload") #wparam에 저장(payload는 Frida고정변수)
with OUT.open("a", encoding="utf-8") as f: #로그파일에다 utf-8형식으로 저장
f.write(chr(wparam))
elif message.get("type") == "error": #에러발생시 에러 출력
print("[error]", message.get("stack"))
def main():
session = frida.attach(PID)
script_code = pathlib.Path(SCRIPT_PATH).read_text(encoding="utf-8") #js파일을 문자열로 가져옴
script = session.create_script(script_code) #가져온 문자열을 스크립트 객체로 생성
script.on("message", on_message) #JS 스크립트 메세지를 on_message로 처리
script.load() #JS스크립트를 프로세스 안에 주입
input("Press Enter to quit .. \n")
if __name__ == "__main__": #파일을 직접 실행했을때만 코드 실행
main()
hooknotepad.js입니다.
function hookfunc (name){ //후킹함수 hook func
const addr = Module.getExportByName("user32.dll", name); //user32.dll안의 DispatchMessageW 후킹
console.log("[*] Found", name, "addr is", addr) //disaptchMessageW의 주소 출력
Interceptor.attach(addr,{
onEnter(args){ //실행되기전에
const lpMsg = args[0]; //lpMsg는 Dispatch의 첫번째 인자(Msg 포인터)(구조체 형식)
if (lpMsg.isNull()) return;//Null일시 종료
const msg = lpMsg.add(0x04).readU32(); //MSG 구조체에서 message는 상대주소 +0x04
const wParam = lpMsg.add(0x08).readU32(); //wParam은 상대주소 +0x08
if (msg === 0x0102) {//msg가 102이면 WM_CHAR
//console.log(`[${name}] msg = 0x${msg.toString(16)} wParam=${wParam}`);
send(wParam)
}
}
});
}
hookfunc("DispatchMessageW");
일단 이것으로 오늘 포스팅을 마치겠습니다.
notepad 후킹하는데 엄청 오래걸렸지만, 나름 뿌듯함이 있는 공부였던 것 같습니다.
곰곰히 생각해서 이런 후킹을 어떻게 더 효율적으로 할 수 있는지도 생각해봐야될 것 같습니다.
이상으로 포스팅 마치겠습니다. 읽어주셔서 감사합니다~

'취약점분석 > Reversing' 카테고리의 다른 글
| [Reversing] angr CTF 문제 풀이 - 1 (00 ~ 03) (0) | 2026.01.01 |
|---|---|
| [Reversing] angr 사용 기초 분석 - 1 (설치 & 실습) (0) | 2025.12.31 |
| [Reversing] Hooking - 8 ( notepad.exe 후킹 ③ ) (4) | 2025.08.08 |
| [Reversing] Hooking - 7 (notepad.exe 후킹 ②) (7) | 2025.07.30 |
| [Reversing] Hooking - 6 (notepad.exe 후킹 ①) (2) | 2025.07.26 |