리버싱

Simcurity·2023년 5월 7일
0

리버싱

목록 보기
2/9

1. 어셈블리어

프로그래머가 고급언어로 코드를 작성하여 실행시키면 그 언어들은 컴파일을 거쳐 기계어로 변환됩니다. 그리고 그 기계어는 운영체제에서 이해할 수 있게 되어 메모리에서 작업을 실행합니다.
컴파일을 해주는 것을 컴파일러라 부르고 일종의 통역사라고 인식할 수 있습니다.
어셈블리어란 고급언어에서 컴퓨터가 이해할 수 있는 저급언어로 변환된 기계어를 사람이 알아볼 수 있도록 만든 일종의 매크로 모음입니다. 어셈블리어와 기계어는 1:1로 매칭됩니다.

1-1) 예시

2개의 인자
ADD EAX, EBX

가장 많이 볼 대표적인 ADD명령입니다.
명령어의 흐름은 오른쪽에서 왼쪽으로 흐릅니다.
즉, EAX값과 EBX값을 더해 EAX에 저장시키라는 명령입니다.

1개의 인자
INC ESI

이 명령은 인자로 오는 ESI의 값을 1증가시키라는 명령입니다.

1개의 인자
PUSH 0

이 명령은 스택의 맨 위에 '00000000'을 입력하라는 명령입니다.

인자가 없음
RETN

이 명령은 스택의 맨 위에 있는 값을 EIP 레지스터에 저장하는 역할을 합니다.

2. 리버싱 기초 예제 abex crackme

2-1) abex crackme(1) 구조

프로그램 실행 시

무엇을 해야하는지 명확하게 알려줍니다.
---> HD가 CD-Rom으로 생각하게 만들라는 것입니다.

PEview로 열어 IMAGE_OPTIONAL_HEADER영역에 Address of Entry Point항목을 보면 00001000으로 되어있습니다. 이는 메모리에 저장될 때 상대 주소가 1000이라는 의미이고 밑에 Image Base를 보면 00400000으로 되어있습니다.
즉, Image Base는 가상 메모리의 실제 주소위치이고 Address of Entry Point는 상대적 위치이므로 00401000에 엔트리 포인트가 있음을 알 수 있습니다.

올리디버거로 파일을 열자 엔트리 포인트에서 브레이크 포인트가 잡혔음을 알 수 있습니다.

현재 이 곳은 코드 영역입니다.
코드 영역의 데이터와 일치하는걸 볼 수 있습니다.

2-2) 스텝 인투, 스텝 오버

이제 스텝 오버(F8), 스텝 인투(F7)를 사용해 한줄 씩 실행해보겠습니다.
둘 다 한줄 씩 실행하지만 스텝 인투는 call을 실행하는 명령어에서 주소의 서브루틴으로 들어간다는 차이점이 있습니다.
스텝 인투를 많이 사용하면 프로그램의 동작을 제대로 파악할 수 없고 스텝 인투를 많이 사용하면 서브루틴의 반복속에서 자신이 어디까지 분석했는지 잃어버릴 수 있습니다. 그러므로 적절히 사용하여야 합니다.
보통 USER32, KERNEL32와 같은 시스템 DLL은 스텝 인투로 분석하지 않습니다.

2-3) 브레이크 포인트

방금 처럼 0040100E 지점에서 스텝 인투를 정확히 하려면 그 곳까지 가야하는데 큰 규모의 프로그램의 경우 시간이 너무 오래 걸리므로 미리 그 부분에 브레이크 포인트를 걸고 실행을 시키면 자동으로 그 곳에서 멈추게 됩니다.
브레이크 포인트 설정(F2), 프로그램 실행(F9)

위에서 B라는 메뉴를 누르면 브레이크 포인트 목록이 나옵니다
<Alt + b>로 설정된 브레이크 포인트 목록 확인 가능

<Ctrl + F2> : 프로그램 다시 시작
---> 디버깅은 단방향으로만 흘러가고 이 어셈블리어를 모두 파악할 순 없습니다.
그러므로 우리는 가정을 세우고 그 가정과의 비교를 통하여 프로그램의 동작을 파악할 수 있습니다.
그렇기에 프로그램 다시 시작은 가장많이 사용되는 단축키입니다.

키패드의 -, +는 내가 이전에 보았던 데이터를 다시 볼 수 있게 해줍니다. 디버깅은 무조건 단방향으로 흘러가기 때문에 되돌릴 수는 없지만 전에 보았던 로그로 다시 돌아가게 해줍니다.

2-4) MessageBox() 함수 분석

이제 제가 들어간 서브루틴인 MessageBox() 함수에 대해 알아보겠습니다.

int MessageBox(
  [in, optional] HWND    hWnd,
  [in, optional] LPCTSTR lpText,
  [in, optional] LPCTSTR lpCaption,
  [in]           UINT    uType
);


hwnd는 메세지를 만들어주는 핸들러로 만약 값이 NULL이면 창이 열리지 않습니다.
lptext은 보여줄 메세지를 뜻합니다.
lpcaption은 메세지 박스의 타이틀을 의미하고 NULL일 경우 Error을 디폴트로 사용합니다.

이제 메모리 영역을 보면

첫번째 인자와 두번째 인자를 넘기기 위해
"abex' 1st crackme"와 "Make me think your HD is a CD-Rom."을 push하고 있는 것을 알 수 있습니다.

2-5) call 과정 (중요)

이제 다음 명령인 call로 서브루틴을 호출하면 다음과 같은 일이 일어납니다.
1. 00401013(복귀 주소)을 스택에 push하여 백업합니다.

2. 호출 이전의 스택 베이스(EBP)값을 스택에 push하여 백업합니다.

PUSH EBP


3. 현재 스택의 위치를 서브루틴의 새로운 EBP로 변경해줍니다.

MOV EBP, ESP


4. 이 후 다음 서브루틴 호출을 위해 인자를 push를 해줍니다.

PUSH DWORD PTR SS:[EBP+14]
PUSH DWORD PTR SS:[EBP+10]
PUSH DWORD PTR SS:[EBP+C]
PUSH DWORD PTR SS:[EBP+8]


5. RETN 10
서브루틴이 종료되면 복귀주소가 들어가있는 ESP 레지스터가 가리키는 주소로 이동합니다.
뒤에있는 10은 16진수로 16을 의미하고 복귀주소로 돌아가면서 16바이트를 반환해야함을 의미합니다.

복귀 주소 아래 0019FF68 ~ 0019FF74까지 반환합니다.

수행 후의 모습
ESP 값은 스택의 최상위 값으로 변경됩니다.

제가 이해하는대로 써보았습니다.

2-6) 프로그램 구조 분석


우선 GetDriveTypeA() 함수는 드라이브 종류를 정수 형태로 반환하는 함수로 함수 종료 시 EAX 레지스터에 정수가 저장됩니다.

리턴 값에 따라 종류 판별

다음에 오는 INC, DEC는 인자 값을 1증가, 1감소 시키는 의미로 총 ESI는 3증가하고 EAX는 2가 감소합니다.

EAX = 1, ESI = 00401003

CMP는 두 레지스터를 비교하여 값이 같을 경우 제로 플래그 레지스터 값을 1로 설정하는 명령어입니다.
JE는 앞에 수행한 비교 구문의 결과가 같다면(즉, 제로 플래그가 1이라면) 지정된 주소로 점프하고 다르다면 다음 행을 수행합니다.
여기선 EAX와 ESI값이 다르므로 다음 행을 수행하겠네요.
그러므로, 다음행이 실행되겠네요.
만약 EAX와 ESI값이 같아 Zero Flag가 1이었다면 JE에서 지정한 0040103D로 점프하여 성공의 메세지를 보여줄 것입니다.

2-7) 문제 해결

1) 코드 수정을 통한 문제 해결
이 방법은 영구적으로 변경하는 방법으로 앞으로 프로그램 실행 시마다 하드디스크를 CD롬으로 인식시킬 수 있습니다.

현재 EAX값은 3, ESI 값은 0입니다.
총 EAX값을 2감소시키고 ESI 값을 3증가 시킵니다. 여기서 JMP위치를 ESI를 2번 증가를 건너뛰어서 점프시키면 EAX와 ESI값은 같아지므로 성공 메세지로 유도할 수 있습니다.
그러므로 JMP 주소를 00401023으로 수정해보겠습니다.

수정 명령 우클릭 후 Assemble을 누르면

창이 뜨는데 제가 원하는 명령을 넣으면 됩니다.

수정을 해주고 Assemble을 누르겠습니다.
그런데 ESI값이 1이아니고 주소값 형식으로 되어있어서 그냥 ESI값을 1로 변경해주었습니다.

성공했습니다.

2) 점프문에서 값 변경을 통한 문제 해결
JE문에서 Zero Flag값을 1로 변경하여 문제를 해결할 수 있습니다.
이 방법은 프로그램 분석과정에서 여러가지 경우를 분석하기 위해 자주 사용됩니다.

0개의 댓글