디버깅 중인 프로세스 덤프하기



 x64dbg에는 Scylla라고 하는 플러그인이 기본으로 내장되어 있고, 이를 사용하면 간단하게 디버깅 중인 프로세스를 덤프하고 IAT를 복구할수 있습니다. 간단하게 UPX 패킹된 샘플을 언패킹하면서 방법을 알아보겠습니다.





 우선 UPX 패킹의 특징인 PUSHAD에서 POPAD로 이어지는 일련의 언팩 과정을 지나, JMP 명령이 가리키는 주소를 찾습니다.

이 주소가 OEP입니다.



OEP까지 이동한 후 위에 표시된 Scylla 플러그인을 실행시킵니다.




 IAT Autosearch, Get Imports 버튼을 차례대로 눌러 IAT를 자동으로 찾습니다. 저의 경우 저렇게 찾지 FThunk가 발생하기도 하는데 원인은 잘 모르겠습니다. 해당 주소로 이동해 봐도 그 주소에서 바로 ret을 할 뿐이었습니다. 이후 오른쪽의 Dump 버튼을 눌러 [기존 파일명]_dump.exe 파일명으로 프로세스를 그냥 덤프하고, 그 밑의 Fix Dump 버튼을 누르고 방금 드랍시킨 파일을 선택해 고친 IAT를 적용시켜 프로세스를 다시 덤프합니다.




 그러면 [덤프된 파일명]_SCY.exe로 파일이 새로 생성되고, 이 파일이 IAT까지 Rebuild된 덤프 결과입니다.




 IDA로 까보면 언팩된 코드를 그대로 볼 수 있습니다.



 중간에 발생한 FThunk의 경우 검색해봐도 잘 나오지 않아서 원인을 찾을 수가 없는데 혹시 아시는 분이 있다면 댓글로 알려주시면 감사하겠습니다.



블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,

 악성코드 하나를 분석하던 중, 아주 흔하면서도 지금까지 짜증나게 만들었던 루틴이 하나 등장했습니다.

단순하게 LoadLibrary와 GetProcAddress를 통해 모듈 핸들과 함수 주소를 가져오고, 이를 전역변수에 대입하여 다른 코드들에서 사용하는 것으로 IAT에 자신이 사용하는 함수명을 표시하지 않게 하는 흔한 기법이지만, 몇가지 문제점이 있었습니다.



[그림 1. 첫번째 문제]


 첫째, 양이 많았습니다. 한 함수 내에서 40개가 넘어가는 함수들의 주소를 가져오는데, 이는 다음 문제점과 더해 미친듯한 시너지를 발휘합니다.



[그림 2. 두번째 문제]


 둘째, 변수명이 스택에 저장되어있습니다. 바이트 단위로 할당되어있기 때문에 보통이라면 이걸 전부 아스키 문자로 바꿔서 하나하나 읽고 무슨 함수인지를 파악해야 합니다. 함수가 엄청나게 많다 보니 1500바이트가 넘어가는 양입니다.




[그림 3. 세번째 문제]


 셋째, 변수 값이 암호화되어있습니다. 정확히는 0x08이라는 고정된 값으로 XOR되어있는 것이지만, 어쨌든 그냥 봐서는 어떤 함수인지 알기가 힘듭니다. 셋중 하나라도 문제가 없었으면 그냥 복사해서 파이썬 스크립트로 복호화하거나 해서 하나하나 네이밍해줬겠지만, 도저히 그럴 양이 아니었습니다.



 여기에서 잠시 현자타임이 왔고, 이걸 어떻게 해결할까 하다가 지금까지 너무 무식하게 분석해왔다는 생각이 들어서 IDAPython을 이용하여 자동 스크립트를 짜기로 결심했습니다.




[그림 04. 스크립트 동작 전]


 작성한 스크립트를 돌리기 전의 모습입니다. 



[그림 05. 스크립트 로그]


 스크립트를 동작시키면 IDAPython 콘솔창에 로그가 남습니다.




[그림 06. 스크립트 동작 후]


 스크립트를 동작한 후 새로고침해주면 위와 같이 GetProcAddress를 이용하여 대입한 값들이 네이밍됩니다. 예전 BoB 4기에서 잠깐 한 적이 있기는 했는데 사실 기억에는 전혀 남아있지 않아서 혼자 여기저기 검색하면서 조잡하게 짰습니다.


이 스크립트에는 아직 개선할 점이 몇 가지 있습니다.


- GetProcAddress의 결과를 전역변수가 아닌 지역변수나 Struct의 멤버변수같은것에 대입하는 경우는 네이밍이 되지 않습니다.

- 특정 함수 내에 커서를 둔 채로 실행해야 하며, 해당 함수 내에서의 GetProcAddress에 대해서만 네이밍을 진행합니다.

- GetProcAddress함수의 주소를 직접 구해서 RenameGetProcAddrVars 함수에 인자로 넣어야 합니다.


스크립트를 사용하면서 나중에 조금씩 업데이트해갈텐데, 그러면서 하나씩 해결해갈 예정입니다.


소스코드 및 정리한 내용은 아래에 있습니다.


IDAPythonUtils.py




 소스코드 내에서 사용한 idaapi의 함수들은 다음과 같습니다.


※ ea라는 용어는 Effective Address의 약자로, 주소값을 의미합니다.

- Byte : 인자로 ea를 받으며 해당 주소의 1바이트 값을 반환합니다. 리터럴 상수처럼 정적으로 값이 확인 가능해야 합니다.
- GetFunctionAttr : 첫번째 인자로 ea, 두번째 인자로 받아올 함수의 속성을 입력받아 해당 함수의 속성을 가져옵니다. 여기에서는 함수 시작 주소와 끝 주소를 받아오는데에 사용했습니다.
- GetDisasm : 인자로 ea를 받아 해당 주소의 값을 디스어셈블하여 문자열로 리턴합니다.
- GetOpnd : 첫번째 인자로 ea를, 두번째 인자로 가져올 오퍼랜드를 0-Base로 입력받습니다. 디스어셈블된 오퍼랜드 자체를 넘겨줍니다. (예:  lea eax, [esp+674h+var_617]의 경우 :[esp+674h+var_617]: 리턴)
- GetOperandValue : 첫번째 인자로 ea를, 두번째 인자로 가져올 오퍼랜드를 0-Base로 입력받습니다. 오퍼랜드에 넘어간 값을 강제로 int형으로 바꿔서 반환하는 것 같습니다. (예: lea eax, [esp+674h+var_617]의 경우 93 리턴)
- here : 현재 커서가 가리키는 주소 반환
- PrevHead : ea를 인자로 받아 바로 이전 명령의 주소 반환
- NextHead : ea를 인자로 받아 바로 이후 명령의 주소 반환
- XrefsTo : ea를 인자로 받아 해당 주소를 참조하는 주소들을 iterable하게 반환




블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,


 사실 무척 간단한 테크닉인데, 지금까지 모르고 계속 모든 struct를 직접 만들었다가 이제야 알게 되어 작성해 본다.


[직접 만드느라고 고생한 struct PEB_LDR_DATA]



 위 이미지는 직접 한땀한땀 만들어준 struct이다. 악성코드를 분석하는 도중 PEB에 접근하여 LDR을 가져오는 루틴이 있었고, 이를 좀더 보기 쉽게 하기 위해서 struct를 만든 것이다. LIST_ENTRY라는 struct도 따로 필요해서 직접 만들어 주었고, 여러모로 귀찮은 작업이 많았다. 그런데 알고 보니 PEB_LDR_DATA와 같은 윈도우에서 사용하는 범용적인 struct들은 이미 IDA에서 전부 만들어뒀다고 한다.



[Type libraries]


 View->Open subviews->Type libraries 혹은 단축키로 Shift+F11을 눌러보자. 'Loaded Type Libraries'라는 탭이 하나 추가되는 것을 볼 수 있다.



[Load New Type Library]


 우클릭->Load type library 또는 단축키 Ins를 누르면 위와 같은 창이 뜨면서 원하는 타입 라이브러리를 추가할 수 있다. 상당히 많은 라이브러리들이 있는데, 여기서 자기가 원하는 타입이나 struct가 선언되어 있는 라이브러리를 선택해서 로드하기만 하면 직접 struct 구조를 생성해 줄 필요 없이 그대로 사용 가능하다. 이제 위의 ntapi를 로드한 후, struct 탭에서 해당 struct를 생성하거나 변수에 타입을 지정해보자.



[자동으로 로드되는 타입들]


 놀랍게도 위와 같이 자동으로 struct가 생성되고 반영된다. 

나는 지금까지 무슨 삽질을 하고 있었던 것인가... 역시 머리가 나쁘면 몸이 고생한다.






블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,

IsDebuggerPresent 함수는 윈도우에서 제공하는 API로, 아주 고전적인 안티 디버깅 함수이다.




 간단히 user-mode debugging을 탐지하여 bool 리턴값으로 디버깅 중이면 true를, 아니면 false를 반환한다.

이를 찾아 바이너리 패치를 통해 우회하는 방법을 적어보려 한다.


IsDebuggerPresent_Sample.exe



 일단 샘플을 그냥 실행시킬 시 "Now not debugging! XD"을 출력하고 종료되며,



 Ollydbg등을 이용해 디버깅하며 실행하면 "Now debugging... :("을 출력하고 종료된다.



 main 함수 내부를 디스어셈블한 모습이다.

IsDebuggerPresent 함수를 실행하여 EAX에 반환된 값을 TEST 연산을 통해 0인지 확인하고, 0일 경우 점프하는 루틴이 있다. 디버깅 중일 경우에는 true, 즉 1을 반환하므로 점프하지 않을 것이다. 따라서 우리는 저 조건 점프문을 그냥 반환  값에 관계없이 점프하도록 바꿔주기만 하면 된다.



 명령어를 더블클릭하면 수정이 가능한데, "Jump if Equal"의 줄임말인 JE를 그냥 무조건 점프하는 JMP로 바꿔준다.

이것만으로는 바이너리가 패치가 되지 않고 메모리만 바뀌기 때문에 드라이브에 저장되어 있는 원본 파일은 바뀌지 않는다. 이를 파일로 따로 저장할 필요가 있다.



 패치한 부분을 드래그하여 선택 후 우클릭, Copy to executable -> Selection 을 선택한다.



 이후 나타난 창에서 Save file을 클릭하여 따로 파일을 저장하면 된다.



 이렇게 패치한 파일은 디버깅 중이어도 항상 Now not debugging!을 출력하게 된다. 이렇게 바이너리 패치를 통해 간단한 안티 디버깅 기법을 무력화할 수 있다.


블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,


 최근 악성코드 하나를 분석하다가 이 악성코드가 레지스트리를 수정한다는 것을 알게 되었다.

이 악성코드는 'SOFTWARE\Microsoft\Windows\CurrentVersion~' 이후 부분을 수정하고 있었는데, 디버깅 이후 아무리 찾아봐도 코드가 정상적으로 실행되었는데도 레지스트리가 생성되지 않았다. 처음엔 관리자 권한을 주지 않아서 그런 줄 알았으나 권한을 부여해도 마찬가지였다. 이상하다고 생각해서 Ctrl + F로 찾아봤더니 다른 경로에 해당 값이 들어있었다.


[디버깅 당시의 레지스트리]


 디버깅 당시에는 위와 같은 경로였으나



[실제 변경된 레지스트리]


 실제 변경된 레지스트리의 경로는 위와 같았다.

이는 Vista 이상 64비트 Windows에서 32비트 프로그램의 호환성을 맞춰주기 위해 자동으로 Redirect하기 때문이다. 여기에 잘 정리된 글이 있으므로 링크를 남겨두겠다. 이와 비슷한 증상이 후킹을 할 때도 나타나는데, 32비트 notepad를 목표로 작성한 후킹 코드는 64비트 Windows에서는 SysWOW64 내부의 notepad.exe를 실행시켜야 한다.


http://coinz.tistory.com/581

'Reversing' 카테고리의 다른 글

IDA에서 타입 추가 로드하기  (0) 2018.09.21
IsDebuggerPresent와 바이너리 패치  (0) 2017.05.24
[ARM] Reversing.kr HateIntel 분석  (0) 2016.04.22
[ARM] 간단한 프로그램 분석  (0) 2016.04.22
[ARM] Hello World 분석  (1) 2016.04.22
블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.



Hello World는 분석하기에 너무 명령어 수도 적고 실력도 크게는 늘지 않을 것 같아 조금 더 소스를 늘려 본 코드를 가져왔다.

조금이지만 더 복잡하므로 분석할 가치는 있어 보인다.




argv[1]로 문자열을 입력받고, 총 10번 해당 문자열을 출력해 주는 간단한 프로그램이다.

이를 컴파일하고 디스어셈블해서 보면 다음과 같다.


대충 봐도 Hello World보다는 훨씬 길고 복잡한 코드이다.

이도 마찬가지로 쭉 분석해 보겠다.





   0x00010480 <+0>: push {r11, lr}

// lr과 r11을 스택에 저장한다. 전에도 봤다시피 함수 프롤로그인 것 같다.

   0x00010484 <+4>: add r11, sp, #4

// r11에 sp+4의 값을 구하여 저장한다.

   0x00010488 <+8>: sub sp, sp, #16

// sp에서 16을 빼서 다시 sp에 저장한다. 스택에 공간을 할당하는 부분이다.

   0x0001048c <+12>: str r0, [r11, #-16]

// *(r11-16)에 r0을 집어넣는다.

   0x00010490 <+16>: str r1, [r11, #-20]

// *(r11-20)에 r1을 집어넣는다.

   0x00010494 <+20>: ldr r3, [r11, #-16]

// r3에 *(r11-16)의 값을 가져와 넣는다.

// 위에서 *(r11-16)에 r0을 넣었으므로 이 명령 이후 r3에는 r0의 값이 들어 있다.

   0x00010498 <+24>: cmp r3, #2

// r3와 상수 2를 비교한다.

   0x0001049c <+28>: beq 0x104b0 <main+48>

// 만약 같았다면 main+48인 0x104b0으로 점프한다.

   0x000104a0 <+32>: ldr r0, [pc, #136] ; 0x10530 <main+176>

// 다르다면 *(pc+136)의 값을 r0에 집어넣는다.

   0x000104a4 <+36>: bl 0x1031c

// 0x1031c로 점프한다.

// 실행시켜본 결과 printf함수가 실행되었다. r0에 들어간 것은 문자열의 주소였을 것이다.

   0x000104a8 <+40>: mov r3, #1

// r3에 1을 집어넣고

   0x000104ac <+44>: b 0x10524 <main+164>

// main+164로 점프하는데, 이곳은 확인해보면 함수 에필로그 부분이다.

// 즉, 위 조건문에서 argc를 비교하고 2가 아닐 경우 printf 함수 실행 후 return 1을 수행한다는 것을 알 수 있다.


   0x000104b0 <+48>: ldr r3, [r11, #-20]

// 정상적으로 조건문을 통과하면 여기로 오게 된다.

// 이번엔 r3에 *(r11-20)을 집어넣는다.

   0x000104b4 <+52>: add r3, r3, #4

// r3에 4를 더한다.

   0x000104b8 <+56>: ldr r3, [r3]

// r3안의 값을 가져와서 r3에 넣는다.

// argv에서 argv[1]의 값을 가져오는 과정이었다고 생각된다.

   0x000104bc <+60>: mov r0, r3

// r0에 r3를 넣는다. ARM에서는 함수 인자를 r0에 넣는 모양이다.

   0x000104c0 <+64>: bl 0x10340

// 이후 0x10340의 함수를 호출한다.

   0x000104c4 <+68>: mov r3, r0

// r0을 r3에 집어넣는다.

   0x000104c8 <+72>: cmp r3, #256 ; 0x100

// r3와 0x100을 비교한다. 방금 점프한 곳은 strlen이었던 것 같다.

   0x000104cc <+76>: bls 0x104e0 <main+96>

// bls 명령은 비교 결과가 작았을 경우 점프하는 명령이다.

   0x000104d0 <+80>: ldr r0, [pc, #92] ; 0x10534 <main+180>

// 256보다 컸을 경우에는 점프하지 않으므로 다음 명령이 바로 실행되는데,

// r0에 *(pc+92)를 집어넣는다.

   0x000104d4 <+84>: bl 0x1031c

// 위에서 확인한 printf 함수의 주소이다.

   0x000104d8 <+88>: mov r3, #1

   0x000104dc <+92>: b 0x10524 <main+164>

// 마찬가지로 r3에 1을 넣고 main의 에필로그로 점프한다.


   0x000104e0 <+96>: mov r3, #0

// 두 번째 조건문까지 무사히 통과하면 r3에 0을 넣는다.

   0x000104e4 <+100>: str r3, [r11, #-8] 

// *(r11-8)에 r3를 집어넣는다.

   0x000104e8 <+104>: b 0x10514 <main+148>

// main+148로 점프한다. 반복문의 시작일 듯 하다. 바로 main+148로 점프하자.


   0x000104ec <+108>: ldr r3, [r11, #-20]

// r3에 *(r11-20)의 값을 가져와 저장한다.

   0x000104f0 <+112>: add r3, r3, #4

// r3에 4를 더하고,

   0x000104f4 <+116>: ldr r3, [r3]

// *r3를 가져와 r3에 다시 넣는다.

// 위에서 봤던 구조와 비슷한 게 argv[1]을 가져오는 과정이다.

   0x000104f8 <+120>: ldr r0, [pc, #56] ; 0x10538 <main+184>

// *(pc+56)을 r0에 집어넣는다.

// 이 값을 확인해 보면 아마 포맷스트링이 나올 것이다.

   0x000104fc <+124>: ldr r1, [r11, #-8]

// r1에는 *(r11-8)을 집어넣는다. 카운터로 사용하는 값이다.

   0x00010500 <+128>: mov r2, r3

// r3를 r2로 집어넣는데, 여기에는 출력할 문자열의 주소가 들어 있다.

// 여기까지 레지스터의 상태를 확인해 보면, r0에는 *(pc+56)으로 포맷스트링이,

// r1에는 *(r11-8)로 카운터 변수, r2에는 argv[1]이 들어 있다.

   0x00010504 <+132>: bl 0x10310

// 뭔가 함수를 실행하는데, 아마 printf일 것 같다. 왜 위의 printf와 다른 주소인지는 잘 모르겠지만 아마 인자의 차이가 아닐까 싶다.

   0x00010508 <+136>: ldr r3, [r11, #-8]

// r3에 *(r11-8)을 가져와 집어넣는다.

   0x0001050c <+140>: add r3, r3, #1

// r3에 1을 더해서 다시 저장하고,

   0x00010510 <+144>: str r3, [r11, #-8]

// 이를 다시 *(r11-8)에 저장한다. 카운터 변수에 ++연산을 해준 모습이다.

   0x00010514 <+148>: ldr r3, [r11, #-8]

// r3에 *(r11-8)을 집어넣는다.

// 방금 반복문의 카운터가 되는 모양으로, 처음에는 0으로 초기화시켰다.

   0x00010518 <+152>: cmp r3, #9

// r3와 9를 비교하고,

   0x0001051c <+156>: ble 0x104ec <main+108>

// 같거나 작을 경우에는 main+108로 점프한다.


// for문 내부를 확인해 보면 총 10번 돌면서 계속 카운터 변수와 argv[1]를 출력함을 알 수 있다.

   0x00010520 <+160>: mov r3, #0

// r3에 0을 집어넣는다.

   0x00010524 <+164>: mov r0, r3

// r0에 r3의 값을 넣는다.

   0x00010528 <+168>: sub sp, r11, #4

// sp에 r11-4를 집어넣는다.

// 지금 보니 함수의 첫부분에 r11에 sp+4를 저장했으므로 이는 r11을 Stack Base Pointer로 쓰는 것과 비슷하다.

// 마지막에 r11-4를 sp에 넣는 것으로 복구까지 시켜주고, 변수를 이 기준으로 참조까지 하는 것을 보니 확실해 보인다.

   0x0001052c <+172>: pop {r11, pc}

// pc와 r11에 각각 pop 하여 함수를 끝낸다.

// 이 이후 명령은 뭔지 잘 모르겠다.

   0x00010530 <+176>: ; <UNDEFINED> instruction: 0x000105b0

   0x00010534 <+180>: andeq r0, r1, r4, asr #11

   0x00010538 <+184>: ldrdeq r0, [r1], -r8



생각했던것보다 양이 많아서 분석하는 데에 오래 걸렸다.

특히 처음에 r11에 sp+4를 집어넣는데 그 이유를 몰라서 더 힘들었던 것 같다.

왜 하필 sp+4를 해서 넣어주는지는 잘 모르겠지만 이게 Base Pointer 역할을 하고 있다는 것을 알게 되었다.

이번에도 간단한 명령들 뿐이긴 했지만 막상 이렇게 분석을 해 보니 역시 ARM은 명령어 체계를 압축해서 쓰며

따라서 원래 Intel계열 어셈블리보다 오히려 더 분석이 쉽고 명확한 부분도 있는 것 같았다.

나름 ARM도 해볼 만한 분야인 것 같다. 재미있다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,

[ARM] Hello World 분석

Reversing 2016. 4. 22. 11:29




Intel 계열이라면 질리도록 봐서 금방 하겠지만 ARM은 그리 만만하지 않았다.

마음같아서는 IDA의 Hex-rays 기능을 이용하고 싶지만 그래도 공부하는 게 좋겠다고 생각해서 이렇게 시작하게 되었다.

ARM의 레지스터 구조 등은 이미 BoB 등에서 공부했기에, 중요한 부분만 간략히 서술하고 들어간다.




분석할 "Hello, World!\n"를 출력하는 프로그램이다.

프로그래밍 처음 배우면서 누구나 접해 볼 프로그램이고, 리버싱을 처음 접할 때 분석하게 되는 프로그램이기도 한다.

ARM 서버 32비트에서 gcc로 컴파일했다.

(Thanks to 5unKn0wn)




명령어들은 다음과 같았다.

분석하면서 하나하나 정리해 보자.


ARM의 명령 실행 구조는 총 세 단계의 구조를 가지는데, 이를 Instruction Pipeline이라고도 한다.

이 단계는 명령어를 메모리에 적재하는 Fetch,

적재된 명령어를 해석하는 Decode,

해석된 명령어를 실행하는 Execute 로 나뉜다.


ARM에서는 이 세 가지의 과정이 한 번에 실행된다.

즉, 하나의 명령이 실행되는 도중(Execute)이라면 그 다음 실행될 명령어가 해석(Decode)되고 있으며,

또 그 다음에 실행될 명령어가 적재(Fetch)되고 있다는 의미이다.


ARM에서 레지스터는 r0 ~ r15까지 총 16개가 존재하는데, 이중 r13은 Stack Pointer로 사용되며,

r14는 리턴 어드레스 용도로 함수 이후 실행될 주소가 저장된다. 이를 lr(Link Register)라고 부른다.

또한 r15가 가장 중요한 Program Counter로, 현재 fetch 되고 있는 명령어의 주소를 의미한다.


push {r11, lr}

// push 명령은 stack에 값을 적재하는 역할을 하는데, 직접 실행해 본 결과 lr, r11 순서로 push하는 것을 알 수 있었다.

// main 마지막에 pop {r11, pc} 를 하는데, 여기에 다시 들어갈 값을 저장하는 함수 프롤로그 부분이라고 볼 수 있다.

add r11, sp, #4

// add 명령은 Rd에 Rn + Op2를 더해서 저장하는 역할을 한다. 여기서는 r11에 sp+4를 저장한다. (ARM에서 상수 표기는 앞에 #을 붙인다.)

// 즉 r11에 sp+4를 넣는다.

sub sp, sp, #8

// add와 형식은 같다. sp에서 8을 빼서 sp에 저장한다. sp -= 8 이라고 볼 수 있겠다.

// 스택 사용 공간을 할당하는 것이 아닐까. 쓸 일은 없겠지만.

str r0, [r11, #-8]

// 갑자기 방향이 바뀌어서 헷갈리겠지만 STR 명령은 *(Rn + Op2)에 Rd를 집어넣는 명령이다. 갑자기 방향이 바뀌었다. 이게 ARM에서 제일 짜증나는 부분이라고 한다.

str r1, [r11, #-12]

// 마찬가지로 *(r11 - 12) 에 r1을 집어넣는다.

ldr r0, [pc, #16] ; 0x10448 <main+44>

// ldr 명령은 Rd에 *(Rn + Op2)를 집어넣는다. 여기서는 r0에 *(pc+16)을 넣는다.

// 세미콜론 뒤에 0x10448이라고 씌여 있는데 이 값은 pc+16의 값이므로 이 내부에 있는 값을 확인하면 문자열의 주소가 들어 있는 것을 확인할 수 있다.

// 이 값을 확인해 보면 "Hello World!\n"가 들어 있다.

// 즉, 이 명령은 r0이라는 레지스터에 문자열의 주소를 집어넣는 명령이다.

bl 0x102c4

// bl은 Branch with Link의 약자이다. Branch는 Intel계열 Assembly에서 JMP와 비슷한 명령으로, 주소 분기 명령어이다. 이 명령을 수행하면서 BL은 자동으로 lr에 다음 명령의 주소를 저장한다.

mov r3, #0

// r3에 0을 집어넣는다.

mov r0, r3

// r0에 r3의 값을 넣는다. 즉, r0에도 0을 집어넣는다.

// return 0; 을 했었으므로 main의 리턴값을 r0에 넣은 것이라고 볼 수 있다.

sub sp, r11, #4

// sp에 r11-4를 집어넣는다.

pop {r11, pc}

// pc와 r11에 각각 pop을 한다.

// 직접 디버깅해보며 실행해 보니 이 명령을 수행한 직후 바로 __libc_start_main으로 진입한다. 즉 여기서 함수가 끝난다고 볼 수 있겠다.

andeq r0, r1, r0, asr #9

// 이 명령에 대한 정보는 아무리 찾아봐도 제대로 나와있지 않아서 잘 모르겠다.

// 혹시 아는 분이 있다면 알려주시면 감사하겠습니다.




실행시킨 결과로 아름다운 Hello, World! 가 출력된다.

이래서 언제 ARM을 다 공부할지 걱정이지만 그래도 계속 공부하자.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,

main에서부터 명령어를 하나하나 보면서 스택과 레지스터에 들어있는 값이 어떻게 변환되고, 프로그램이 어떻게 작동하는지를 하나하나 파악해 보도록 하겠다. 하나하나라고는 하지만 정말 완전히 한 명령어씩만 하지는 않고, 연관성이 있는 명령어들은 묶어서 설명한다.


프로그램 소스는 매우 간단하다. 조금만 프로그래밍을 해본 사람이라면 한 번쯤은 짜봤을 법한 소스이다.

<소스>

#include <stdio.h>

int main()
{
	int i;
	int sum = 0;
	for(i=1; i<=100; i++) sum+=i;
	printf("sum : %d\n", sum);
	return 0;
}


필자는 컴파일과 분석을 우분투 14.04 64bit 버전에서 수행했다.

64비트 환경이므로 32비트로 컴파일하고 분석하기 위하여 컴파일시 뒤에 -m32 를 붙여주자.

또한 분석을 용이하게 하기 위해 뒤에 -mpreferred-stack-boundary=2 도 붙여주도록 하자.


여기까지 했으면 간단한 분석의 준비가 끝났다.

바로 GDB를 이용해 하나하나 분석해 보도록 하자.


gdb를 실행할 때 뒤에 붙인 -q 옵션은 gdb 실행 시 나타나는 긴 버전 및 도움말 등을 스킵하는 옵션이다.

disas main 명령어를 쳐서 메인을 보면 위와 같이 AT&T 문법으로 나타나는 메인의 어셈블리어들을 볼 수 있다.

혹시나 리버싱을 먼저 했거나, Intel의 문법이 더 익숙한 사람들은 ' set disassembly-flavor intel ' 명령을 이용하여 문법의 변경이 가능하다.

우선 ' b *main '으로 main의 첫 부분에 BreakPoint를 걸고 실행시켜 main에서 멈추게 한 뒤 ' info reg '로 레지스터들의 초기값들을 확인해보자.

eax, ecx, edx, ebx는 지금 당장 눈여겨 볼 필요는 없고, 현재 ebp 값이 0이라는 점을 보고 넘어가자.


이제 한줄씩 실행해 가며 분석을 시작할 텐데, 한 번 할때마다 계속 값을 확인하기는 귀찮으므로 display 명령을 이용하여 자동으로 값이 출력되도록 설정하도록 하겠다. 봐야 하는 것은 다음에 실행할 명령어들 일부와 main에서 사용되는 레지스터들의 값이다.

main에서 실질적으로 사용하는 범용 레지스터는 eax밖에 없으므로 esp, ebp, eax 및 esp을 기준으로 스택에서 0x20바이트, eip를 기준으로 명령어 5개를 display하겠다.

이제 si나 ni 등으로 한 명령어씩 수행할 때마다, 혹은 BP에 걸릴 때마다 display된 값들을 자동으로 출력해 준다.


ni를 이용하여 명령을 수행하자.

우선 push %ebp와 mov %esp, %ebp이다.

mov    %ebp와 mov    %esp, %ebp라는 명령이었다.

이 명령을 통해 esp에 +4를 한 후 ebp값이 esp에 저장되고, esp의 값이 ebp에 들어갔으므로, 둘은 0xffffd128이라는 같은 주소를 가리키게 되었다.


다음 명령이다.

sub    $0x10, %esp 이라는 명령이었다.

esp에서 0x10만큼 뺀다는 것인데, 스택은 주소상 위에서 아래로 자라므로 스택에 0x10 bytes만큼을 할당했다고 볼 수 있다. 0xffffd128을 가리키던 esp가 0xffffd118을 가리키게 되었다.

다음 명령이다.

movl    $0x0, -0x4(%ebp) 라는 명령이었다.

ebp-0x4가 가리키는 위치에 0을 넣는다는 뜻인데, 

변한 게 없는 것 같지만 이미 0x00000000이라는 값을 가진 위치가 0x0으로 바뀐 것이기 때문에 변화가 없는 게 당연하다. 지역변수에 0을 할당하였다.

다음 명령이다.

mov    $0x01, -0x8(%ebp) 였다.

지역변수2에 1이라는 값을 할당하였다.

메모리상 ebp-0x8 위치가 1로 바뀐 것을 확인할 수 있다.

다음 명령이다.

jmp    0x804843d 라는 명령.

아래로 점프하였다.

eip의 값이 0x804843d로 변경되었다.

다음 명령이다.


이번에는 연관이 있는 명령이라 한번에 두 개의 명령을 동시에 수행했다.

cmpl $0x64, -0x8(%ebp) 였는데, 이후 jle 0x8048433 명령이 수행되었다.

이는 $0x64라는 값과 ebp-0x8에 들어 있는 값을 비교하여 *(ebp-0x8)이 같거나 작을 경우 지정한 주소로 점프하는 명령어이다.

반복문의 시작점이라고 볼 수 있겠다.

연재 ebp-0x8에는 1이 들어 있고, 이는 아마도 반복문의 카운터가 되는 값이다.

계속하자.


두 개의 명령을 한번에 실행했다.

mov    -0x8(%ebp), %eax  와  add    %eax, -0x4(%ebp) 이었다.

eax에 -0x8(%ebp)를 집어넣는데, 이는 아까 가져온 카운터 값이다. 그리고 처음에 0으로 초기화한 -0x4(%ebp)에 집어넣는다. 아마 이 부분이 sum 변수일 것으로 생각된다.

다음 명령이다.


addl $0x1, -0x8(%ebp) 이었다.

카운터라고 추측했던 -0x8(%ebp)에 상수 1을 더한다.

그리고 cmpl과 jle 문을 지나면서 반복문을 계속한다.

-0x8(%ebp)가 0x64보다 작거나 같을 때까지 반복하는 반복문이므로 C언어로 표현하면

for(i=1; i<=100; i++) sum += i;

이런 구문이라고 볼 수 있겠다.

반복문 다음의 mov, 즉 main+38에 BP를 걸고 c로 계속 진행시키자.


-0x8(%ebp)의 위치에는 0x65가 들어가 반복문을 빠져나온 직후이며, 결과인 sum은 0x13ba라는 값인 것을 알 수 있다.

다음은 함수의 인자를 지정해 주는 단계이다. 계속하자.


-0x4(%ebp)에 들어 있는 결과값을 eax에 집어넣고, 이를 또 0x4(%esp)에 넣는다.

마지막으로 문자열의 주소가 저장되어 있을 것으로 예상되는 0x80484f0이라는 주소를 (esp)에 넣는다.

보통 함수의 인자를 넣을 때 인자를 거꾸로 넣으며 push를 이용해 스택에 넣어준다고 알고 있는 사람이 많은데,

이렇게 esp를 미리 함수 인자만큼 할당 후 (esp)에 첫번째 인자, (esp+4)에 두번째 인자 등으로 넣어 주어도 관계는 없다.

어차피 push reg 라는 명령이 { sub    $0x04, %esp  /   mov    reg, (%esp) } 와 같은 의미이기 때문이다.

이제 call 명령을 수행한다.


명령을 수행하자마자 " sum : 5050 " 이라는 문자열이 커맨드라인에 출력되었다.

첫 번째 인자로 들어갔던 0x80484f0의 주소에 들어 있는 값을 확인하니 "sum : %d\n"라는 문자열임을 확인할 수 있다.

다음으로 return 0을 수행하기 위해 %eax에 0을 넣는다.


이제 마지막으로 남은 것은 함수를 끝내는 함수 에필로그 뿐이다.

이 부분은 조금 자세히 보자.


leave 명령을 수행한 직후의 모습이다.

leave는 저장해 두었던 main 이전 함수의 %ebp, 즉 SFP(Saved Frame Pointer)를 원래대로 되돌리는 명령이다.

이 명령은 { mov %ebp, %esp  /  pop %ebp } 로 이루어져 있다.

%ebp의 값을 %esp에 넣게 되므로 %esp와 %ebp는 같은 위치를 가리키게 되고, %ebp는 main 함수의 프롤로그에서 { push %ebp  /  mov %esp, %ebp } 를 한 후 고정되어 있었으므로 당연히 SFP를 가리키고 있다.

pop 명령은 %esp를 기준으로 진행되는 명령이므로 pop %ebp를 하게 되면 SFP가 ebp로 들어가고, 이렇게 원래의 ebp가 복원되었다.

위 이미지에서 가장 처음에 ebp가 갖고 있던 초기값인 0x0이 복원된 것을 볼 수 있다.

다음으로 ret 명령이다.


leave 명령이 %ebp를 복원하는 명령이었다고 한다면, ret 명령은 %eip를 원래 있어야 할 장소로 돌려보내는 명령이다.

call로 함수를 수행하게 되면 자동으로 RET을 스택에 push 하는데, 이는 이 함수 이후 수행할 명령어가 저장된 주소이다.

예를 들어 이 프로그램에서라고 하면 call printf로 함수를 수행한 뒤에도 eip는 return 0을 수행하기 위해 그 다음의 주소로 정상적으로 돌아왔다.

실제 함수의 주소는 main이 아닌 libc에 있기 때문에 %eip가 달라져 다른 주소로 점프하게 되는데, 점프하기 전에 다시 돌아오기 위해 스택에 값을 저장해 두는 것이다.

ret 명령은 { pop %eip  /  jmp %eip } 이다. 원래 %eip는 mov나 pop 같은 명령어로는 변조가 불가능한 매우 중요한 레지스터이기 때문에 뒤에 jmp %eip를 통해 정당성을 확보해 준 것이다.

esp는 leave 명령을 수행한 이후 pop 때문에 SFP+4의 위치를 가리키고 있는데, 여기에서 pop을 하게 되면 SFP 직전에 저장된 RET가 eip에 들어가게 되어 eip가 정상적으로 복구되는 것이다.

RET가 스택에 저장된다는 취약점을 이용하여 버퍼 오버플로우 등을 이용해 이 부분을 원하는 값으로 덮어씌우면 원하는 주소의 명령을 수행하게 할 수 있다.





블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

,