블로그 이전했습니다. https://jeongzero.oopy.io/
[CodeEngn] Advance RCE L10
본문 바로가기
워게임/CodeEngn

[CodeEngn] Advance RCE L10

728x90

1. 문제


Serial이 WWWCCCJJJRRR 일때 Name은 무엇인가
Hint 1 : 4글자임
Hint 2 : 정답으로 나올 수 있는 문자열 중 (0~9, a~z, A~Z) 순서상 가장 먼저 오는 문자열

시리얼 넘버는 주어줬고, name을 찾아야한다.

패킹은 따로 안걸려 있다

2. 접근방법


int __cdecl main(int argc, const char **argv, const char **envp)
{
...

  v3 = alloca(16);
  __main();
  std::operator<<<std::char_traits<char>>((int)&std::cout, "Enter Your Name:   ");
  std::string::string((std::string *)name);
  std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, name, 10);
  name_size = std::string::size((std::string *)name);
  if ( name_size <= 3 && name_size )
  {
    v4 = std::operator<<<std::char_traits<char>>((int)&std::cout, "Name must be longer than 3 chars.");
    std::ostream::operator<<(v4, std::endl<char,std::char_traits<char>>);
    system("PAUSE");
    std::string::~string(v5);
  }
  else if ( name_size )
  {
    std::operator<<<std::char_traits<char>>((int)&std::cout, "Enter Your Serial: ");
    std::string::string((std::string *)serial);
    std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, serial, 10);
    if ( std::string::size((std::string *)serial) )
    {
      if ( std::string::size((std::string *)serial) == 12 )
      {
        std::string::string((std::string *)serial_, (const std::string *)serial);
        std::string::string((std::string *)name_, (const std::string *)name);
        v20 = check_serial(name_, serial_);
        std::string::~string(v14);
        std::string::~string(v15);
        if ( v20 )
          v16 = std::operator<<<std::char_traits<char>>(
                  (int)&std::cout,
                  "Good job. Now write a keygen and tutorial if you have the time.");
        else
          v16 = std::operator<<<std::char_traits<char>>((int)&std::cout, "Wrong serial. Keep trying.");
        std::ostream::operator<<(v16, std::endl<char,std::char_traits<char>>);
        system("PAUSE");
        std::string::~string(v17);
        std::string::~string(v18);
      }
  ...
  }
  return 0;
}

정리하자면 Name은 4이상의 사이즈를 입력해야하며 Serial 은 12자리를 반드시 입력해야한다. 입력한 name과 serial을 인자로 check_serial() 함수를 호출하는데, 호출 결과값이 참이 되야지 성공 메시지가 나온다. 검증 함수를 확인해보자

int __cdecl check_serial(std::string *name, std::string *serial)
{
....

  std::allocator<char>::allocator(buf);
  std::string::string(
    (int)key,
    "AJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLNAJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLNAJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLN",
    (int)buf);
  std::allocator<char>::~allocator(buf);
  std::allocator<char>::allocator(v26);
  std::string::string((int)buf, (char *)&byte_443070, (int)v26);
  std::allocator<char>::~allocator(v26);
  std::allocator<char>::allocator(key_);
  std::string::string((int)v26, (char *)&byte_443070, (int)key_);
  std::allocator<char>::~allocator(key_);
  std::string::string((std::string *)check, name);
  StringToUpper(key_);
  std::string::operator=(name, (std::string *)key_);
  std::string::~string(v2);
  std::string::~string(v3);
  std::string::string((std::string *)key_, serial);
  StringToUpper(check);
  std::string::operator=(serial, (std::string *)check);
  std::string::~string(v4);
  std::string::~string(v5);
  v23 = 1;
  for ( i = 0; i <= 3; ++i )
  {
    v17 = std::string::length(name) >> 2;
    v7 = std::string::length(name);
    v18 = (__int64)(floor((double)(v17 + i * (v7 >> 2))) - 1.0);
    v8 = (char *)std::string::at(name, v18);    // name+v18 오프셋 주소
    std::string::operator=((std::string *)buf, *v8);
    for ( j = 3 * i; 3 * i + 3 > j; ++j )
    {
      serial_j = (char *)std::string::at(serial, j);// serial+j 주소
      std::string::operator=((std::string *)v26, *serial_j);
      std::string::string((std::string *)check, (const std::string *)v26);
      std::string::string((std::string *)key_, (const std::string *)key);
      address_secnod_serial_from_key = stringFindSecond(key_, check);// key에서 input_serial 한바이트 주소구함
      std::string::~string(v10);
      std::string::~string(v11);
      std::string::string((std::string *)check, (const std::string *)buf);
      std::string::string((std::string *)key_, (const std::string *)key);
      address_secnod_name_from_key = stringFindSecond(key_, check);// key에서 input_name 주소구함
      std::string::~string(v12);
      std::string::~string(v13);
      if ( (int)std::abs(address_secnod_serial_from_key - address_secnod_name_from_key) > 5 )
        v23 = 0;                                // 일로 빠지면 안됌
    }
  }
  std::string::~string(v6);
  std::string::~string(v14);
  std::string::~string(v15);
  return v23;
}

코드는 생각보다 간단해서 흐름 파악하고 바로 파이썬으로 포팅시키려했다. 하지만 어셈으로 분석하면서 실제보니 부동소수점과 관련된 명령어들이 많아서 하다가 욕이 나와 버렸다.

.text:004017FB                 fild    [esp+140h+var_140]
.text:004017FE                 lea     esp, [esp+8]
.text:00401802                 fstp    qword ptr [esp+138h+lpfctx] ; X
.text:00401805                 call    _floor
.text:0040180A                 fld1
.text:0040180C                 fsubp   st(1), st
.text:0040180E                 fnstcw  [ebp+var_B2]
.text:00401814                 movzx   eax, [ebp+var_B2]
.text:0040181B                 or      ax, 0C00h
.text:0040181F                 mov     [ebp+var_B4], ax
.text:00401826                 fldcw   [ebp+var_B4]
.text:0040182C                 fistp   [ebp+var_C0]
.text:00401832                 fldcw   [ebp+var_B2]
.text:00401838                 mov     eax, dword ptr [ebp+var_C0]
.text:0040183E                 mov     edx, dword ptr [ebp+var_C0+4]
.text:00401844                 mov     [esp+138h+lpfctx+4], eax ; unsigned int
.text:00401848                 mov     edx, [ebp+name]

FPU 레지스터에 들어가는 값들을 확인하면서 + 부동소수점 관련 명령어를 공부하면서 시간을 날렸다. 결국 저 부분은 사실 볼 필요가 없었다. 메인 핵심 로직은 for문 안이다.

 for ( i = 0; i <= 3; ++i )
  {
    v17 = std::string::length(name) >> 2;
    v7 = std::string::length(name);
    v18 = (__int64)(floor((double)(v17 + i * (v7 >> 2))) - 1.0);
    v8 = (char *)std::string::at(name, v18);    // name+v18 오프셋 주소
    std::string::operator=((std::string *)name, *v8);
    for ( j = 3 * i; 3 * i + 3 > j; ++j )
    {
      serial_j = (char *)std::string::at(serial, j);// serial+j 주소
      std::string::operator=((std::string *)v26, *serial_j);
      std::string::string((std::string *)check, (const std::string *)v26);
      std::string::string((std::string *)key_, (const std::string *)key);
      address_secnod_serial_from_key = stringFindSecond(key_, check);// key에서 input_serial 한바이트 주소구함
      std::string::~string(v10);
      std::string::~string(v11);
      std::string::string((std::string *)check, (const std::string *)name);
      std::string::string((std::string *)key_, (const std::string *)key);
      address_secnod_name_from_key = stringFindSecond(key_, check);// key에서 input_name 주소구함
      std::string::~string(v12);
      std::string::~string(v13);
      if ( (int)std::abs(address_secnod_serial_from_key - address_secnod_name_from_key) > 5 )
        v23 = 0;                                // 일로 빠지면 안됌
    }
  }

이중 포문을 돌면서 뭐를 하는데 stringFindSecond 함수가 두번 호출되는걸 볼 수 있다. 첫번째 함수에서는 입력한 serial 값 한바이트와 초반에 주어진 엄청 긴 문자열의 주소를 가지고 호출된다

int __cdecl stringFindSecond(std::string *key, std::string *buf)
{
  int v2; // eax

  v2 = std::string::find(key, buf, 0);
  return std::string::find(key, buf, v2 + 1);
}

말그대로 key의 문자열에서 buf에 담긴 값의 주소를 구하는 함수이다. 자세히는 검색된 두번째 오프셋이 반환된다. 만약 key가 " abcdabdf " 이고 buf에 a가 담겼다고 해보자.

현재 a가 오프셋 0, 오프셋 4 두곳에 존재하고 첫 번째가 아닌 두 번째 오프셋인 4가 반환되는 것이다.

디버깅 결과를 통해서 위 과정을 알게되었다. 실제 확인해보자

3. 풀이


  • 입력 Name : abcd
  • 입력 Serial : 123456789123

첫번째 stringFindSecond 함수이다. 첫 번째 인자에는 엄청 긴 key 문자열 주소가 들어있고, 두번째 인자에는 입력한 serial이 담긴 주소가 들어있다. 그 주소를 덤프창에서 확인해보면 실제 입력한 serail의 첫번째 값이 나오는 걸 볼 수 있다.

AJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLNAJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLNAJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLN

현재 두번째 1의 오프셋이 0x43이다. 따라서 해당 함수에서는 0x43을 반환해줄 것이다

정확하다. 이번엔 Name을 봐보자

첫번째 인자는 동일하게 긴 문자열이고, 두번쨰는 0x66fdc0주소이다. 해당 주소 안에는 Name의 첫바이트 주소가 들어가있다. 그 주소를 확인해보면, 그림과 같이 입력한 Name의 첫바이트 A 가 들어가있다.

AJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLNAJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLNAJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLN

현재 A는 저렇게 오프셋 0과 오프셋 0x25에 위치해 있다. 그렇다면 두 번째 함수는 0x25를 반환 할 것이다.

정확하다.

지금 현재 이중 for문을 돌면서 체킹한다. 바깥 for문에는 name을 기준으로 안쪽 for문이 총 3번 돌아가는데 이 말은

name 한바이트를 기준으로 serial 3바이트를 비교한다는 뜻이다.

내가 입력했던 값 기준으론

  • 입력 name : abcd
  • 입력 serial : 123456789123

요게 다음과 같이 비교를 진행한다

  • a, a, a < = > 1,2,3
  • b, b, b < = > 4,5,6
  • c, c, c < = > 7,8,9
  • d, d, d < = > 1,2,3

      std::string::~string(v13);
      if ( (int)std::abs(address_secnod_serial_from_key - address_secnod_name_from_key) > 5 )
        v23 = 0;                                // 일로 빠지면 안됌
    }
  }

그다음 찾은 serial offset - name offset을 진행하고 그 결과를 절대값으로 표현하는데 그 값이 5보다 큰 경우 v23=0이된다.

결국 최종적으로 v23이 반환되는데 초기값은 1이다. 따라서 저 분기문으로 들어오지 못하게

  • | serial offset - name offset | ≤ 5

가 되게끔 해야한다.

그럼 가장 간단하게 생각 할 수 있는게 지금 serial 값은 정해졌으므로 name 오프셋을 시리얼 오프셋과 동일하게 입력한다면 같은 값의 차이는 0이므로 조건을 통과할 것이다

key = "AJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PD ECL NA J XG R F V 6BK O W 3Y9TM 4S2ZU I70H5Q 81PDE C LNAJX GRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLN"

name : wcjr
serial : WWWCCCJJJRRR

serial : W, W, W
name : w, w, w

serial : C, C, C
name : C, C, c

serial : J, J, J
name : j, j, j

serial : R, R, R
name : r, r, r

저렇게 입력하면 성공이긴 하지만 사이트에서 인증은 되지 않는다. 왜냐하면 두 오프셋 차이가 0~5 사이 값이기만 하면 되므로 여러 경우의 수가 존재한다. 그리고 인증되는 플래그는

정답으로 나올 수 있는 문자열 중 (0~9, a~z, A~Z) 순서상 가장 먼저 오는 문자열

이다. 따라서 다음처럼 노가다를 해보았다

key = "AJXGRFV6BKOW3Y9TM4S2ZU I70H5Q81PD ECL NA J XG R F V 6BK O W 3Y9TM 4S2ZU I70H5Q 81PDE C LNAJX GRFV6BKOW3Y9TM4S2ZU I70H5Q81PDECLN"

W : 25 //오프셋은 가정임

1. 25-24 : O
2. 25-23 : K
3. 25-22 : B
4. 25-21 : 6
5. 25-20 : V
6. 25-26 : 3 // select
7. 25-27 : Y
8. 25-28 : 9
9. 25-29 : T
10. 25-30 : M

C : 25
1. 8 1 P D E
2. L N A J X
=> 1 select

J : 25
1. E C L N A
2. X G R F V
=> A select

R : 25
1. N A J X G
2. F V 6 B K
=> 6 select

w의 오프셋이 25라고 가정하고, 0~5 사이의 오프셋으로 나올 수 있는 값을 추려봤다.

  • W 기준 좌우 +5,-5 값 가능. 그중에서 가장 빠른 순서 선탱 ⇒ 3
  • C 기준 ⇒ 1
  • J 기준 ⇒ A
  • R 기준 ⇒ 6

결과적으로 31A6 가 가장 순서적으로 빠른 문자열이다.

4. 몰랐던 개념


  • fild

    fild st0, qword ptr ss:[esp] // [esp] 의 값을 st(0) 레지스터에 push

  • fstp

    fstp qword ptr ss:[esp], st0 // st0 레지스터 값을 [esp]에 복사하고 st스택 pop

  • fld1

    st0 레지스터에 1 push

728x90

'워게임 > CodeEngn' 카테고리의 다른 글

[CodeEngn] Advance RCE L09  (0) 2021.02.04
[CodeEngn] Advance RCE L08  (0) 2021.02.02
[CodeEngn] Advance RCE L07  (0) 2021.02.02
[CodeEngn] Advance RCE L06  (0) 2021.01.30
[CodeEngn] Advance RCE L05  (0) 2021.01.29