1. Home
  2. 보안 강좌/CheatEngine
  3. [Cheat Engine] 치트엔진 튜토리얼 #8 풀이

[Cheat Engine] 치트엔진 튜토리얼 #8 풀이

글을 읽기 전 필요한 지식

C언어 포인터

어셈블리 명령어 (x86 / x64 intel / amd 명령어)

컴퓨팅 구조

 

8단계: 다중 레벨 포인터 (PW = 525927)
이 단계에서는 다중 레벨 포인터를 사용하는 방법에 대해 설명합니다.
6단계에서는 간단한 1단계 포인터를 가졌으며 이미 찾은 첫 번째 주소가 실제 기본 주소였습니다.
하지만 이번 단계는 4단계 포인터입니다. 이것은 포인터에서 포인터를 지나 또 다른 포인터를 거쳐 최종적으로 체력 값에 도달하는 방식입니다.

기본적으로 6단계와 같이 값을 접근하는 것을 찾아 명령어와 기본 포인터 값, 그리고 오프셋을 찾아 기록하면 됩니다. 그러나 이 경우에 찾은 주소는 또한 포인터일 것입니다. 값과 마찬가지로 그 포인터의 포인터를 찾아야 합니다. 찾은 주소에 접근하는 것을 찾고, 어셈블리어 명령어를 살펴보고, 가능한 명령과 오프셋을 기록하고 사용하면 됩니다.
기본 주소가 정적 주소(초록색으로 표시됨)일 때까지 계속 진행하면 됩니다.

값을 변경하려면 "값 변경"을 클릭하여 튜토리얼이 체력에 접근하도록 해야 합니다.
포인터 경로를 찾았다면 "레지스터 변경"을 클릭하십시오. 그러면 포인터와 값이 변경되며 주소를 5000으로 고정할 3초가 주어집니다.

추가: 이 문제는 자동 어셈블러 스크립트 또는 포인터 스캐너를 사용하여 해결할 수도 있습니다.

추가2: mov eax, [eax]와 같은 명령을 만날 때 CE(Codefinder)의 설정을 액세스 위반으로 변경하는 것이 권장되며, 디버그 레지스터는 변경된 후에 표시되기 때문에 포인터 값이 무엇인지 찾기가 어려워집니다.

추가3: 아직 읽고 계시다면, 어셈블러 명령을 살펴볼 때 포인터가 동일한 코드 블록(동일한 루틴, 어셈블러를 알고 있다면 루틴의 시작까지 올라가보세요)에서 읽히고 채워지는 것을 알 수 있을 것입니다. 이것은 항상 그렇지는 않지만 디버깅이 어려울 때 포인터를 찾는 데 매우 유용할 수 있습니다.

안녕하세요 파일입니다. 이번엔 치트엔진 튜토리얼 #8번 풀이입니다. 이번 문제는 저번 #6번 문제의 연장선상이라고 할 수 있는 문제입니다. 저번에는 변수를 가리키는 포인터 변수 하나를 찾았지만, 이번엔 포인터에, 포인터에, 포인터에, 포인터인 무려 4중 포인터를 찾아내야 하는 문제입니다. 심지어 무조건 내가 찾는 변수의 위치를 가리키는게 아니라 몇바이트씩 ±씩 어긋나 있는 오프셋(Offset) 개념도 등장합니다.

 

물론 방법 자체만 알면 이번 문제도 어렵지 않게 풀어낼 수 있습니다. 개념적인 이해가 어렵겠지만요..

설명은 드릴건데 상당히 매울 수 있으므로 주의하시길 바랍니다 ㅎㅎ;

그럼 시작해보겠습니다!

 

Remind of #6

시작전에 #6번 문제의 풀이를 한 번 복습해보는 시간을 가지겠습니다.

저번에는 초기 튜토리얼 6번을 실행하면 다음과 같이 체력값 공간(==val1) 을 어떤 포인터가 가리키고 바꾸는 형태였습니다.

 

그리고 포인터 변경 버튼을 누르는 순간에 빈 공간을 찾아서 새로 체력값 공간(val2)을 잡고, 포인터가 그곳을 가리키게 하여 다시 조작하는 형태였죠.

 

원래 체력값 공간(val1) 은 더 이상 사용되지 않기 때문에 기존에 체력값 주소를 찾아놨다고 해도 무용지물이 되는것이였습니다. 이토록 포인터를 사용하면 메모리 조작이 어려워지는데요.

 

치트엔진으로 메모리 조작을 원천적으로 차단하긴 어려우므로, 요즘 게임들은 이렇게 조작할 여지가 있는 값(체력, 공격력, 등등)을 포인터로 조작한다던가, XOR 암호화 해놓는다던가 여러 보안 솔루션을 마련해 놓습니다.

 

어쨌던 이번 문제도 6번처럼 포인터를 이용해서 값을 바꾸고 있는데, 4중 포인터로 값을 바꾸고 있으므로 어떤 값을 바꾸는 포인터를 찾았다고 해도, 그 포인터 역시 포인터의 포인터가 바꾸므로 제대로 값을 고정시킬 수 없고, 포인터의 포인터를 찾아도 포인터의 포인터의 포인터... 가 또 바꾸기 때문에 포인터 위치가 바뀌면 제대로 값을 고정시킬 수 없습니다.

 

결론적으로 4중 포인터이므로 제일 베이스로 값을 바꾸는 포인터의 포인터의 포인터의 포인터. 를 찾아내야 합니다 ㅋㅋㅋ

포인터 하나만으론 보안이 부족하다고 생각했는지 이렇게 다중 포인터를 이용해 보안을 줄 수도 있나 봅니다.

좀 지저분하고 귀찮은 문제인데 어쨌던 시간만 오래걸릴 뿐이지 결국엔 찾아낼 수 있습니다.

 

Solution

항상 하던대로 치트엔진으로 튜토리얼 프로세스를 잡고(Attach) 시작 한 후,

Change value 옆의 값 33을 검색합니다.

 

바꾸고 다시 검색! 그럼 현재 화면에 표시되는 값의 주소를 알 수 있습니다.

0154AE8 이라는 주소에 있군요. (치트엔진에서 초록색 주소값이 아닌경우엔 고정이 아니라는 뜻이기 때문에 이 주소값은 아시다 싶이 프로그램을 다시 시작할때마다 바뀌고, 컴퓨터 OS 환경에 따라 전부 다르게 나타납니다)

 

1차적으로 우리가 값을 찾은 상태를 그림으로 나타내면 다음과 같습니다. 찾아놓은 값의 이름을 편의상 val1 이라고 하겠습니다.

 

일단은 4레벨 포인터에 의해 값이 바뀐다고 했으니깐 1차적으로 val1 을 가리키는 포인터가 존재할 것입니다.

 

mov [0154AE8], Change Value누르면 바뀔값

그리고, 포인터가 바꾸던 이 변수에 접근해서 직접바꾸던 간에 Change value를 누른 순간에 다음과 비슷한 명령어가 실행될 것이라는걸 추측 해볼 수 있습니다.

Change value를 누른 순간에 어떤값이 랜덤으로 생성되서 val1에 저장되겠죠.

 

여기서 중요한점은 mov 명령어 실행 시 저장되어야 할 공간인 [0154AE8] 에 대한 부분입니다.

 

만약에 포인터로 접근해야 한다면 어떤 포인터 변수가 0154AE8 라는 주소를 가지고 있을 것이고, 이를 통해서 접근하게 될 것입니다. 그러나.. 여기선 오프셋이라는 개념이 등장해서 포인터 변수가 꼭 0154AE8 라는 위치를 정확히 가르키지 않고, 몇 바이트 + 되거나 - 되는 곳을 가리키게 될 수 있습니다.

 

아직은 무슨말인지 잘 이해가 안되실 겁니다.

치트엔진의 기능을 통해서 어떻게 포인터가 val1에 값을 쓰게 되는지 알아보겠습니다.

 

어떤 포인터 변수가 이 주소에 접근해서 값을 쓰고 있는지 추적하기 위해 Find out what accesses.. 를 클릭합니다.

 

이와 같은 창이 뜨고 Change value 버튼을 한번 누르면 다음과 같은 명령어 2개가 개입되어서 val1의 값이 바뀌고 있다는걸 알 수 있습니다.

 

mov [0154AE8], Change Value누르면 바뀔값

아까도 추정했듯이 val1의 값을 바꾸기 위해선 val1의 주소에 값을 쓰는 명령어가 있어야 하고, 위와 같은 형태가 될 것이라고 추측했습니다.

 

10002E94A - 89 46 18  - mov [rsi+18],eax

이것과 비슷한 형태로 값을 쓰는 명령어는 이 명령어 하나밖에 안보입니다.

 

우선 eax는 RAX 레지스터의 32비트 부분, 000001D2 이라는 값이 담겨있고 이건 10진수로 466, Change value를 눌렀을 때 화면에 표시된 그 값입니다.

 

그리고 RSI의 경우 001574AD0 가 저장되어 있고, 거기에 +18을 하고있습니다.

 

이건 우리가 찾아놓은 val1의 주소값입니다.

 

10002E94A - 89 46 18  - mov [rsi+18],eax
= mov [01574AE8], 1DE

결론적으로 상수형태로 표현하면 이 명령어는 우리가 처음 실행될것이라고 추정했던 그 명령어가 맞습니다.

 

그러나 여기서 중요한 점은 [rsi+18] 로 접근한다는 점인데, 제가 확인해본 결과 여기서 RSI는 포인터 변수가 가리키는 주소가 되고, +18은 오프셋이 된다는 걸 알았습니다. (오프셋은 곧 설명드리도록 하겠습니다)

왜 이렇게 되는걸까요?

 

여기서부턴 제가 공부하면서 연구한 부분이라 틀린 내용이 있을 수 있습니다. 잘못된 내용이 있으면 댓글로 피드백 부탁드려요!

 

모든 어셈블리 코드를 하나하나 살펴보진 않았으므로 여기서도 약간 상상력과 추측이 필요합니다.

 

일단은 여기서도 상상을 해보는건데, 일단저 치트엔진에서 명령어를 클릭하면 나오는 아래 레지스터 값들은 그 명령어를 실행했을 때 당시의 레지스터 상황을 보여주는 창입니다.

 

저 명령어 실행당시 rsi 값은 001574AD0 이라는 주소값인데요. CPU는 레지스터에 값을 설정할 때 메모리(RAM) 에서도 가져올 수 있고, 상수값을 그대로 설정할 수도 있습니다.

 

아마 어딘가에 포인터 변수가 있고, 그 포인터 변수의 값이 001574AD0 라는 공간을 가리키고 있어서, 

mov [rsi+18],eax 명령어 실행 이전에 포인터 변수 공간에서 001574AD0 라는 주소값을 가져와 rsi 레지스터에 설정하지 않았나 하는 추측입니다.

 

 

주소는 제가 지금 찾아놓은 값인 0517AE8로 하겠습니다. 실행환경에 따라 주소가 다 다르겠지만 양해하고 읽어주세요.

그림으로 나타내면 다음과 같이 됩니다. 원래 001574AD0 라는 공간을 가리키는 포인터 변수가 램 어딘가에 존재했으며 (아직 주소를 모르므로 ?로 표기했습니다)  주소 001574AD0 의 "기준" 에서 val1의 값에 접근하려면 주소에 +18만큼 더해야 합니다. (16진수 18을 의미하며, 우리가 쓰는 10진수로는 24만큼 더하는것을 의미합니다)

 

이 +18을 오프셋(Offset) 이라고 합니다.어떤 기준에서 얼마만큼 떨어져있는걸 오프셋 ~~ 만큼 떨어져 있다고 표현하게 됩니다. (또는 오프셋 +18 이라고 합니다.)

 

어쨌던 컴퓨터는 val1 주소의 -18 위치의 주소를 가리키는 포인터를 이용해서,

그 포인터 값에 오프셋 +18을 더해서 val1에 접근하게 됩니다.

컴퓨터 과학에서 어떤 값을 기준으로 떨어져 있는 정도를 표현할 때 오프셋이라는 개념을 사용합니다.
예를 들어서 "abcdef" 라는 문자열이 있다면 d는 a를 기준으로 3칸만큼 떨어져있습니다.
즉 a 시작점 기준에서 d는 +3의 오프셋을 가집니다.

 

왜 val1의 주소 0517AE8의 주소를 직접 가리켜서 접근하지 않고 이렇게 -18 위치의 주소를 가리키고, 나중에 +18을 더해서 접근하냐? 하면 이건 사실 여러가지 이유가 있을 수 있는데, 일단 첫번째로 컴퓨터 맘이고.. (이게 기계어 실행시 가장 효율적인 메모리 접근 방법이므로 이렇게 했을 수 있고), 두번째로 게임으로 따지면 저기 val1이 체력이 되는데, 구조체로 위에 캐릭터의 공격력도 있을 수 있고, 마나(MP) 도 있을 수 있습니다. 

 

저기 ???? 로 표시된 값들은 확인해보지 않아서 RAM에 어떤 값이 저장되어 있는진 모르지만 val1위에 ????로 표시된 모르는 값들이 캐릭터의 체력, 마나통, 스킬 쿨타임 등등이라고 상상해보세요.

 

저 001574AD0 이라는 꼭대기가 캐릭터 구조체의 첫 시작주소고, 저걸 기준으로 +4 만큼해서 체력을 구하고! 다시 +4해서 마나통 구하고! 이런식이라고 보시면 되겠네요.

 

mov RSI , [??????]

 

일단 컴퓨터 CPU 입장에선 ptr1이라는 포인터 변수를 이용해 val1에 접근해야 하므로 먼저 rsi 레지스터에 위와 같은 명령어를 통해서 포인터 변수가 가지는 주소값을 복사해왔을 겁니다.

저기서 ?????는 아직 모르는 ptr1 포인터 변수의 주소값입니다.

 

그냥 ptr1 포인터 변수 메모리 값을 그대로 읽어서 메모리 - 메모리 위치로 값을 조작하면 되지 않겠냐고 반문할 수 있겠다만 인텔이 x86 명령어(32비트 명령어)를 만들떄 mov 명령어로 mov 메모리 주소, 메모리 주소 처럼 메모리 - 메모리 간의 값을 복사하는 명령어 자체를 만들지 않았기 때문에 메모리값을 이용해, 메모리 값을 조작할때는 이렇게 레지스터로 먼저 복사할 수 밖에 없습니다.

 

그러면 CPU의 RSI 레지스터 역시 ptr1 포인터 변수처럼 똑같이 0514AD0을 가리키는 포인터가 되게 됩니다.

 

mov [rsi+18],eax

그리고 이제 val1의 값을 접근하기 위해서 위 명령어를 실행합니다.

 

바로 아까 치트엔진에서 봤던 그 명령어가 되죠.

 

그래서 결론이 무엇이냐? mov 여기서 rsi 레지스터의 값은 결국 메모리 상의 어떤 포인터 변수가 가지는, 가리키는 곳의 주소가 된다는 겁니다.

 

빨간색 원으로 쳐놓은 곳이 전부 동일한 값이 됩니다.

어짜피 rsi 값이란게 포인터 변수에서 읽어온 주소값을 복사해온게 되기 때문입니다.

 

mov [rsi+18],eax

위 명령어가 실행되기 이전에...

 

mov RSI , [??????]

다음과 같은 명령어가 실행되서 ?????? 주소에 있는 포인터 변수가 가리키는 주소값을 rsi에 저장해뒀고

결론적으로 rsi는 ptr1이 가리키는 곳의 주소가 된다는 겁니다!!

 

그러니깐 앞으로 이런 코드를 보면

 

아.. 어떤 포인터 변수가 가리키는 주소가 있고 이게 rsi 레지스터에 복사되어서 들어왔겠구나... [보이진 않지만 이 부분을 상상해야 합니다]

그럼 지금 rsi 값이 어떤 포인터 변수가 가리키는 주소겠군?? 그리고 +18을 했으니 오프셋은 18일꺼야! 라는게 됩니다.

 

여기까지 생각하는게 어려우면 그냥 저런 mov [rsi+18], eax 같은 명령어를 보면 rsi가 어떤 포인터 변수가 가지는 주소값이고, 오프셋은 18이다 그냥 외워주시면 됩니다.

제가 프로그램 몇개를 분석해봤는데 포인터의 경우 대게 저런 양상으로 나타나더라구요.

 

 

그래서 문제 원점으로 돌아와서 rsi 레지스터에 저장된 값이 어떤 포인터가 가리키는 주소값이란걸 알았습니다.

우리는 지금 포인터 변수를 찾는거였잖아요?

 

저 ptr1이라는 포인터 변수가 어떤 메모리 주소에 위치하고 있는지 알아내야 합니다.

이건 쉽게 찾을 수 있습니다. ptr1이라는 포인터 변수가 어떤 메모리 주소에 저장되고 있는진 모르겠지만 포인터 변수가 저장하는 값이 주소 001574AD0니깐, 치트엔진으로 메모리에 001574AD0 라는 값을 가진 변수들을 찾아내면 그게 바로 포인터 변수가 되겠죠!!

 

HEX를 맞추고 001574AD0 값을 가지는 메모리를 검색해봅니다.

 

그럼 이렇게 3개가 뜨는데요.

일단 여기서 진짜 포인터 변수는 딱 1개라서 나머지 2개는 걸러내야 합니다.

 

일단 위 3개의 값을 다 더블클릭해서 밑에 Address Table에 등록시켜 주시구요..

 

일단 눈치밥으로 밑에 025D로 시작하는 주소가 비슷한 2개 변수는 포인터 변수가 아닐거 같고 014.. 로 시작하는게 맞을 거 같지만 정확한 검증이 필요합니다.

 

일단 지금 문제는 4레벨 포인터 문제로 지금 찾아놓은 포인터 변수 또한 어떤 포인터 변수가 접근하고 있습니다. (포인터의 포인터) 그러므로 이 포인터 변수값에 대한 포인터가 찾아지는게 바로 포인터 일겁니다. (말로 설명하니깐 진짜 햇갈리네요 ㅋㅋㅋ;) Find out what accesses ... 를 눌러서 확인해봅니다.

 

디버거를 돌려서 확인해본 결과 역시 맨 위에 있는게 포인터가 맞네요. Change value를 눌렀을 때 반응오는게 맨 위에 값입니다.

 

그리고 안타깝게도 튜토리얼 프로그램이 디버거를 한꺼번에 띄어놓고 추적하니 튕겨서 다시 실행하니 주소가 이렇게 바뀌어버렸습니다.

양해해주세요 ㅎ; 이제 글쓰는 저까지 햇갈릴 따름입니다.

지금 위에 있는 찾아놓은 값들 주소를 다시 확인해주세요. 

 

밑에 2개는 필요없는 값들이니 삭제합니다.

 

그럼 이렇게 어떤 변수를 가리키는 포인터는 찾은 셈이 됩니다.

 

mov RSI , [??????]

아 참고로 아까 위처럼 RSI 레지스터에 포인터 변수의 값을 넣는 mov 명령어가 있을거라고 예측했었는데 역시 디스어셈블러 올려보니 mov rsi, [rsi] 와 같은 구문이 있네요. 참고로 불러온 [rsi] 에서 rsi에는 포인터 변수의 주소가 들어있답니다. (포인터 변수가 가리키는 주소가 아닌, 포인터 변수 본인의 주소)

 

참고차 알아두세요!

 

일단 첫번째로 찾아놓은 포인터에 대해, 다시 포인터를 찾아야 합니다. (2중 포인터)

2중 포인터를 찾기 위해 다시 what access 탭을 열어서 추적해봅니다.

 

아까와 비슷한 구문이 보입니다. mov가 아니라 cmp긴 한데 어쨌던 간에 mov [메모리 주소], 값 과 같은 형태의 명령어만 찾으면 됩니다. 보다 싶이 [rsi] 에 00을 비교하고 있는데요. 일단은 저 레지스터 rsi에 들어있는 값이 포인터 변수가 가리키는 주소가 될겁니다. 근데 이번엔 +하거나 -하는게 없으므로 Offset은 없어서 0,

 

rsi는 보다 싶이 저희가 찾아놓은 그 포인터 변수의 주소와 일치하네요.

결론적으로 더블 포인터가 존재하며, 지금 저희가 찾아놓은 포인터를 가리키고 있다는 셈이되겠죠.

 

mov [eax±offset], val

아까도 말했듯이 이런 패턴이 나오면 그냥 외워주시면 되는데 

위와 같은 형태로 나오면 eax 레지스터 값이 어떤 포인터 변수가 가리키는 주소 값이고, 뒤에 더하거나 빼는 값이 offset입니다. (없으면 0)

 

New scan을 하고 0015FC2E0을 가리키는 포인터를 또 찾습니다.

 

이번에도 여러가지가 나오는데 저기서 더블 포인터는 0759... 에 저장되어 있는 값입니다.

찾는 과정은 위에서 설명했으니깐 생략합니다.

 

다시 한번, 2중 포인터를 가리키는 포인터, 3중 포인터를 찾습니다.

다시 Find out what access.. 로 찾으면 됩니다.

 

이번엔 또 오프셋이 존재하네요. 으아악 ㅠㅠ rsi 값을 보면 알겠지만 007591920 이라는 주소를 가라키는 포인터 변수가 있고, 여기에 오프셋 +18을 더해서 3중 포인터로써 접근하고 있습니다.

 

이번에도 007591920 라는 값을 가리키는 포인터 변수를 찾고, 더블클릭해서 밑 주소 테이블에 등록해주시면 됩니다.

 

위에서 열심히 했으므로.. 3중 포인터 찾기 생략.

마지막으로 4중 포인터 역시 똑같은 방법으로 찾아주시면 됩니다.

 

마지막 4중 포인터까지 찾으면 이런 모습이 됩니다. 초록색 주소값이라 마지막 4중 포인터는 static 으로 선언되어 있을 거 같고 고정이네요.

 

결론적으론 일단 다 찾았습니다.

아직 치트엔진은 저 위에 값 4개가 포인터라는 사실을 모르므로 저번에 했던것처럼 4중 포인터를 등록시켜주셔야 합니다.

 

Add Address Manually 를 누릅니다.

 

일단 가장 상위의 4중 포인터를 먼저 등록시켜줘야 합니다. 

Pointer에 체크하고 1번엔 4중 포인터가 저장되어 있는 곳의 주소를, 2번엔 4중포인터가 가리킬 곳의 Offset을 적습니다.

찾아놓은 바로는 4중 포인터는 3중 포인터 주소보다 -10 만큼 떨어져서, offset 10만큼을 더해서 접근하고 있었습니다.

 

이렇게 되면 4중 포인터를 이용하여 정확히 3중 포인터의 주소 위치를 가리키게 됩니다.

 

Add Offset 버튼을 눌러서 3중 포인터, 2중포인터, 1중 포인터 순서대로 아까 찾아놨던 오프셋을 입력하면서 포인터가 가리킬 곳을 잘 조정해줍니다.

 

오프셋을 잘 맞춰서 가리키게 해주면 최종적으로 4중 포인터 연결 체인이 만들어져서 최종적으로 2285를 가리키게 됐음을 확인 가능합니다.

 

끝났으면 OK를 누릅니다.

 

4중 포인터 체인이 완성되었고, 이제 가리키는 곳을 5000으로 고정해버리면 끝!

 

튜토리얼로 와서 Change pointer을 누르면 Next가 활성화 되며 문제가 클리어 된 것을 알 수 있습니다.

축하합니다.

 

4중 포인터 흐름도

위 상황을 그림으로 나타내면 다음과 같습니다. (Pointer Chain)

 

 

ptr4 -> ptr3 -> ptr2 (정정 : ptr4의 주소 01614AE0 -> 100325B00)
ptr2 -> ptr1 -> val1

 

ptr4 -> ptr3 ->ptr2 ->ptr1 전체 큰 그림 (정정 : ptr4의 주소 01614AE0 -> 100325B00) (클릭시 확대)

 

4중 포인터 & 오프셋에 의해 값이 접근되는 과정

 

참고로 어셈블리어에서 사용하는 숫자는 일반적으로 16진수로 치트엔진에서 Offset으로 표현해둔 값들은 전부 16진수입니다. 다만 PPT에선 읽기 편하라고 4바이트씩 뺀 주소 값을 10진수로 표시해놨습니다.

 

 

 

 

 

 

여담 & 팁

글을 다 쓰고 났는데 복잡한 이론들을 설명하다보니깐 글의 난이도가 배로 올라간 거 같아서 아쉽습니다.

일단 이 부분은 개인적으로 치트엔진 튜토리얼 문제중에 가장 어려운 문제라고 생각됩니다. 방법을 그냥 외워서 하면 쉽긴 한데 제 글 목적 자체가 전공자로써 깊은 이해다보니깐 글이 굉장히 길어졌네요.

 

사실 저도 글쓰면서 햇갈리고 포인터 찾을때마다 햇갈립니다 ㅠㅠ

그러므로 이 부분은 여러번 읽어보시고 반복해보세요. 참고로 여기서 포인터 찾는법이 저번 #6번보다 훨씬 정확한 방법입니다. 모르겠으면 그냥 외우는것도 좋은 방법인 거 같습니다 ㅎㅎ; 저도 자꾸 햇갈려서 모르겠으면 그냥 외워라.. 로 해야 할지 고민중이네요 6^;;

 

그럼 다음 9편에서 봅시다. Bye~~

SNS 공유하기
네이버밴드
카카오톡
페이스북
X(트위터)

최근글
인기글
이모티콘창 닫기
울음
안녕
감사
당황
피폐