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
'워게임 > 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 |
Uploaded by Notion2Tistory v1.1.0