constWORLDant

HACKINGCAMP 19 Can You Login ? 본문

0x02 Reverse Engineer

HACKINGCAMP 19 Can You Login ?

data type ConS_tanT 2019.02.18 03:31

[들어가기에 앞서]

안녕하세요. Demon팀 c0nstant입니다.

문제 풀이 위주가 아닌, 초심자의 눈으로 바라보고 분석해나가는 시나리오로 작성되는 문서입니다.

이것을 보고 리버스 엔지니어링을 생전 처음 접하는 사람도 기법을 익혀갈 수 있도록 세심하게 작성하였습니다.


[운영체제]

ELF 64bit이면서, PIE(shared object), strip 기법을 지니고 있는 바이너리입니다.


[동적분석]

참가자에게 준 바이너리를 실행하면 Welcome Hacker~라는 문장이 여러번 출력되면서 Segmentation Fault가 출력되면서 프로세스가 종료되는 것을 확인할 수 있습니다.


Segmentation Fault는 메모리에 존재하지 않는 주소에 접근할 때 발생하는 메모리 보호 기법입니다.


Segmentation Fault가 이 프로그램에서 왜 터졌는지를 알고 싶을 땐 ‘ulimit -c unlimited’ 라는 명령어를 사용하면 됩니다.

core 파일이 생성되었음을 확인할 수 있습니다.


core 파일의 정보를 알고 싶을 땐 “gdb -c core ./client”를 사용하면 됩니다.

Program terminated with signal SIGSEGV, Segmentation fault라고 에러문이 쫜 뜨고

0x0000000000000001 in ?? 라고 출력됨을 확인할 수 있습니다.


우리의 프로그램은 비정상적 종료를 열심히 유저에게 알려주고 있습니다. 그리고 이는 ‘OS’에 의해 발생하는 신호입니다.


보다 더 자세한 정보를 알고 싶습니다.

그럴 땐 레지스터를 보는 방법을 택할 수 있습니다. ‘i r’을 입력해보면 결과를 보여줍니다.

rip가 0x1이라고 합니다. 우리는 0x1 주소에 접근을 할 수 없습니다.

동적분석을 다른 방법으로 한번 해보고자 합니다. 사용된 도구는 ‘ida64’ 입니다.


ida는 원격 디버깅이 가능합니다. 호스트와 가상머신을 연결시켜서 진행할 수 있습니다.

우선 ida를 통해 메인으로 추정되는 녀석을 찾았고, 그 부분에 breakpoint를 진행할 것입니다.

0x14BD offset에 breakpoint를 걸고 진행해봅니다.


리눅스 클라이언트를 이용하는 분은 ida의 remote파일을 copy&paste하시면 되고 리눅스 서버를 이용하시는 분은 scp를 이용하여 호스트에서 리모트 서버로 파일을 전달할 수 있습니다. 저는 리눅스 서버를 이용하고 있기에 scp를 이용하여 remote 파일을 넣었습니다. remote 파일의 경로는 아래의 사진을 참고해주세요.


linux server를 실행하고 ida에 원격접속을 하기 위해서는 linux server가 실행되고 있는 환경의 ip 주소를 ida에 적어주어야 합니다.


그럼, 이제 동적분석하러 가봐요.

디버깅이 진행되지 않는다. 그렇다면, 이 파일을 로드할 수 없다는 것일까요?

그럼 정적분석으로 다시 찬찬히 살펴봅니다.


ida 디버깅 정보를 다 삭제한 후, 다시 로드 시켜보면 이러한 것을 먼저 찾을 수 있습니다.

start라는 함수가 있는데요. start는 한국말로 ‘시작'입니다. 리눅스에서는 이를 엔트리 포인트라고 부릅니다. 여기서 아까 우리가 봤던 Welcome Hacker가 있음을 볼 수 있습니다.


그렇다면, 여기서 실제 리모트서버에서는 어떤 문자열이 출력되는지를 확인해보고 넘어가도록 하겠습니다.


그럼 유저가 받은 파일은 “변조” 된 파일임을 알 수가 있죠. 이전 발표 시간에 해시 값을 비교할 수 있다고 했는데요. 해시 값을 비교해보도록 하죠.



HASH sha1

User Client

89cb797b4e5c64362650269fa5b8f1f11d84c030

Remote Client

e842bbb61b4827c481750be29f622b12084cefc1


sha1 HASH 값이 다릅니다. 무결성이 깨짐과 동시에, 유저가 가지고 있는 프로그램은 디버깅을 방해하는 요소가 들어있다고 볼 수 있습니다.


그렇다면, 어떻게 패치를 할 수 있을까요?

start의 주소를  실제 start주소로 슥삭 바꾸면 됩니다.

이는 main함수를 호출하는 부분에 존재합니다.


해당 섹션에서 주소를 변경할 수 있습니다.

dq offset start


이 부분에서 __libc_start_main을 호출합니다. 그렇다면, 실제 start는 offset BD0이 됩니다.


패치를 슥삭 슥삭 진행해봅니다.

일단 엔트리포인트를 다시 정상적으로 교체를 하였고, 이제 이를 저장해야합니다.


IDA 에서 Edit  > Patch Program > Apply patches to input file를 진행하면 됩니다.  


이런식으로, 섹션에 빨갛게 되는 경우는 함수로 만들어 두지 않아서인 경우가 있습니다. 이는 p라는 명령을 통해 함수를 만들면 해결됩니다.

하물며, 함수를 성공적으로 만들어주지 않으면 헥스레이는 동작하지 않습니다.


이제 헥스레이가 동작합니다 와아아아~~ > - <



그런데, main함수에서 그냥 바로 start를 호출하는 것을 볼 수 있네요.


start? 나 아까전에 start 주소 변경했는데? 라고 생각할 수도 있지만, sub_BD0으로 되어있지, start로 되어있지는 않다는 것을 위에서 재확인 하여 알 수 있습니다. 이미 해커가 정상 파일을 변조를 해두었기 때문에 그냥 단순히 함수 오프셋으로 call 을 하게 됩니다.

start에 들어가보면 Welcome Hacker ~ 이 녀석이 뜨게 됩니다. 이럴땐 어떻게 해야할까요?


헥스레이로 보지 않고, 어셈블리어로 보면서 패치할 부분을 패치하면 됩니다. 하나하나 같이 해보겠습니다.


jmp start를 jmp sub_14BD로 바꾸면 패치가 되겠지요~? 참 쉽습니다 핫핫핫


패치를 하고 다시 IDA로 실행해봅니다.

어라? 헥스레이가 안됩니다. 왜 안될까요? 지금 sub_14BD는 분명 함수로 되어 있는데 프롤로그가 없음을 확인할 수 있습니다. 그렇다면 이 녀석은 함수가 아니라 메인함수에 속하는 일반적인 지역변수일 수도 있겠네요. 필요없는 부분은 제거하면서 (nop sled) 다시 헥스레이가 적용되도록 바꾸어 보았습니다.


이제 제대로 인식 되네요~~ 홍홍



v25는 아이디를 입력 받는 변수이며, 이 변수를 이용하여 sub_1077을 호출합니다.

이 함수는 base64 encoding하는 함수입니다.

다시 main 함수로 돌아오면, sub_D92를 호출하는 부분을 볼 수 있습니다. 과감하게 접근 접근 !!

단순하게 한 바이트씩 비교를 하는 부분임을 확인하였습니다.


그렇다면, base64 encoding 한 값이 Senbtvaymde5이 되어야 할까요? 아닙니다.

정확한 덤프는 더블클릭해서 data에 접근해야 합니다.


이로써, 우리는 id 값을 획득했습니다.

pw를 찾으러 가봅시다 ~야호 ~~


패스워드에는 필터링이 적용되어 있습니다.

Python>"21402324255e262a2829".decode("hex")

!@#$%^&*()


이 부분은 숫자, 대문자, 소문자, 몇가지 특수 문자가 들어가는 패스워드 인지를 검증합니다.



sub_1186에서는 입력한 비밀번호를 통해 어떤 작업을 할까요?

단순한 xor을 취한 뒤, 5부터 84까지 반복문을 통해 5의 배수이면서 i값이 74이하인것이 만족한다면 v2에 특정 값을 넣는 구조입니다.


aGn과 byte_2012168은 테이블입니다. 즉, 특정 값들이 하드코딩 되어 있습니다. 이 값들을 잘 이용하면 문제를 풀 수 있을거라는 생각이 듭니다.


그전에,  s1 = byte_202102 ^ *a1 여기서부터 쫘라락 하드코딩 되어 있는 값은 ( 고정 값 xor 입력한 패스워드 한 바이트씩) 이라는 공식을 띱니다.


s1 = byte_202102 ^ *a1;

 byte_2021AB = byte_20214B ^ a1[11];

 byte_2021A3 = byte_202109 ^ a1[3];

 byte_2021AA = byte_202135 ^ a1[10];

 byte_2021A9 = byte_202134 ^ a1[9];

 byte_2021A5 = byte_202118 ^ a1[5];

 byte_2021AD = byte_20214D ^ a1[13];

 byte_2021A6 = byte_202132 ^ a1[6];

 byte_2021A1 = byte_202104 ^ a1[1];

 byte_2021A7 = byte_202132 ^ a1[7];

 byte_2021A4 = byte_20210A ^ a1[4];

 byte_2021A8 = byte_202133 ^ a1[8];

 byte_2021A2 = byte_202106 ^ a1[2];

 byte_2021AC = byte_20214C ^ a1[12];

위의 표에서 가장 낮은 주소가 202102 , 가장 높은 주소가 20214D임을 확인했습니다.

그렇다면, 우리는 0x201202 ~ 0x20214D 까지를 알아둘 필요가 있습니다.

byte_202102     db 64h && byte_20214D     db 2Ch


그리고 좌측의 변수들도 규칙이 있음을 눈치채야 합니다.

범위 : s1 ~ byte_2021A1 ~ byte_2021AD

사실상, s1은 byte_2021A0가 되는거고, 패스워드 길이는 14자리임을 알아냈습니다.


char password[15] = “\0”;  // byte_2021A0~byte_2021AD


Hex Editor를 이용할거에요.

Table의 원리를 알 수 있습니다. Table은 배열일테고, 배열의 마지막은 널임을 이용합니다. 그러면 이러한 결과를 알 수 있게 됩니다. “해커는 0x201202부터를 사용하지만, 실제 Table 주소는 그렇지 않다"


Gnd~ 08ad까지가 실제 테이블 입니다.

길이는 잘 모르니 대략 100정도로 잡아둡니다.

char table[100] = “\x47\x6E\x64\x6B\x33\x31\x30\x6B\x33\x39\x23\x61\x6B\x63\x26\x61\x6B\x63\x7B\x6B\x64\x69\x75\x67\x5F\x6B\x6E\x62\x69\x39\x31\x6C\x6B\x6E\x61\x39\x30\x31\x38\x31\x33\x6B\x52\x6E\x63\x6B\x65\x24\x35\x34\x31\x69\x61\x6B\x63\x6E\x61\x6C\x70\x69\x71\x6C\x64\x6A\x6C\x71\x69\x62\x6D\x61\x6F\x71\x70\x38\x37\x67\x31\x2C\x6D\x61\x30\x38\x61\x64“


hmm.. 여기서 끝이 아니네요? 또 다른 테이블이 사용됨을 확인할 수 있습니다.


byte_202168[dword_2021B0++];


Python>len("0D0E0A0D0B0E0E0F0E0E0B0D0A0E")/2

14


char xor_data[16] = “\x0D\x0E\x0A\x0D\x0B\x0E\x0E\x0F\x0E\x0E\x0B\x0D\x0A\x0E”;



for ( i = 5; i <= 84; ++i )

 {

   if ( !(i % 5) && i <= 74 )

     v2 += aGn[i] * byte_202168[dword_2021B0++];

 }


v2는 계속 가변적이다. 마치 c를 처음 배울 때 hap += n 이런 로직을 사용한다고 느낄 수 있습니다.


다시 한번 코드를 보면서 머리를 정리해보도록 하겠습니다.


__int64 __fastcall sub_1186(_BYTE *a1)

{

 unsigned int v2; // [rsp+10h] [rbp-8h]

 signed int i; // [rsp+14h] [rbp-4h]


 v2 = 0;

 s1 = byte_202102 ^ *a1;

 byte_2021AB = byte_20214B ^ a1[11];

 byte_2021A3 = byte_202109 ^ a1[3];

 byte_2021AA = byte_202135 ^ a1[10];

 byte_2021A9 = byte_202134 ^ a1[9];

 byte_2021A5 = byte_202118 ^ a1[5];

 byte_2021AD = byte_20214D ^ a1[13];

 byte_2021A6 = byte_202132 ^ a1[6];

 byte_2021A1 = byte_202104 ^ a1[1];

 byte_2021A7 = byte_202132 ^ a1[7];

 byte_2021A4 = byte_20210A ^ a1[4];

 byte_2021A8 = byte_202133 ^ a1[8];

 byte_2021A2 = byte_202106 ^ a1[2];

 byte_2021AC = byte_20214C ^ a1[12];

 for ( i = 5; i <= 84; ++i )

 {

   if ( !(i % 5) && i <= 74 )

     v2 += aGn[i] * byte_202168[dword_2021B0++];

 }

 return v2;

}



정리를 해보겠습니다.

  1. 패스워드를 매개변수로 삼는다.

  2. 패스워드와 특정 값을 xor 해서 새로운 배열에 집어 넣는다.

  3. aGn은 사실상 2번에서 특정 값 xor 테이블이다.

  4. 마지막으로 가져온 테이블의 한 바이트씩 3번 테이블의 한 바이트와 곱하여 v2에 대입한다.

  5. 반복문 다 돌고 난 v2 값을 반환한다.

패스워드의 조건이 숫자, 알파벳 대문자, 알파벳 소문자, 몇 개의 특수문자 중 하나 이런식으로 다 적용되어야 합니다. 패스워드는 14자리이면서 반드시 “한 번은" 위의 조건들이 쓰여야 합니다.

제가 이 문제를 만든 의도가 나오게 됩니다. 우리가 사용하는 대부분의 웹 사이트는 패스워드의 검증이 strongable 합니다. 이 패스워드 검증을 C 코드로 구현하여 문제를 제작하였습니다.



역연산을 진행해보기 위해 가젯들을 정리 해봅시다. 14자리라서 브루트포싱이나 angr, z3를 이용 해야할 것 같지만, 하지 않아도 됩니다.

시작하는 부분은 202100에 있는 값 G입니다.

; char unk_202100[2]

.data:0000000000202100 unk_202100      db 47h ; G ; DATA XREF: sub_1186+1B2↑o

.data:0000000000202101                 db 6Eh ; n

.data:0000000000202102 byte_202102     db 64h ; DATA XREF: sub_1186+F↑r

.data:0000000000202103                 db 6Bh ; k

.data:0000000000202104 byte_202104     db 33h ; DATA XREF: sub_1186+DB↑r

우리의 코드에서는 가장 첫번째 나오는 xor 타겟은 byte_202102이니까 [2]가 첫번째 xor에 취해지는 녀석입니다.


그 다음, 볼 코드는 이것입니다.

strcmp에서 첫번째 인자와 두번째 인자를 사용하게 되는데, 두 번째 인자는 .data 섹션에 위치해 있습니다.

char compare[100] = “\x65\x7A\x4D\x70\x38\x1A\x70\x43\x18\x28\x20\x34\x63\x2D”; // unk_2020E0


if ( strcmp(&s1, unk_2020E0) )

 {

   puts("wrong");

   exit(0);

 }


즉, 무조건 결과는 “\x65\x7A\x4D\x70\x38\x1A\x70\x43\x18\x28\x20\x34\x63\x2D”가 되어야 한다.


역연산 하는 방법은 우선 strcmp 하기 전에 거치는 xor 값을 구해야합니다. 이 값은 상당히 간단하게 구할 수 있습니다. 그 이유는 고정 된 값으로 연산을 진행하기 때문입니다.


for ( i = 5; i <= 84; ++i )

 {

   if ( !(i % 5) && i <= 74 )

     v2 += unk_202100[i] * byte_202168[dword_2021B0++];

 }

 return v2;


우리는 unk_202100 과 byte_202168의 배열을 알고 있으니 이렇게 코드를 짤 수 있습니다.


int set_xor()

{       

       int hap=0;

       static int j =0;

       

       for(int i=5; i<85; i++)

       {

               if(i%5==0 && i<75)

               {

                       hap += table[i] * loop[j];

                       j++;

               }

               else

                       continue;

       }

       // first encrypt success

       return hap;


}



전체 코드의 역연산 원리를 적용한 PoC 입니다.


#include <stdio.h>

#include  <stdlib.h>

#include <time.h>


// xor table

char table[100] = "\x47\x6e\x64\x6b\x33\x31\x30\x6b\x33\x39\x23\x61\x6b\x63\x26\x61\x6b\x63\x7b\x6b\x64\x69\x75\x67\x5f\x6b\x6e\x62\x69\x39\x31\x6c\x6b\x6e\x61\x39\x30\x31\x38\x31\x33\x6b\x52\x6e\x63\x6b\x65\x24\x35\x34\x31\x69\x61\x6b\x63\x6e\x61\x6c\x70\x69\x71\x6c\x64\x6a\x6c\x71\x69\x62\x6d\x61\x6f\x71\x70\x38\x37\x67\x31\x2c\x6d\x61\x30\x38\x61\x64";


char real_encrypt[100] = "\x65\x7A\x4D\x70\x38\x1A\x70\x43\x18\x28\x20\x34\x63\x2D";

char filter[256] = "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x61\x62\x63\x64\x65\x66\x67\x68\x69\x7a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x5a\x21\x40\x23\x24\x25\x5e\x26\x2a\x28\x29";


// loop table

char loop[15] = "\x0d\x0e\x0a\x0d\x0b\x0e\x0e\x0f\x0e\x0e\x0b\x0d\x0a\x0e";



int set_xor()

{

int hap=0;

static int j =0;


for(int i=5; i<85; i++)

       {

               if(i%5==0 && i<75)

               {

                       hap += table[i] * loop[j];

                       j++;

               }

               else

                       continue;

       }

       // first encrypt success

       return hap;


}


int main(int argc, char *argv[])

{

char input[14] = "\0";

int xor_val = 0;

char data = '0';

char cp_data = '0';

char flag[14] = "\0";

// 테이블을 이용해서 새로 세팅

char new_table[14] = "\0";

new_table[0] = table[2];

new_table[1] = table[4];

new_table[2] = table[6];

new_table[3] = table[9];

new_table[4] = table[10];

new_table[5] = table[24];

new_table[6] = table[50];

new_table[7] = table[50];

new_table[8] = table[51];

new_table[9] = table[52];

new_table[10] = table[53];

new_table[11] = table[75];

new_table[12] = table[76];

new_table[13] = table[77];

// get xor

xor_val = set_xor();

xor_val &= 0xff;

printf("xor val = %d\n",xor_val);


for(int i=0; i<14; i++) {

data = real_encrypt[i] ^ xor_val ^ new_table[i];

printf("%c",data);

}



}



패스워드를 여러 사이트에 똑같은 것으로 사용하는 사람이 많다는 것을 알고 있었습니다.

최근, 실제로 지인이 똑같은 패스워드를 여러 사이트에 등록해둬 피해를 입을 뻔한 사례가 있었습니다.

여러분은 각 사이트별로 다른 비밀번호를 사용하여 안전하게 개인 정보를 지킬 수 있기를 바랍니다.


소스코드입니다.

여기


끝~

'0x02 Reverse Engineer' 카테고리의 다른 글

ANTIDEBUG Problems  (0) 2019.02.18
HACKINGCAMP 19 Can You Login ?  (0) 2019.02.18
0 Comments
댓글쓰기 폼