security/리버싱 - CodeEngn.com

Basic RCE L16~L20

민사민서 2023. 4. 16. 21:39

L16. Serial 값 구하기 (Name = "CodeEngn" 일 때)

콘솔 프로그램이네. 
"Enter your Name:" 출력, 이름 입력 시 "Enter your Password:" 출력, 틀리면 "Wrong password!"

IDA 디컴파일 기능을 이용해 분석해보면 if(v12==v13) 결과에 따라 성공/실패 분기가 나뉜다
* v12는 password로 입력받은 값이 그대로 들어가고 (cin 으로 &v12에 바로 저장)
* v13은 name으로 입력받은 값 가지고 조작해서 생성함 (v14)

bp 걸어놓고 스택 상태 확인해보자
int v12 = [ebp-0x40] // password로 입력한 숫자가 들어가네 ("1234" => 4D2
unsigned int v13 = [ebp-0x3C] // 조작된 값이 들어가네 ("CodeEngn" => E4C60D97)

즉 "CodeEngn"에 해당하는 시리얼 값은 v12에 0xE4C60D97이 들어가도록 하는 정수값

 

try 1 )  signed int 고려 음수값 넣기
v12는 4byte signed int이므로 -(0x100000000 - 0xE4C60D97) = - 0x1B39F269 = -456782441

음수 시도해보니 안되네? 음수를 입력 못받게 되어있나보다
=> v12에 msvcrt.dll:msvcrt_strerror_s+0x4A0 주솟값이 담긴다


try 2 ) signed int에서 overflow 시도해볼까?

양수 최댓값 0x7FFFFFFF + (0xFFFFFFFF - 0xE4C60D97 + 1) = 0x9B39F268 = 2604266088

=> 0x9B39F268 잘 들어가잖아? signed 여부랑 상관없이 4바이트에 그대로 저장되나보다


final try ) signed int 고려 안하고 그냥 입력하면 되겠네
=> 그냥 3838184855(= 0xE4C60D97) 입력

L17. 특정 key 값을 생성하는 Name 구하기

패킹되어있지 않음. Name/Key 입력하고 "Check it!" 버튼 눌러 확인하는 식.
올바른 답 넣지 않으면 버튼 눌러도 반응 없다

Search for text 하다보니 마지막 쯤 의심스러운 문자열들 존재

"CALL 00404C3C" 호출 결과 ZF에 따라 성공/실패 분기 나눠짐
404C3C 함수 호출 전에 EAX에 사용자 입력 key값, EDX에 생성된 key값 들어감 => 두 값을 strcmp 한다고 유추 가능

 

Ollydbg로 key 생성 루틴을 살펴보자
- 0043A074 함수 호출을 통해 EDX로 넘겨준 위치에 Name 문자열 주소를 받아오고
Name 길이가 3이상 30 이하인지 확인하고
- 0043A074 함수 호출을 통해 EDX로 넘겨준 위치에 Key 문자열 주소를 받아오고
- EAX에 Name 문자열 주소를 담아 0045B850 함수를 호출하여 key를 생성한다. key 주소는 EBP-0x14에 저장됨

 

0045B850 함수 분석하면 끝. 너무 복잡하니 IDA로~

[PART1] v4 = name의 길이 입력
do-while문 v4번 만큼 반복, esi(v6/v3/v2) 조작

[PART2] v7 = name의 길이 입력
do-while문 v7번 반복, v29 조작

[PART3] v10 = name의 길이 입력
do-while문 v10번 반복, edi(v8), eax(v9) 조작

[PART4] v12 = name의 길이 입력
do-while문 v12번 반복, ebx(v11/v13/v14) 조작

[PART5] sub_4086C8 , sub_404D0C 함수 번갈아가며 호출 (위에서 계산한 값으로 serial 생성하는구나 추측)

 

각 함수들을 분석하기는 힘드므로, serial을 구성하는 요소들을 어디서 가져오는지 거꾸로 확인하자.

생성된 serial
[PART1~5] 결과 메모리에 생성된 serial의 일부분들

Name="min"일때 최종적으로 생성된 serial과 지역변수들에 담겨있는 주소의 덤프값을 비교했더니
serial (421F - CFD1 - D8B7E74D - AC5F - 9143) 은 다음과 같이 구성됨

[EBP-0x1C(v26)이 가리키는 문자열] - [EBP-0x24(v24)이 가리키는 문자열] - [EBP-0x2C(v22)이 가리키는 문자열] - [EBP-0x30(v21)이 가리키는 문자열] - [EBP-0x38(v19)이 가리키는 문자열] 

 

그리고 do-while문에서 계산한 결과는 다음과 같음
v6 = v3 = v2 = 421F7DE0
v29 = CFD19A0
v8 = AC5FAD9E
v9 = F65304A8
v13 = v14 = v11 = 09143F69

 

정리하자면
v26이 가리키는 주소에 v3 값의 상위 4자릿수를 넣고
v24가 가리키는 주소에 v29 값의 상위 4자릿수를 넣고
v22가 가리키는 주소에 ???
v21이 가리키는 주소에 v8 값의 상위 4자릿수를 넣고
v19가 가리키는 주소에 v11 값의 상위 4자릿수를 넣어서 serial을 생성한다!

BEDA-2F56-BC4F4368-8A71-870B 가 key려면??
아스키코드 값 48~123 (숫자 + 알파벳 대소문자 포함하는 범위) 에서

v3=BEDA~ , v29=2F56~ , v8=8A71~ , v11=870B ~ 만족하는

ascii = 70 일 때밖에 없다, 대문자 F. => F의 MD5 해시값 입력하면 된다~

// 파이썬 코드 짜서 ASCII 범위 내에서 func~func4 돌려보았다.

def func(x): # v3
    v6 = 1906*x
    return 2280*(v6*v6+v6)
    
def func2(x): # v29
    return 605316*(x+12)

def func3(x): # v8
    return 33682*(x+4240)

def func4(x): # v11
    v13 = 2*x
    v14 = x+883*((v13*v13*v13)^16 | 0x44) + 1091
    return v14*v14
    
for x in range(48,123):
    print(str(x)+" : "+str(hex(func4(x)))[-8:])

// 생각해보니 Name Length 필터링 코드를 패치하여 브루트 포스해도 됐겠네. 분기문에 bp 걸어놓고 생성된 결과값만 확인하면 되니까 (경우의 수도 ASCII 범위로 적고)

JGE, JLE를 JMP로 변경하여 Name Length filtering을 무력화시켰다
Name에 F만 입력했더니 다음과 같이 serial 생성되었다 ㅋㅋ

L18. Name="CodeEngn"일 때 Serial 값

Detect It Easy에 던져보니 ARM Protector 적용되어있음 (프로텍터 - 안티 리버싱 기법 범벅?!!)

Name/Serial 입력하고 틀리면 "You serial is Wrong, try again" 메시지박스 뜸
최소 이름 5자 이상 입력 필요.

GetDlgItemTextA로 user input을 가져와서 00407EF0에 저장한 뒤
004080F0의 serial 값과 lstrcmpiA 비교해서 같으면 성공 / 다르면 실패임

분기문에만 bp걸고 달리면 문제없이 분석 가능하다.
Name="CodeEngn"일 때 004080F0 버퍼에 저장되는 값은 06162370056B6AC0

ARM protector가 그렇게 강력한 anti-debugging feature를 가지고 있지는 않는것같네

L19. 프로그램 종료 지연 시간 구하기 (안티 디버깅 + 멀티쓰레드)

Detect It Easy에 던져보니 UPX 패커로 패킹되어있음
실행하면 "CodeEngn.com by Lee Kang-Seok" 메시지박스 뜨고
- 확인 누르면 바로 종료되는데
- 확인 안누르면 일정 시간 지나고 종료되네

 

이 코드가 실행된다

언패킹하고 ollydbg로 이동해 F9(실행) 시키니까
"This is a compiled Autoit script. AV researchers please email avsupport@autoitscript.com for support" 메시지박스 뜸
디버깅 중일때 일반 실행과는 다른 액션 보인다 - 안티 디버깅!!!

 

Step1. 안티 디버깅 기법 무효화

Search for text하니 "CodeEngn" 문자열 없음.
하지만 MessageBoxA / MessageBoxW API들은 많이 보임 -> 문자열 복호화되면서 MessageBox 호출되나 싶음
MessageBoxW API 호출부에 전부 bp 걸어놓자

"This is a compiled Autoit script" 메시지박스 호출부에 bp 걸어놓고 
Ctrl+F8 tracing을 통해 해당 메시지박스 호출될때까지의 코드 흐름을 살펴보고자 함
메시지박스 호출 직전(bp 걸리기전) 19_unpacked 모듈의 코드 흐름에서 F12 눌러서 멈추고 step over하면서 살펴보자

19_unpacked 모듈 상의 코드. 여기 Stepover하면 안티 디버깅 메시지박스 호출부가 실행된다

"CALL 0040E940"에서 Step over하면 메시지박스 호출코드 bp에 걸림.

Kernel32!IsDebuggerPresent 리턴값이 0이 아니면 안티디버깅 메시지박스 호출부로 JMP

0040E940 함수 내부를 살펴보니 Kernel32.IsDebuggerPresent API 호출!

=> 디버깅 중이면 코드 흐름 바꿔서 이상한 메시지박스 호출해버린다

일반 실행 시 떴던 메시지박스 형태

EAX=0으로 조작 or ZF=1로 조작 후 F9 하면
우리가 알던 format의 MessageBoxW 호출부에서 bp 걸리면서 멈춤

Step2. Timeout 어떻게 거는지 파악

후보 1) MessageBoxTimeoutW (Undocumented API in USER32.dll)

MessageBoxW 타고 들어가니(step in) 내부적으로 MessageBoxTimeoutW API 호출하는 것을 볼 수 있었음

Undocumented API... 구글에 찾아보니 가장 마지막(6번째) 인자가 Timeout 값이라고 한다.
6번째 인자 dwMilliseconds 값으로 0xFFFFFFFF milliseconds(-1)이 들어가는 상황. 아닌 것 같다

후보 2) MessageBox가 생성되는 스레드 상에서 timeout 세팅?

MessageBox 호출부 위에 CurrentThreadID 리턴값, 00444C3A 주소(PUSH 00444C3A) 등을 인자로 넣고 004162C6을 호출하는 코드 존재
004162C6 내부에서는 CreateThread 통해 스레드 생성

 

IDA로 분석해보자 =>  MessageBoxW 호출부 근처

_beginthreadex를 통해 스레드를 생성하고 MessageBoxW API를 호출한다
* 3번째 인자 00444C3A(StartAddress, 새 스레드의 실행을 시작하는 루틴의 시작 주소 = ThreadProc 느낌?)
* 4번째 인자 arglist 포인터

IDA로 분석해보자 =>  00444C3A(StartAddress) 함수를 분석해보자

두번의 timeGetTime() 호출을 통해 elapsed time을 구하고, a1[1]보다 커질 때까지 sleep(10ms) 하면서 loop를 돈다

EnumThreadWindows 함수를 통해 hDlg에 Messagebox window 핸들을 담고
FindWindowExW 함수를 통해 OK button window를 찾아 BM_CLICK(0xF5u) 메시지를 보내 ok 버튼을 클릭한 효과를 줌

 

00444C3A에 bp 걸고 계속 실행하면 MessageBoxW API 호출 후 00444C3A에서 멈춘다

argument로 전달된 배열의 버퍼 확인해보면 두 번째 원소 값 0x2B70 = 11120
=> 11120 ms 동안 기다리다가 프로그램 종료

cf) 만약 a1[1]의 값이 10의 배수가 아니었다면 올림해야 정답이다 (10ms씩 sleep 하므로)

 

cf) _beginthreadex 함수

 

uintptr_t _beginthreadex( // NATIVE CODE
   void *security,
   unsigned stack_size,
   unsigned ( __stdcall *start_address )( void * ),
   void *arglist,
   unsigned initflag,
   unsigned *thrdaddr
);

- c/c++ Runtime Library에서 제공하는 함수이다.

- 쓰레드가 실행 완료된후 내부적으로 CloseHandle()을 호출하지 않기 때문에 사용자가 명시적으로 CloseHandle()을 호출해주어야 한다.
- 내부적으로 _exitThreadex()함수를 호출하여 메모리 정리까지 해준다.(멀티 쓰레드 환경에 적합하다)
- return값이 unsigned int형이므로 (HANDLE)형으로 캐스팅해주면 Win32 Thread관련 함수들도 사용할 수 있다

 

cf) CreateThread 함수

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  SIZE_T dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID lpParameter,
  DWORD dwCreationFlags,
  LPDWORD lpThreadId
);

- CreateThread는 c/c++ Runtime Library에서 제공하는 함수가 아닌, WinApi 전용함수
- 왠만하면 쓰레드 생성시에 CreateThread는 안쓰는게 좋다
- 쓰레드 구문 내부를 C/C++를 사용해 작성하는 경우 (c/c++ 런타임 함수를 사용할 경우) C/C++ 런타임 라이브러리에서 제공하는 _begin-threadex 함수를 사용해야 한다

 

cf) 스레드 종료 여부 파악?

GetExitCodeThread(m_Thread, &dwExitCode);
 while ( dwExitCode== STILL_ACTIVE)
 {
   Sleep(100);
   GetExitCodeThread(m_Thread, &dwExitCode);
 }

 

쓰레드의 종료 여부를 판단하기 위해 GetExitCodeThread 를 쓰게되면 무한 루프를 돌면서 종료 기다리

WaitForSingleObject(m_Thread, INFINITE);

 

따라서 GetExitCodeThread 보다는 WaitForSingleObject 를 주로 많이 사용합니다.

cf) 멀티스레드

스레드(thread)는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다. 일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다. 이러한 실행 방식을 멀티스레드(multithread)라고 한다.

https://ledpear.tistory.com/122
 

 

Window via C/C++ 6장 - 스레드의 기본

0. 개요 모든 프로세스는 적어도 하나 이상의 스레드를 사용한다 운영체제가 스레드를 다루기 위해 사용하는 스레드 커널 오브젝트, 스레드 커널 오브젝트는 시스템이 스레드에 대한 통계 정보

ledpear.tistory.com

cf) 19_unpacked 메시지박스 호출부 분석

_beginthreadex를 이용해 메시지박스를 호출하는 main thread 외의 thread를 하나 더 생성한다

새 스레드는 StartAddress(0x00444C3A)를 시작 주소로 하여 실행을 시작한다.

새 스레드는 11120ms 동안 while문을 돌며 기다리다가, 메인 스레드의 Messagebox 윈도우를 찾아 OK 버튼을 누르는 동작을 진행한다. 이 때 메인 스레드의 MessageBoxW API 호출이 종료되며 EAX=1이 리턴된다.

메인스레드는 새로 생성된 스레드가 종료될 때까지 기다려주고(WaitForSingleObject(hObject, WAIT_FOREVER)), CloseHandle()을 통해 해당 스레드를 종료한다

L20. 특정 결과 문자열을 출력하기 위한 파일 생성하기

Detect It easy로 분석해보니 패킹 x, 근데 L19마냥 "overlay: binary" 라는 게 있네
=> 또 안티디버깅 기법 있을 수 있겠구만 (근데 없었음 ㅋㅋㅋ)

CreateFileA 호출을 통해 "CRACKME3.KEY" 파일의 핸들 가져오고 ReadFile 호출을 통해 402008 버퍼에 내용 읽어옴
- 버퍼 길이 0x12랑 다르면 "CrackMe v3.0 - Uncracked" 문자열로 004010AE~ 부터 진행
- 버퍼 길이 0x12면 0040106F ~ 004010AB 동안 조작 진행

* CALL 401311 에서 402008 버퍼 조작 + 4020F9에 값 저장 + 402149에 loop 횟수 저장

* CALL 40133C 에서 402016부터 4바이트 값 리턴 (버퍼 15~18번째 값)

* 4020F9에 들어있는 값(과 0x12345678을 xor한 값이지 실제론)과 버퍼의 마지막 4바이트가 같아야지 성공

* 성공한 경우 CALL 00401362를 통해 성공 MessageBox 호출

ollydbg가 IDA 디컴파일보다 이해 쉬움 ㅋㅋ

* 버퍼 조작 루틴 자세히 분석

- ECX, EAX 초기화, BL=0x41('A')로 초기화

- BL 값을 증가시켜가며 반복문을 최대 15회 진행하며 (BL=0x41 ~ 0x4F) 

- 버퍼 한 바이트와 BL을 xor한 결과를 다시 버퍼에 저장. xor한 결과를 4020F9에 계속 합산한다

- 중간에 xor한 결과가 0이면 반복문 탈출. 이 때 반복문 진행한 횟수(CL)을 402149에 저장한다

 

이건 IDA가 더 이해하기 쉬움 ㅋㅋ

* 성공 MessageBox 호출 루틴 자세히 분석

- a1 = 파일 내용 버퍼, a2 = "Cracked by:   " 문자열 주소, a3 = "Now try the next crackme!" 문자열 주소

- 첫번째 qmemcpy: a2 문자열에 ECX(402149 값)만큼 파일 내용 버퍼에서 복사하여 추가

- *v4 = 3361 을 통해 a2 문자열에 " !"(\x0D\x21) 추가

- 두번째 qmemcpy: a2 문자열 뒤에 a3 문자열 덧붙임

=> 결과적으로 "Cracked by: (이름) ! Now try the next crackme!" 문자열이 생성되어 MessageBoxA 인자로 들어감

즉 (이름) 부분에 "CodeEngn" 문자열 들어가려면
- 402149에 8이 저장되어야 하며

- 버퍼 조작 결과 처음 8바이트가 "CodeEngn"이어야 하며

- 마지막 4바이트가 DWORD PTR:[4020F9] xor 0x12345678과 같아야 한다

 

// xor 2번하면 원래 값이라는 점에서 착안해 파이썬 코드 짜 봄

result = "CodeEngn"
org_buffer = []

for i in range(8): # xor 결과 result 문자열
    org_buffer += [hex(ord(result[i])^(65+i))]
org_buffer += [hex(65+8)] # xor 결과 0, loop 탈출
    
print(org_buffer)


버퍼는 ['0x2', '0x2d', '0x27', '0x21', '0x0', '0x28', '0x20', '0x26', '0x49'] 로 시작

=> 이렇게 조작하면 4020F9에는 0x3003 저장, 402149에는 0x8 저장~~

0x3003 xor 0x12345678과 = 0x1234557B

=> 버퍼 마지막 4바이트 [0x7B, 0x55, 0x34, 0x12] 저장하면 됨


=> 가운데 5바이트는 어케 하든지 상관 x!! (근데 정답은 전부 \x00 으로 채워야만 인증되네ㅋㅋ)
=> 022D2721002820264900000000007B553412

 

cf) what is overlay?

An overlay (aka extra-data) is simply data appended to the physical image of a Portable Executable.

Take a PE, append stuff to the end without adjusting the header, and it has an overlay.

This data area is not defined as part of the image by the PE header and therefore isn't part of the virtual image of the loaded PE.
The presence of an overlay is not necessarily malicious or suspicious, but it could be an indicator that the PE file has been modified, packed, or protected.

 

'security > 리버싱 - CodeEngn.com' 카테고리의 다른 글

Advance RCE L01 ~ L07  (0) 2023.04.20
Basic RCE L11~L15  (0) 2023.04.13
BASIC RCE L06~L10  (0) 2023.04.11
BASIC RCE L01~L05  (0) 2023.04.09