문제 풀이/Write-up

[Write-up] Captain-Hook Write up (frida Hooking ③)

poiri3r 2025. 7. 14. 18:51

 

안녕하세요. 오늘 포스팅은 드림핵 리버싱 5단계 문제인 Captain-Hook 문제의 write-up입니다.

문제를 자력으로 풀어본적은 없고 다른 write up이랑 코드를 보면서 플래그만 얻었었는데요,

이번에 후킹에 대해 공부도 해봤고, 난이도가 조금 높긴 하지만 풀이 참고만 하고 write up을 작성해보겠습니다.

https://dreamhack.io/wargame/challenges/51

 

1.exe파일 분석

 

저 captainhook.exe파일을 실행해보겠습니다.

 

화면에 숫자 또는 알파벳과 랜덤생성된 것 같은 선들이 있습니다.

좌클릭을 할때마다 안에 들어있는 글자와 선들이 바뀝니다. 클릭을 계속하면 계속 바뀌고, 나중가면 0이 대부분으로 채워져있습니다.

 

PE구조를 먼저 확인해보겠습니다.

 

크게 특이한 부분은 없고 EP가 1D89C라고 되어있으니 해당 부분을 IDA로 확인해보겠습니다.

 

1D89C

안에 _security_init_cookie는 스택 보호를 위한 시그니처 값을 초기화하는 함수입니다.

__scrt_common_main_seh() 함수는 프로그램이 실행 전 기본 함수들과 초기화 루틴 등을 담당하고, 진짜 시작점인 WinMain을 호출합니다.

 

메인 함수 부분을 살펴보기에 앞서서 함수의 Import 테이블을 살펴보겠습니다.

gdiplus라는 모듈이 import되고 있는데 ,GDI는 그래픽 디바이스 인터페이스로 주로 window 창에 선을 그리는 역할을 합니다.

import 테이블을 봤을 때 저희가 실행했을 때 보이는 난수생성된 선과, 글자들이 gdiplus 모듈을 통해 그려졌음을 확인할 수 있습니다. gdi관련 함수들을 유심히 봐야겠다고 생각하면서 WinMain 함수를 분석해보겠습니다.

winMain의 디스어셈블

여기서 sub_140016200을 열어보겠습니다.

sub_140016200

case별로 조금 분류가 되어있는데 case 0xFu에서 GDI로 그림 그리는 객체를 생성한 뒤 sub_140016360()호출 후 정리를 하고 있기 때문에 그림 그리는 핵심은_140016360안에 있음을 확인할 수 있습니다.

또 case0x202u는 마우스 좌클릭 이벤트입니다.

마우스 좌클릭 이벤트가 발생했을 때 화면이 다시 그려지게 합니다(클릭할 때마다 화면이 바뀜)

 

이제 저희는 sub_140016360을 분석해보고, 구조를 확인한 뒤 어떤 함수를 후킹해서 어떤 결과를 찾을지 생각해보면 됩니다.

해당 함수를 분석해보겠습니다.

 

 

sub_140016360을 열어보면 3300줄이 넘는 코드가 있습니다.

난독화도 되어있고 복잡하기 때문에 한줄한줄 분석하는건 크게 의미가 없습니다.

슥슥 내려보면서 함수 구조를 파악할만한 함수를 찾아보겠습니다.

 

 

내리다보면 중간중간 다른 함수들을 호출하는 부분을 발견하였습니다.

호출된 함수들의 내부를 들여다봤을 때 공통적인 부분이 있었는데, 

 

함수 내부에 난독화가 쭉 되어있고 마지막 부분에 보면 아까 유의해서 보려고 했던 GDI 관련 함수들이 있습니다.

호출되는 함수가 꽤 많이 있는데 대부분 비슷한 형태로 이루어져 있습니다

그 중 하나인 sub_140005060을 자세히 분석해보겠습니다.

(왼쪽)함수의 초반,(오른쪽)함수의 중반
3번째 이미지
함수의 마지막 부분

GPT의 도움을 받아 분석한 결과 1,2번째 사진의 코드가 난수를 생성한 뒤 3번째 이미지에서 난수생성된 선을 그립니다.

그 뒤 5번 반복해서 랜덤한 5개의 선을 그립니다.

(*GdipDrawLineI의 인자값이 난수값입니다) 

그 뒤 맨 마지막 부분의 코드에서 선을 그리는데 GdipDrawLineI의 인자값이 그림을 유추해볼 수 있게 되어있습니다.

해당 도형은 D와 같은 형태입니다.

해당 부분의 그림을 그려주는 함수로 추측가능할 것 같습니다.

sub_140005060부분을 분석해본 결과 제일 마지막의 GdipDrawLineI만 확인하면 어떤 글자를 나타내는지 알 수 있을 것 같아 다른함수 하나만 더 확인해보겠습니다.

 

sub_140001240의 마지막 부분을 확인해보겠습니다.

 

해당 함수는 숫자 0을 나타내는 그림입니다.

 

 

핵심코드인 sub_140016360에 대한 요약을 해보겠습니다.

  • 코드의 중간중간에 다른 함수들을 호출한다
  • 호출된 함수들은 5개의 랜덤한 선을 그린다.
  • ★호출된 함수들은 각자 다른 문자를 GdipCreatePen을 통해 그린다
  • 매번 선을 그릴때마다 CreatePen1,GdipDrawLineI,GdipSetSmoothingMode,GdipDeletePen이 반복되는 구조이다.

IDA-View로 봤을 때 다음과 같은 병렬구조입니다.

 

일단 여기까지 중간 점검을 해봤을 때 목표는 다음과 같습니다.

클릭할 때 마다 나오는 문자를 모두 저장해야합니다.
->프로세스를 실행해서 처음 나오는 글자는 4D5A로 PE파일의 MZ시그니처임
->함수의 호출주소나 리턴주소 등을 활용해 어떤 글자가 적히는지 따로 저장

 

저희가 이제 생각해봐야하는 것은 2가지입니다.

첫번째로 어떤 함수를 후킹해서 문자,숫자를 저장할지

두번째로 좌클릭 이벤트를 어떻게 발생시킬지 생각해봐야합니다.

 

이런 좀 난이도 있는 문제를 풀 때는 여태까지의 진행 상황과 중간단계의 목표들을 정해놓고 풀어보는게 조금 더 원할하게 풀 수 있는 것 같습니다.

 

 

2.후킹 함수 찾기

 

어떤 함수를 후킹해야할지 생각해보겠습니다.

고민을 좀 해봤는데 IDA VIEW로 프로그램의 동작을 분석해본 결과 글자로 표현되는 수는 총 16개입니다.

함수도 총 16개 존재하고 함수마다 각 기호가 연결되어있는데 처음 함수 실행했을 때 4D5A(MZ 시그니처)가 나왔고, 해당 글자들은 모두 16진수로 이루어진게 아닌가 하는 생각이 들었습니다.

GPT한테 물어보면서 대충 대응을 해봤는데 지피티가 멍청해서 그림도 제대로 못그리는건지 똑같은 숫자가 한 3개정도 나오네요..

좌표만 보고 직접 그림을 그려보면서 대응을 해봐야될 것 같습니다.

일단 16진수에 대응한다는것을 알았으니 다시 처음부터 좌표를 그려가며 함수에 알파벳을 대응해보겠습니다.

 

다음과 같이 대응되는 16개의 16진수 숫자를 구했습니다. 나름 친절하게 되어있었네요 ..

이제 해당 함수를 어떻게 매치시켜서 저장할지 생각해보면 될 것 같습니다.

 

클릭할때마다 실행되는 16가지의 함수 중에서 공통적인 특징을 뽑아내서 추출하면서도 차이를 읽어서 매핑된 숫자를 저장해야합니다.

1.GdipCreatePen1의 인자값을 이용해서 후킹

여기 펜을 생성하는 GdipCreatePen에서 특이한 부분이 있는데

난수생성펜
16진수 생성 펜

난수생성된 선을 그리는 GdipCreatPen1의 첫번째 인자에는 모두 4278190080LL (0xFF000000) *검은색이 들어가고,

16진수를 그리는 GipCreatPen1의 첫번째 인자에는 a4가 들어갑니다.

화면을 확대해보았을 때 난수생성된 선을 그리는 색상과 숫자를 그리는 펜이 미세하게 다른데 숫자를 그리는 펜은 GdipSetSmoothingMode를 통해 안티 앨리어싱이 적용되어서 그렇습니다.

안티 앨리어싱이랑 색상은 난수생성된 선과 숫자기호끼리 다른거라 함수별 차이는 없는 것 같습니다.

 

=> 색상을 통한 후킹 분류는 실제로 적용하긴 어려울 것 같습니다.

 

2.GdipDrawLineI의 인자값을 이용해 그림을 이용해서 숫자 분류하기

 

0~F까지 그리는 루틴은 x좌표y좌표가 고정되어 있는데 , 그 좌표값을 이용해서 숫자를 매핑하는 방법도 가능할 것 같습니다.

Interceptor.attach(Module.findExportByName(null, 'GdipDrawLineI'), {
    onEnter: function(args) {
        const x1 = args[2].toInt32();
        const y1 = args[3].toInt32();
        const x2 = args[4].toInt32();
        const y2 = args[5].toInt32();

 

해당 방식으로 GdipDrawLineI의 인자를 x1,y1,x2,y2에 저장해둔뒤 수 많은 반복문을 통해 문자 패턴을 찾을 수 있지 않을까 싶었습니다.

if (x1 === 10 && y1 === 10 && x2 === 10 && y2 === 40) {
            send("1");

 

근데 생각해보니 숫자가 digit형식이고 여러개의 선들로 이루어져 있어서 인자들로 분류하는 코드를 짜는게 너무 복잡해 보입니다.

  v182 = GdipDrawLineI(*(_QWORD *)v181, v201, a2 + 1, a3 + 60, v156, a3 + 60);

이런식으로 인자가 고정된 값이 아니라 런타임으로 계산되기 때문에 더 어려워보입니다.

구현을 하려면 할 수 있을 것 같긴한데 case를 통한 분류만 해도 엄청 길어질 것 같습니다.

또한 저희가 추출해야하는 숫자가 굉장히 많아서 코드가 복잡해지면 추출을 하는 과정이 너무 길어질 수 있어서 다른 방법을 찾아보겠습니다.

 

3.GdipCreatePen1의 호출 위치 기준으로 후킹

Frida에서 제공하는 this 기능이 있습니다.

this는 Frida가 후킹시에 넘겨주는 특수 객체로 this.returnAddress는 후킹한 함수의 리턴 주소를 반환해줍니다.

따라서 GdipCreatePen1을 후킹한 뒤 this.returnAddress를 통해 호출 지점을 확인하고 Base주소를 뺀 뒤 GdipCreatePen을 호출한 함수가 어딘지 확인하고 매핑하는 방식으로 접근해봐야 합니다.

 

먼저 문자 수집은 나중에하고 GdipCreatePen1이랑 GdipDrawLineI이 호출될때마다 로그를 찍어주는 코드를 한번 작성해보겠습니다.

const base = Module.findBaseAddress('CaptainHook.exe');

Interceptor.attach(Module.findExportByName(null, 'GdipCreatePen1'), {
    onEnter: function(args){
        const offset = this.returnAddress.sub(base).toString(16);
        console.log('GdipCreatePen1 called');
        console.log('returnaddress offset: 0x' + offset);
    }
})
Interceptor.attach(Module.findExportByName(null, 'GdipDrawLineI'), {
    onEnter: function (args){
        const offset = this.returnAddress.sub(base).toString(16);
        const x1 = args[2].toInt32();
        const y1 = args[3].toInt32();
        const x2 = args[4].toInt32();
        const y2 = args[5].toInt32();
        console.log('[*] GdipDrawLineI called');
        console.log('    ↳ returnAddress offset: 0x' + offset);
        console.log(`    ↳ (${x1}, ${y1}) → (${x2}, ${y2})`);
    }
});

console.log('hook.js loaded')

 

코드는 다음과 같이 짰고, GdipCreatePen1과 GdipDrawLineI를 후킹했습니다.

GdipCreatePen1이 호출될때는 오프셋만, GdipDrawLineI가 호출될 때는 주소값과 좌표까지 찍히도록 코드를 작성했습니다.

저장후 다음 명령어로 실행을 해줬습니다.

frida -f Captainhook.exe -l logcheck.js

다음처럼 로그가 찍히는데 정보량이 좀 많아서 주석을 이용해 하나씩 확인해보겠습니다.

먼저 GdipCreatePen1입니다.

로그가 이런식으로 찍히는데, 각각 호출 위치가 달라서 난수생성하는 CreatePen이든 아니면 기호를 그려주는 함수 호출 위치든 상관없이 후킹이 가능할 것 같습니다.

 

이번엔 GdipDrawLineI입니다.

 

GdipDrawLineI도 마찬가지로 그려지는 숫자에 따라 리턴 주소가 다르기 때문에 해당 함수를 기준으로 후킹이 가능할 것 같습니다!

 

가능한 호출이 적은 함수를 사용해야 숫자를 수집할 때의 소요시간이 줄어들 것 같은데 어차피 선 하나당 CreatePen1 -> DrawLineI -> DeletePen이 반복되는 구조기 때문에 아무거나 선택해도 상관 없을 것 같습니다.

몇번째 호출 함수를 기준으로 삼을지도 아무 상관이 없을것 같기 때문에 로그가 5개씩 찍히는 난수 선의 호출 위치를 기준으로 대응하는 digit map을 작성하겠습니다.

const base = Module.findBaseAddress('CaptainHook.exe');

Interceptor.attach(Module.findExportByName(null, 'GdipCreatePen1'), {
    onEnter: function(args){
        const offset = this.returnAddress.sub(base).toString(16);
        console.log('GdipCreatePen1 called');
        console.log('returnaddress offset: 0x' + offset);
    }
})

 

해당 코드로 프로세스를 계속 돌려보면서 메모장을 키고 digit map을 작성해두겠습니다.

작성을 했더니 해당 주소가 나왔습니다. 한번 검산차원에서 확인해보겠습니다.

 

0,1,2를 확인했을 때 위치가 call 다음 ret값인 것을 확인해볼 수 있었고 해당 위치를 IDA로 확인해봤을 때 아까 작성했던 함수:숫자 대응에 일치하는 것을 확인했습니다.

 

3.후킹스크립트 작성

이제 후킹 스크립트를 작성해보겠습니다.

 

먼저 digitmap을 코드로 작성해야합니다. 다음과 같이 작성했습니다.

const digitMap = {
    0x9aed: '0', 0xb020: '1', 0xc38d: '2', 0xd83d: '3',
    0xeced: '4', 0x1012d: '5', 0x115dd: '6', 0x12b0d: '7',
    0x13f4d: '8', 0x154dd: '9',0x1f7d: 'a', 0x349d: 'b',
    0x495d: 'c', 0x5d9d: 'd', 0x71ed: 'e', 0x86ad: 'f', 
}

 

이제 GdipCreatePen1을 후킹해서 따로 메모장에 작성해두는 코드를 작성해야 합니다. 

저희는 파이썬코드를 작성한 후 파이썬 코드에서 자바스크립트를 실행할 것이기 때문에 파이썬 코드를 먼저 완성하겠습니다.

import frida
pid = frida.spawn(["CaptahinHook.exe"])
session = frida.attach(pid)

with open("cthookcode.js", "r", encoding="utf-8") as f:
    script = session.create_script(f.read())
 
def on_message(message, data):
	if message['type'] == 'send':
    	with open("flag.txt", "a" as f:
        	f.write(message['payload'])
        print(message['payload'])

script.on("message", on_message)
script.load()
frida.resume(pid)
input("Press AnyKey to exit ...")
session.detach()

 

session.create_script는 Frida의 메서드로 대상 프로세스에 JavaScript 후킹 코드를 주입할수 있게 만들어줍니다.

jscode에서 메세지를 받으면 해당 메세지 타입이 send인지 검사하고 send로 전송된 경우에만 flag.txt에 내용을 저장하는 코드입니다.

 

다음으로 자바스크립트를 마저 완성해보겠습니다.

const base = Module.findBaseAddress('CaptainHook.exe')

//digitMap
const digitMap = {
    0x9aed: '0', 0xb020: '1', 0xc38d: '2', 0xd83d: '3',
    0xeced: '4', 0x1012d: '5', 0x115dd: '6', 0x12b0d: '7',
    0x13f4d: '8', 0x154dd: '9',0x1f7d: 'a', 0x349d: 'b',
    0x495d: 'c', 0x5d9d: 'd', 0x71ed: 'e', 0x86ad: 'f', 
}

//후킹코드
Interceptor.attach(Module.findExportByName(null, 'GdipCreatePen1'),{
	onEnter: function(args){
    	if (!this.context.rcx || !this.context.rcx.equals(0xFF000000)){
        	for (let i = 0; i<4; i++){
            	args[i] = ptr(0);
               }
               return;
             }
        const off = this.returnAddress.sub(base).toInt32();
        const ch = digitMap[off];
        
        if(ch){
        	console.log(ch);
            send(ch);
        }
    }
});

 

대부분의 코드가 이해되실거라 생각하고 한곳만 추가 설명을 붙이겠습니다.

if (!this.context.rcx || !this.context.rcx.equals(0xFF000000)){
        	for (let i = 0; i<4; i++){
            	args[i] = ptr(0);
               }
               return;
             }

 

해당 부분은 크래시 방지를 위한 부분으로, GdipCreatePen1 호출될 때 rcx가 존재하는지 확인하고 들어가있는 값이 0xFF000000인지 확인하여 맞는 경우에만(GdipCreatePen1의 첫번째 인자) 다음 후킹을 진행하고 아니라면 GdipCreatePen1의 인자에 0을 넣고 넘깁니다.

우리가 후킹하려는 대상 외에도 GdipCreatepen1을 시스템 내부적으로 호출할 가능성이 있기 때문에 작성해야합니다.

 

4.코드 작동 확인

파워셸에서 작성한 코드를 잘 작동하는지 실행해보겠습니다.

코드도 잘 실행되고 메모장에도 잘 저장은 되지만 한가지 문제가 있습니다. 랜덤생성하는 createpen1을 후킹 대상으로 잡았더니 한번에 숫자가 5번씩 찍히는 문제가 발생했습니다. 해당 문제점은 digitmap을 조금 수정해서 글자를 그리는 createpen1을 후킹으로 해도 되고 파이썬으로 코드를 작성해서 1,5,10 .. 째 글자 외엔 다 지우는 식으로 해도 될것같습니다.

 

좀 귀찮긴한데 digitmap을 수정해보겠습니다.

const digitMap = {
    0xa00a: '0', 0xb54a: '1', 0xc8aa: '2',  0xdd5a: '3',
    0xf217: '4',  0x1064a: '5', 0x11b07: '6', 0x13037: '7',
    0x14477: '8', 0x15a07: '9', 0x24a7: 'a', 0x3b95: 'b',
    0x4e87: 'c',  0x62c7: 'd', 0x770a: 'e',  0x8bd7: 'f',
}

 

일일히 수정을 마쳤습니다. 다시 실행해보겠습니다.

이번에도 로그가 두번 찍히는데, 메모장엔 정상적으로 저장이 되고 있습니다. 아마 파이썬에도 print구문이 있고, js에도 해당 구문이 있어서 그런것 같아서 하나를 지워주고 나머지 작업을 진행하겠습니다.

출력이랑 저장 다 완벽하게 작동합니다.

이제 조금 고민해봐야되는 부분이 있는데 마우스 클릭 이벤트를 어떻게 처리할것인가입니다.

쉽게 생각하려면 오토마우스를 다운받은 뒤 클릭을 자동으로 하면 되기는 합니다만.. 아무래도 리버싱하는 입장에서 아쉽긴 합니다.

전에 처음 프로세스를 분석하면서 좌클릭 입력을 받아서 그림을 다시 그려주는 코드가 있었던 것 같아 찾아보겠습니다.

해당 case 0x202u부분을 패치해봤는데 아쉽게도 실패해버렸습니다.

sub_140016360을 호출하기 전에 GDI 초기화를 해줘야하는데 해당 코드를 주입할수가 없어서 실패했고, Frida로 WM_LBUTTONUP 메세지를 자동으로 보내는 방식도 있는데 오토마우스가 더 효율적일 것 같아서 그냥 오토마우스를 설치하겠습니다 ..

여기서 설치했습니다

 

https://sosal.kr/1076

 

오토마우스 무한클릭 v1.7 - Auto Click

/* * http://sosal.kr/ * made by so_Sal */ 마우스를 자동으로 클릭해주고, 키보드를 자동으로 입력하게 하는 오토마우스 무한클릭입니다. 무한클릭 프로그램은 관리자 권한을 일체 요청하지 않는 안전한

sosal.kr

화면이 귀엽네요.

파워셸에서 후킹파일을 붙여서 captainhook.exe을 실행해준뒤 오토마우스를 키고 정보를 수집해봅시다.

 

같이 추출을 했고 한 30분정도 지나서 추출을 완료했습니다.

메모장엔 엄청나게 긴 문자열이 수집이 됐고 헥스 덤프이기 때문에 복구 스크립트가 필요합니다.

 

5.hex코드 -> exe파일 변환

복구 스크립트는 지피티의 도움을 받아 파이썬으로 작성해보겠습니다.

 

with open("flag.txt", "r") as f:
    hex_data = f.read().replace("\n", "").replace(" ", "")

with open("recovered.exe", "wb") as f:
    f.write(bytes.fromhex(hex_data))

를 flagtoexe.py로 저장을 했고 셸에서 실행해주었습니다.

해당 스크립트는 flag.txt를 읽기 모드로 열어서 파일 전체를 문자열로 읽고 줄바꿈 및 공백을 제거해줍니다.

(원래 flag.txt에 줄바꿈과 공백이 없어서 생략해도 가능합니다. 생략한 스크립트는 다음과 같습니다.)

with open("flag.txt", "r") as f:
    hex_data = f.read()

with open("recovered.exe", "wb") as f:
    f.write(bytes.fromhex(hex_data))

 

그 뒤 recoverd.exe를 읽고 쓰기 모드로 읽어서  바이너리 바이트 배열로 변환하고 저장합니다.

파일에 다음과 같은 실행파일이 생겼습니다.

실행 시켜보겠습니다.

 

저 검은 부분만 패치를 하면 FLAG를 획득할 것 같습니다!

IDA로 실행시켜주겠습니다.

여기서 문제가 발생했는데, 바로 recovered.exe가 UPX 패킹된 파일이라는 것입니다. 

패킹된 파일은 자체로 패치가 불가능하기 때문에 언패킹 과정을 거친 후에 안의 코드를 수정해야합니다.

UPX 패킹은 굉장히 대중적인 패킹 방법이어서 언패킹하는 방법을 알아두면 좋습니다.

패킹 언패킹 관련해서는 다음에 따로 포스팅을 해보겠습니다.

 

6.UPX 언패킹

 

먼저 다음 명령어로 UPX를 설치해두겠습니다.

sudo apt install upx

 

그 다음 아래 명령어로 패킹을 풀어주겠습니다.

upx -d recovered.exe

패킹을 풀고 다시 ida로 연결해주면

이렇게 정상적인 화면이 나오게 됩니다.

 

이제 본격적으로 패치를 통해 FLAG를 찾아보겠습니다.

 

7.파일 패치

내부 함수들을 들여다보면 sub_14001770에 다음과 같은 함수들이 나열되어있습니다.

 

안을 들여다보면 다음과 같습니다.


저희에게 아주 익숙한 펜으로 선을 그리는 함수입니다.

지피티가 아주 똑똑하기 떄문에 내부구조랑 겉의 인자들을 보여주면 선들을 시각화해서 보여줍니다. 처음 풀 때는 그냥 느낌으로 패치를 했었는데, 지금은 구조가 이해가 되기 때문에 지피티의 도움을 받겠습니다.

보면 다음과 같이 저희의 FLAG를 가리는 선인걸 확인할 수 있습니다.

나머지 다른 함수들도 사실 일일히 분석할 필요 없이 지피티한테 분석을 맡기면 다 그려줍니다.

개개인의 실력을 향상시키는 것도 좋지만 AI를 잘 활용하는 것도 능력이라고 생각합니다..ㅎ

얼마전에 부산대부경대 세미나에서 AI와 함께하는 CTF 이런 내용을 들었는데 확실히 필요해보이네요

 

굳이 패치를 안하고도 함수를 분석해서 플래그를 뽑을 수 있는데 다음과 같습니다.

(*직접 해보시라고 가렸습니다)

 

만약에 패치를 하고 싶으면 sub_1400016C8의 내부를 NOP으로 패치하거나 바로 ret하게끔 패치할 수 있습니다.
내부 바로 첫번째 어셈블리구문을 ret으로 바꿔줬습니다.

그 다음 프로그램을 실행시키면

이렇게 나타납니다.

 

이상으로 CaptainHook의 Write-up을 마치겠습니다.

정말 정말 긴 글이 되었네요 .. 

5단계 문제치고도 정말 중요한 내용들이 정말 많고 좋은 문제라 문제풀이를 본격적으로 시작하고 싶은 사람들에게 강추하고 싶은 문제입니다.

며칠에 걸쳐서 썼는데 쓰면서도 참 공부가 많이 된 것 같습니다. 이런 write up은 꼭 따라해보면서 읽으셨음 좋겠습니다.

아마 이 문제만큼은 제가 Write-up을 제일 잘 쓰지 않았을까..싶네요 ㅎ

 

이상으로 포스팅을 마치겠습니다. 다음 포스팅은 좀 더 가벼운 포스팅으로 돌아오겠습니다. 읽어주셔서 감사합니다~