security/리버싱 - CodeEngn.com

Basic RCE L11~L15

민사민서 2023. 4. 13. 15:36

L11. OEP + Stolen Bytes

HxD로 까서보니 UPX 패킹되어있네
언패킹해보았더니 EP=4071F0 에서 OEP=401000으로 바뀌었다

언패킹 된 파일을 실행해보았더니 문자열 깨진 MessageBox 뜨더라 - Stolen byte 때문?

11.exe를 ollydbg로 열어보니 0x401000부터 0x40100B까지가 NOP로 채워져있음. Stolen byte

automatic unpack 옵션 체크해제하고 다시 연 다음 POPAD로 간다. PUSH 명령어 3개 발견가능
=> stolen bytes: 6A0068002040006812204000

L12. Key 값 + 코드 패치 (RVA to RAW)

Detect It Easy에 던져보니 패킹 안 된 순수한 파일
serial key 입력하고 check버튼/about버튼/quit버튼 클릭 가능

Ollydbg로 열어보니 EP에 바로 메인함수 등장
DialogBoxParamA API를 호출하는데 0x00401029 함수를 DialogProc으로 세팅
DialogBox에 각종 이벤트 발생할 때마다 콜백함수 401029 호출된다 => DialogProc 분석하면 됨

00401082의 "JNE SHORT 00401098"이 성공/실패 분기이다.

00401063~0040107B (반복 loop)에서 Serial을 생성하여 EAX에 저장하고, 그 값이 0x7A2896BF와 같으면 성공!!

IDA로 분석한 결과이다. GetDlgItemInt로 user가 입력한 key값을 가져와 반복적으로 sub_40110F 함수를 호출하며 serial을 생성한 뒤, 2049480383(=0x7A2896BF)과 비교하여 성공 여부를 판단한다

 

1) 올바른 serial 값을 생성하도록 하는 key를 구하자

serial = GetDlgItemInt(hDlg, 3001, 0, 0); // get user input (왼쪽부터 숫자 나올때까지 인식)
for( i = 0x403000; i<0x403528; i+=4) {
	serial = func(serial, *(DWORD *)i);
}
DWORD func(DWORD a, DWORD b) {
	return (a & -(b+1)) + (b & -(a+1));
}

 

신기하게도 이 과정을 거친 최종 serial 값은 GetDlgItemInt() 리턴값과 동일하다
// 연산 결과 f(x)=x가 되도록 0x403000~0x403527 버퍼가 세팅되어있는듯하다
=> 즉 우리가 입력해야하는 key는 2049480383 (=0x7A2896BF) 이다.
cf) 그 뒤에 아무런 문자열(+숫자) 붙여도 됨 (ex. 2049480383MINSEO25)


2) 성공메시지 대신 key값 출력하기 위해 패치해야하는 RAW 범위

성공 MessageBoxA의 두번째 parameter(Text)로 들어가는 문자열의 주솟값은 0040353B이다
=> RVA = 353B (.data 섹션, Image Base = 0x400000)

=> RAW = 353B(RVA) - 3000(Virtual Address) + 800(Pointer to RAW) = D3B

HxD에서 직접 열어 교차 검증 해보았다.

=> 즉 0xD3B ~ 0xD61에 있는 기존 문자열에서 
=> 0xD3B~0xD45를 key 10자리 + *널바이트* 해서 11바이트만큼 overwrite하면 된다!

 

cf) DialogBoxParamA API

INT_PTR DialogBoxParamA(
  HINSTANCE hInstance,       // 애플리케이션 인스턴스 핸들
  LPCSTR    lpTemplateName,  // 대화 상자 리소스 식별자 또는 이름
  HWND      hWndParent,      // 부모 창의 핸들
  DLGPROC   lpDialogFunc,    // 대화 상자 프로시저의 주소
  LPARAM    dwInitParam      // 응용 프로그램 정의 값
);

 

DialogBoxParamA API는 Windows 애플리케이션에서 대화 상자를 생성하고 관리하기 위한 기능
대화 상자가 나타나기 전에 DialogProc 실행
이 프로시저는 대화 상자의 이벤트와 메시지를 처리하며, 프로시저 내에서 사용자 정의 코드를 실행

cf) DialogProc

INT_PTR CALLBACK DialogProc(
  HWND   hwndDlg,  // 대화 상자의 핸들
  UINT   uMsg,     // 메시지
  WPARAM wParam,   // 추가 메시지 정보
  LPARAM lParam    // 추가 메시지 정보
);

 

대화 상자가 표시되기 전에 DialogProc가 실행, 초기화 작업을 수행하거나 컨트롤의 상태를 설정
사용자 인터페이스와 상호 작용할 수 있으며, 이벤트 및 메시지 처리를 사용자 정의 코드로 구현

cf) GetDlgItemInt 함수

UINT GetDlgItemInt(
  [in]            HWND hDlg,
  [in]            int  nIDDlgItem,
  [out, optional] BOOL *lpTranslated,
  [in]            BOOL bSigned
);

 

대화 상자에서 지정된 컨트롤(nlDDlgItem 식별자 이용)의 텍스트를 정수 값으로 변환

L13. .NET Assembly 프로그램 분석 

콘솔 프로그램. "Please enter the password:" 하고 대기하다가 실패하면 "Bad Luck! Try again!" 출력

* Ollydbg로 열어보려하니 Unable to start file 경고 메시지가 뜨네
* IDA로 열려고 하니 Microsoft.Net assembly라고 하네. 근데 해당 버전에서는 분석 못한다고 하네

.NET Framework로 개발된 프로그램은 dnSpy, ILSpy, dotPeek 등의 디컴파일러 툴에 의해 소스코드를 쉽게 확인 가능

dnSpy에서 open해보니 소스코드 전부 확인 가능

 

Main함수에서 레인달 알고리즘을 이용한 RijindaelSimpel.Decrypt() 복호화 함수 호출을 통해 cipherText를 복호화 한 결과를 text에 저장한다. 이것이 password이다.

line 22에 bp 걸고 달리면 이미 암호화 복호화가 다 끝나고 text에 답이 저장되어 있을 것.
=> "Leteminman"


cf) .Net Framework란?

https://luckygg.tistory.com/216

 

[.Net] .Net Framework와 디컴파일러(Decompiler) ILSpy 및 dotPeek 예제

디컴파일러(Decompiler) 혹시 디컴파일러에 대해 알고 계신가요? 저는 디컴파일러는 아마도 리버스 엔지니어링과 관련된 내용이지 않을까 생각합니다. 디컴파일러는 말 그대로 역으로 프로그램을

luckygg.tistory.com

- Windows에서 응용 프로그램을 빌드하고 실행하기 위한 소프트웨어 개발 프레임워크
- .Net 어플리케이션은 C#, F# 또는 Visual Basic 프로그래밍 언어로 작성
- 코드는 언어에 구애받지 않는 Common Intermediate Language(CIL)로 컴파일
- 컴파일된 코드는 .dll 또는 .exe 파일 확장명을 가진 어셈블리에 저장
- 어플리케이션이 실행될 때 CLR(Common Language Runtime(CLR): 실행중인 어플리케이션을 처리하는 실행 엔진)은 어셈블리를 가져와서 just-in-time(JIT) 컴파일러를 사용하여 실행 중인 컴퓨터의 특정 아키텍처에서 실행할 수 있는 기계어 코드로 변환
- 전용 디컴파일러에 의해 소스코드 노출이 쉬움Native C++로 구현하거나 Wrapper Class 이용해 보호


cf) 레인달 알고리즘 이용 암호화 / 복호화 함수

L14. 이름에 따른 Serial 값

Detect it easy에 던져보니 UPX packed 되어있네 unpack하자
OEP부터 NOP 없이 바로 코드 진행되는거보니 stolen bytes는 없네

name/serial 입력 후 check 버튼 누르는 crackme. 실패 시 "You Have Enter A Wrong Serial, Please Try Again"

0040133C에서 성공/실패 분기 생성됨, 00401383 함수의 리턴값과 ESI 값 비교

 

(코드에 00403038, 00403138 주소가 하드코딩되어있어서 알기 쉬움)

ESI = 0 초기화 후, 403038 주소의 데이터 가지고 반복문을 돌면서 ESI 값 세팅

403038에는 user name 저장되고 -> ESI 값은 user input name에 의해 바뀐다 (분기문 직전 코드에서 조작)

IDA로 분석. 인자 lpString(0x403138)을 가지고 조작하여 리턴값 생성

403138에는 user serial 저장된다 -> EAX 값은 user input serial에 의해 바뀐다 (401383 함수의 리턴값)

* Name = CodeEngn일 때 ESI 값은 0x129A1 = 76193
* 5개의 숫자로 되어있는 정답을 찾으면 된다, 인자값 lpString=403138로 고정되어있다


401383 함수를 IDA로 분석한 결과를 좀 다듬어보았다.

int __stdcall sub_401383(LPCSTR lpString) {
  int sum = 0;
  int len = lstrlenA(lpString);
  LPCSTR pStr = lpString;
  int val;
  
  do {
    val = *(unsigned __int8 *)pStr++;
    val -= 48;
    for (int i = len - 1; i; --i )
      val *= 10;
    sum += val;
    len--;
  } while ( len >= 1 );

  return sum;
}

 

[숫자 5자리]
'0':48 ~ '9':57 이므로 각 문자의 아스키값에 48을 뺀 값은 0~9이다.
첫번째 자리는 10^4, 두번째 자리는 10^3, ... 다섯번째자리는 10^0을 곱하므로
결과적으로 입력한 정수 문자열에 해당하는 정수값이 리턴된다 ㅋㅋㅋ ("12345" => EAX=12345)

=> serial 값 76193


굳이 싶지만? bruteforce 코드를 짜볼까

def check(nums):
    sum = 0
    for i in range(5):
        sum += (nums[i]-48)*pow(10,(4-i))
    return (sum==76193)

def bruteforce():
    for i1 in range(48,58):
        for i2 in range(48,58):
           for i3 in range(48,58):
                for i4 in range(48,58):
                    for i5 in range(48,58):
                        nums = [i1,i2,i3,i4,i5]
                        if check(nums):
                            print("Found : "+str(nums))
                            return 1
    return 0
    
if not bruteforce():
    print("Fails to find the answer")

 

=> Found : [55, 54, 49, 57, 51]

L15. 이름에 따른 Serial 값

Detect It Easy에 던져보니 패킹되어있지는 않네
Name/Serial 입력 후 check해야 하는데
1) Serial에 문자열 입력 시 " 'serial' is not a valid integer value " 라고 메시지박스 뜸
2) 잘못된 값 입력 시 "Try Again !" 메시지박스 뜸

Search for referenced text strings 하면 맨 아래에 의심 문자열들이 다수 존재

main함수는 458800부터~

CALL 00458760 결과 EDX에 Name 문자열 주솟값이 들어가고 + 0045B844에 특정 값을 저장한다

(0045B844에 저장되는 값은 성공/실패 분기문에서 비교 대상이 되는 값임)
CALL 004255C0 결과 [EBP-4]에 Serial 문자열 주솟값이 들어간다

0x00458829에서 EAX에 입력한 serial 문자열의 주솟값을 저장하고 CALL 00407774 하면

serial로 문자 입력한 경우 타고 들어가다보면 이런 RaiseException() 함수 만남 ㅋㅋ

- serial이 문자인 경우 exception 발생한다. Shift+F7/F8/F9로 디버기에게 Exception 넘겨주면 디버기의 SEH에 의해 처리되면서 메시지박스 출력된다
- serial이 숫자인 경우 정수 문자열을 hex 값으로 바꿔 EAX에 저장하여 리턴함 ("1234" => 4D2)

분기문에서 EAX와 0x0045B844 (하드코딩되어있음)의 4바이트 값을 비교해 성공/실패 결정

=> Name에 의해 4바이트 값 결정됨!!
Name="minseo"일때는 DWORD PTR:[45B844] = 0x5220

Name="CodeEngn"일때는 DWORD PTR:[45B844] = 0x6160 = 24928

=> CodeEngn에 해당하는 Serial 값은 24928이다.
=> 각각의 함수들을 분석하지 않아도 메인함수 흐름 상에서 트레이싱하며 레지스터 / 스택 / 메모리 상태를 보고 분석 가능


<추가 분석할 것 - IDA 디컴파일 기능 이용>
1. 00458760 함수

1-1) 하드코딩된 주소인 45B844에 어떤 방식으로 serial 값 저장하는지 파악 가능

1-2) EDX에 Name 주소 저장하는 방식 확인 가능

do-while문과 그 아래 두 개의 조작문(line 28-29)에 의해 serial 값이 생성되네
sub_4037E8(&v4) 함수에 의해 EDX에 name 주소값 저장되네

v4에 담긴 name 주솟값에 따라 45B840이 결정되고 (함수 sub_40383C)
45B840은 반복 횟수인 v0에 영향을 미치고 또 45B844 연산 과정에서도 영향을 미친다

=> 즉 Name에 따라 dword_45B844에 저장되는 값이 달라진다~~


2. 00407774 함수 (EAX에 serial 문자열 주솟값)

2-1) 정수 문자열을 어떻게 hex로 바꿔서 리턴하는지 확인 가능

sub_402974(a1,&v9) 함수에서 정수 문자열을 hex로 바꾼 결과 리턴
(00407774 함수와 00402974 함수의 첫번째 parameter인 a1은 "serial 문자열 주소" 나타냄)

00402974 함수 내부:
result를 0으로 초기화해 변환한 정수값을 담을 공간을 마련한다
do-while문을 통해 serial 문자열 앞의 padding을 없애주고 (space[0x20] 발견될때마다 *v2++)
switch-case문에서 +/- 부호 기호를 파악하여 최종 결과에 -1을 곱할지 말지 결정

while문을 돌면서 NULL 바이트 만날때까지 "ascii값-48"한 값을 더하고 result에 10배 곱하여 결과값 생성 후 리턴

2-2) 숫자 문자열인지 어떻게 탐지해 Exception 발생시키는지 확인 가능

숫자가 아닌 문자열 입력 시 402974 함수 직후의 if문이 실행된다

sub_40746C 함수 내부에서 sub_4032E0 함수를 호출하는데, sub_4032E0 내부에서 RaiseException() 함수 호출

 

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

Advance RCE L01 ~ L07  (0) 2023.04.20
Basic RCE L16~L20  (0) 2023.04.16
BASIC RCE L06~L10  (0) 2023.04.11
BASIC RCE L01~L05  (0) 2023.04.09