2018. 8. 3. 22:53ㆍ0x07 CTF/[스스로 푼 것]
CTF가 끝난 지는 조금 되었지만, 틈틈이 시간을 내어 바이너리를 분석해본 결과 플래그를 획득할 수 있었다.
노력은 배신하지 않는다 !!
출제자의 문제 concept
스레드에서 breakpoint(0xCC)를 이용하여 프로그램을 종료하는 로직을 사용
md5를 너무 너무 사랑함 (이 CTF rev 제일 처음 문제도 md5 였음 ㅋㅋ)
5단계 마다 루틴이 다 다름
한눈에 보는 KEY 값
1round : 2033218911
2round : 1664380511
3round : 1647600432
4round : 1835360107
5round : 829307169
플래그 추출 루틴 분석 뿐만 아니라 출제자가 어떤 식으로 코드를 작성했는지 궁금해서 이것 저것 둘러보았다.
출제자의 습관도 알 수 있을 뿐만 아니라, 역공학 시나리오를 작성하거나, 혹은 상상할 때 약간의 도움이 되지 않을까 해서 이다.
(대회 때는 이렇게 하면 손해~일 것 이다) 몇일 걸렸는지 모르겠지만, 이런 유형 흐름 파악은 이제 쉬울 것 같다..
[시작]
메인함수에 접근하게 되면 제일 먼저 눈에 띄는 작업이 Thread를 생성하는 것이다.
그렇다면, 이 바이너리는 Thread를 이용해서 무엇 인가를 할 것이라는 추측을 할 수 있다.
그 다음 보이는 함수가 Sleep함수
디버거를 이용하여 step over를 진행하게 되면 100초를 멍하게 기다려야 한다.
참을 성이 없기 때문에 가볍게 패치를 진행한다.
그 다음 루틴을 살펴보면 다음과 같다.
그렇다면 지금 까지 알 수 있는 정보
출제자는 2개의 스레드를 가지고 놀 것이다.
스레드는 생성에 성공하게 되면 커널 오브젝트 핸들이 반환 되고, 그렇지 않은 경우는 가차 없이 NULL을 뿌려댄다.
현 바이너리에서는 생성에 성공을 했으므로 각각 핸들이 반환 됨을 확인할 수 있다.
[1]movaps xmm0, dqword ptr [0x403240]
movaps 명령어는 [0x403240]의 값을 XMM 레지스터로 값을 load하기 위해 사용하고 있다.
[0x403240]주소에는 0x390E0E0E0E0E0E7358515C5C555E5755가 들어있다.
dqword이기 때문에 128비트 (16바이트)
xmm0의 값을 Ollydbg에서 어떻게 보는지 몰랐다.
하지만 걱정하지 않아도 된다.
[2] movups dqword ptr [ebp-0x24], xmm0
이번에는 load가 아니라 store를 진행하고 있다.
load와 store는 이렇게 생각하면 편할 것 같다.
A라는 게임을 하다가 그만하고 다음에 이어서 진행하고 싶을 때 우리는 "저장"이라는 키워드를 이용한다.
"저장" 곧, 백업이다.
메모리구조에서 백업이라는 것은 스택에 값을 잠시동안 모셔두는 행위라고도 볼 수 있다.
반면 , load는 스택 혹은 메모리 덤프에 있는 데이터를 다른 행위에 사용하기 위해 "일시적 쉼터"인 레지스터에 가져오는 행위를 일컬을 것이다.
위의 그림과 같은 flow가 진행되어 mov al, byte ptr [ecx]가 실행 될 수 있는 것이며 ecx가 1바이트 씩 증가하다가 마지막 비트를 가리키게 되면 \00이 되어 test 연산을 거친 후 조건에 맞게 분기 하게 된다.
아직은 이 값이 중요한지 중요한 값이 아닌지 그닥 궁금하지 않다.
이 부분에서 0x39 ^ 0x30 을 거쳐 0x9가 되는 것은 확인을 하였고, 깨져 있는 opcode를 정렬시켜 보니 unknown 이 떠버렸다.
0x4018BC에 접근 시 어떠한 명령을 하는지 알 수 없기 때문에 Error가 뜬다 흑흑 ..
패치도 진행 않고 연산을 그대로 따라갔을 뿐인데 이런 결과가 떠서 정적분석을 겸하기로 하였다.
ida로 보면 nop dword ptr [eax+00h]라고 되어 있었다.
hexlay로 보면 v3이 v14의 길이를 조건 값으로 사용하고, 특정 연산이 만족할 시 v3이 증가한다는 것을 볼 수 있었다.
즉, v3은 v14의 길이보다 작아야만 한다.
만약 v14의 길이가 14이고 v3이 증가함에 따라 14가 되게 된다면 v3 < 14 조건에 만족하지 못하므로 for문을 빠져나올 수 있게 된다.
v14는 load_data로 수정하고, v3는 include_len으로 수정해서 분석이 좀 더 효율적 일 수 있게 하였다.
해당 루틴이 정상적으로 진행되고 나면, 초기세팅 즉, 해당 바이너리 분석의 시작점 이라고 판단할 수 있는 부분을 호출하게 된다.
여기에서 조금 프로그래밍 분석 능력이 낮았다는 것을 실감하기도 했었다.
pipe\\LogPipe가 특정 경로라고 생각하고 \\\\는 사실상 신경 쓰지 않고 현 경로에 pipe 디렉터리를 생성하고 LogPipe라는 바이너리를 생성해보았었다.
이렇게 한 이유는 CreateFile이라는 API함수가 특정 경로의 파일의 핸들을 반환하는 것이고, 그 반환 받은 핸들을 이용해서 특정 작업을 한 뒤 LogPipe에 Log를 심어준다고 예상해봤기 때문이다.
하지만, CreateFile은 파일 뿐만 아니라, 입출력 장치 또한 열 수 있다는 것을 뒤늦게 상기하면서 이 삽질은 더 이상 고려하지 않게 되었다.
pipe에 대해 다시 한번 생각해보는 좋은 시간이었다.
해당 루틴은 이해가 바로 되지 않아서 바이너리를 실행해보았다.
바이너리를 실행하면 5개의 숫자를 입력하면 플래그를 준다고 해놓고, 바로 종료가 되어버리는 것을 발견하였다.
이거는 알기까지 조금 오래 걸렸다. 2일 쯤 걸린 것 같다.
우선 처음 진행해본 방식은 다음과 같다.
String Reference를 이용하여 적절한 곳에 software breakpoint 베이스캠프를 설치하자.
이 방법으로는 저 문자열들을 볼 수가 없었다.
문자열이 어떻게 발생하는지 원리를 알아보다가 Thread의 역할 까지 깨우치게 되었다.
StartAddress와 set_CC를 잘보아야 한다.
set_CC는 함수이다.
이 함수에 접근하면 이런 연산이 존재한다. init_func함수에서 반환 된 값과 시작 주소의 값을 소거한 데이터를
또 다른 함수에 매개변수로 이용하는 모습을 볼 수 있다.
0x55 ^ 0x99를 하게 되면 0xCC 즉 INT3 (Software breakpoint)가 만들어 지게 된다.
while 조건문에서 연산을 수행 한 후 0x99와 일치 여부를 판가름 하고, if문으로 분기할 것인지 1을 반환할 것 인지를 결정 짓는다.
다시 함수를 곱씹어보면, ( StartAddress의 시작주소 + offset ) ^ 0x55 의 값이 0xcc인지를 판별하고
0xcc가 아니라면, 1을 반환 하는데 !breakpoint_set(...) 이기 때문에 not 연산자로 인해 1->0이 되어 버리며 결국 프로그램을 강제로 종료하는 exit함수를 호출 받게 된다.
while문의 위치는 어디인가 확인해 보았더니 이전 루틴에서 ??? 를 봤듯이 이 부분도 잘못 된 instruction을 불러오고 있어서 nop 패치를 진행해두었다.
그런데 중요한 것은 이 루틴까지 도달할 순 없었다.
그 이유는 또 다른 루틴에서 나를 괴롭히고 있었기 때문이다.
이 부분이 문자열을 출력하는 부분임을 인지하였다.
여기로 가는 여정 중에 add byte ptr [edx+0xc0844101],cl 에서 참조할 수 없는 값을 얻어오기 때문에 프로세스가 termitated가 되었기 때문에 nop 패치도 진행했다.
그러면서 아까 관심 없이 지나친 문자열을 또 발견하게 되었다.
#include <stdio.h>
int main(int argc, char*argv[])
{
char arr[] = "\x0E\x0E\x0E\x0E\x0E\x0E\x73\x58\x51\x5C\x5C\x55\x5E\x57\x55\x10\x53\x42\x51\x53\x5B\x0C\x0C\x0C\x0C\x0C\x0c";
for (int i = 0; i < sizeof(arr) / sizeof(arr[0])-1; i++)
{
arr[i] ^= 0x30;
printf("%c", arr[i]);
}
}
두 번째 문자열도 이런 식으로 출력하면 된다.
두 번째는 0x21과 XOR연산을 진행하였었다.
스레드를 이용하는 이유는 2개의 행동을 동시에 하기 위함으로 보여진다.
이제 문자열에 대한 의문점은 풀렸으니 0xCC를 우회하기 위한 작업을 해야 한다.
그래야 숫자를 입력하면서 동적분석을 할 수 있기 때문이다.
CreateThread2번을 없애버렸다.
00401863 push eax ; lpThreadId
.text:00401864 push 0 ; dwCreationFlags
.text:00401866 push 0 ; lpParameter
.text:00401868 push offset set_CC ; lpStartAddress
.text:0040186D push 0 ; dwStackSize
.text:0040186F push 0 ; lpThreadAttributes
.text:00401871 call esi ; CreateThread
문자열은 깨져버린다 xor 하는 부분까지 nop 처리했기 때문이다.
그래도 이제 입력할 수 있게 되었다.
또 다른 분석을 하러 가보자.
이제 본격적인 분석이 가능하다.
지금 5자리를 입력했는데 You Loseeee가 출력 되었다.
브레이크 포인트를 걸고 진행해보자. 그 전에 네이밍을 했다.
뭔가 매끄럽지 않은 분석이 되어서 2번째 Thread 부분에서 호출 된 함수의 instruction을 IDA와 동일하게 패치를 진행했다.
후 배점이 높은 이유를 서서히 알아간다. 아직 패치에 대해 미숙함을 느끼면서 분석을 계속 진행한다.
IDA에서 보는 명령과 동일할 수 있게 작업을 진행했다. 바이너리 diffing을 어서 만들어서 좀 편하게 작업을 해야겠다...
Thread 내부에 접근하기 위해 Hardware breakpoint를 적극 이용해야 한다.
TIP : PUSH 0x4017F0을 trace 한 다음에 hb를 걸면 다시 Access 할 때 pause 상태가 될 수 있다.
ADDR 4015C0 : 53 8B DC 83
ADDR 4012E0 : 55 8B EC 81
mov edx, 004015C0
sub edx, 004012E0
4015C0 -4012E0 => 2E0
이 루틴을 마치고 여러 루틴을 수행하다보면 ollydbg상태에서 바이너리가 Running상태로 지속되게 되는데
이 부분을 IDA로 확인하면 unconverted 상태임을 확인 할 수 있고 이를 convert 시킴으로써 새로운 루틴을 볼 수 있게 된다.
convert 하게 되면 이상한 jmp가 있는데 안티헥스레이로 보여져서 패치를 수행하였다.
헥스레이로 보이지 않던 ConnectNamedPipe 함수가 보이기 시작한다.
ConnectNamedPipe함수는 WIN32 GUI프로그램에서의 SendMessage와 동일한 역할을 하는 것 같다.
지금 러닝상태가 지속된 이유는 연결이 안 됬기 때문이 아닐까..
디버깅이 더이상 안되니 고민을 해보았다.
우선 우리가 입력해야 하는 숫자 횟수가 5회 그렇다면 5번의 검증이 있지 않을까 하고 생각하고 분석을 진행했다.
우선 ,
.text:00401366 cmp dword ptr [ebp-40Ch], 0
.text:0040136D mov [ebp-420h], esi
.text:00401373 jbe short loc_401340 // 분기
이 부분에서 만족 못하면 ConnectNamedPipe로 간다
만족 했을 때
lea esi, [ebp-404h]
.text:0040137B xor eax, eax
.text:0040137D xor ebx, ebx
.text:0040137F lea edx, [esi+1]
.text:00401382 xor edi, edi
loc_401384: ; CODE XREF: .text:00401389↓j
.text:00401384 mov cl, [esi]
.text:00401386 inc esi
.text:00401387 test cl, cl
.text:00401389 jnz short loc_401384 // 분기
.text:0040138B sub esi, edx
.text:0040138D test esi, esi
.text:0040138F jle short loc_4013AC // 분기
[1] LOC_401384
ESI값을 증가시키면서 \0을 만날 때 까지 루프 돔
[2] LOC_4013AC
loc_4013AC: ; CODE XREF: .text:0040138F↑j
.text:004013AC mov esi, [ebp-420h]
.text:004013B2 cmp esi, 1 // ESI와 1을 비교
.text:004013B5 jnz short loc_40142C // 일단 보류
.text:004013B7 cmp ebx, 1Eh // 비교
.text:004013BA jnz loc_40157D
.text:004013C0 mov cl, al
.text:004013C2 xor cl, bl
.text:004013C4 mov ebx, eax
.text:004013C6 mov [ebp-411h], cl
.text:004013CC mov ecx, eax
.text:004013CE sar eax, 18h // 의심
.text:004013D1 xor al, 1Eh
.text:004013D3 sar ecx, 10h
.text:004013D6 movzx edx, al
.text:004013D9 xor cl, 1Eh
.text:004013DC shl edx, 8
.text:004013DF sar ebx, 8
.text:004013E2 movzx eax, cl
.text:004013E5 xor bl, 1Eh
.text:004013E8 or edx, eax
.text:004013EA movzx eax, bl
.text:004013ED shl edx, 8
.text:004013F0 or edx, eax
.text:004013F2 movzx eax, byte ptr [ebp-411h]
.text:004013F9 shl edx, 8
.text:004013FC or edx, eax
.text:004013FE cmp edx, 672E6B41h
[3] loc_40157D : // 실패 구문으로 보여짐
loc_40157D: ; CODE XREF: .text:004013BA↑j
.text:0040157D ; .text:loc_401404↑j ...
.text:0040157D mov esi, [ebp-410h]
.text:00401583 lea eax, [ebp-408h]
.text:00401589 push 0
.text:0040158B push eax
.text:0040158C push 2
.text:0040158E push offset a0 ; "0"
.text:00401593 push esi
.text:00401594 call ds:WriteFile
.text:0040159A push esi
.text:0040159B call ds:CloseHandle
.text:004015A1 mov ecx, [ebp-4]
.text:004015A4 xor eax, eax
.text:004015A6 pop edi
.text:004015A7 pop esi
.text:004015A8 xor ecx, ebp
.text:004015AA pop ebx
.text:004015AB call @__security_check_cookie@4 ; __security_check_cookie(x)
.text:004015B0 mov esp, ebp
.text:004015B2 pop ebp
.text:004015B3 retn 4
.text:004015B3 ; ----------------------------------------
[4]
cmp edx, 672E6B41h // [2]에서 이어짐
.text:00401404
.text:00401404 loc_401404: ; CODE XREF: sub_401506-47↓j
.text:00401404 jnz loc_40157D
.text:0040140A
.text:0040140A loc_40140A: ; CODE XREF: sub_401506+13↓j
.text:0040140A mov ebx, [ebp-410h]
.text:00401410 lea eax, [ebp-408h]
.text:00401416 push 0 ; lpOverlapped
.text:00401418 push eax ; lpNumberOfBytesWritten
.text:00401419 push 2 ; nNumberOfBytesToWrite
.text:0040141B push offset a1 ; "1"
.text:00401420 push ebx ; hFile
.text:00401421 call ds:WriteFile
.text:00401427 jmp loc_401340
대충 이정도 보면, shl, sar 이런걸로 연산을 하고 특정 값과 비교하는 부분이 있다.
이런 것은 p를 통해 함수로 만들 수 있다고 한다.
아는 동생에게 물어봤는데 알려주었다. 덕분에 분석이 좀 더 수월해졌다.
ConnectNamedPipe 함수가 보인다.
이제 엄청난 연산들이 기다리고 있다.
분별해보았다.
if ( suntak == 1 )
{
JUMPOUT(sum, 0x1E, &loc_40157D); // hex값의 각 자릿수 를 더했을 때 나와야 하는 수
*(_BYTE *)(a2 - 1041) = sum ^ CALC_VAL;
prove_loop = (*(unsigned __int8 *)(a2 - 1041) | (((unsigned __int8)(BYTE1(CALC_VAL) ^ 0x1E) | (((unsigned __int8)(BYTE2(CALC_VAL) ^ 0x1E) | ((unsigned __int8)(HIBYTE(CALC_VAL) ^ 0x1E) << 8)) << 8)) << 8)) == 0x672E6B41;
goto LABEL_8;
}
LABEL_8:
JUMPOUT(!v8, &loc_40157D); // LOC_40147D의 주소와 같지 않으면 아웃
}
JUMPOUT(suntak, 4, sub_40151E); // 어떤 함수로 또 진입
JUMPOUT(v4, 34, &loc_40157D);
v13 = make_hash((BYTE *)(a2 - 1028));
const_hash = "e861a6e17bd11a7cec8b6c8514728d2b";
v15 = v13;
do
{
a1 = *v15 < (const unsigned __int8)*const_hash;
if ( *v15 != *const_hash )
goto LABEL_1;
if ( !*v15 )
break;
v16 = v15[1];
a1 = v16 < const_hash[1];
if ( v16 != const_hash[1] )
goto LABEL_1;
v15 += 2;
const_hash += 2;
}
while ( v16 );
}
e861a6e17bd11a7cec8b6c8514728d2b : MD5 : 1835360107 임을 알아 냈다. (by hashkiller)
하지만 이 md5값은 1라운드의 값이 아니다.
---------------------------- 아래의 연산이 1라운드의 값이 된다 ------------------------------------------------------------------------------
JUMPOUT(sum, 0x1E, &loc_40157D); // hex값의 각 자릿수 를 더했을 때 나와야 하는 수
*(_BYTE *)(a2 - 1041) = sum ^ CALC_VAL;
prove_loop = (*(unsigned __int8 *)(a2 - 1041) | (((unsigned __int8)(BYTE1(CALC_VAL) ^ 0x1E) | (((unsigned __int8)(BYTE2(CALC_VAL) ^ 0x1E) | ((unsigned __int8)(HIBYTE(CALC_VAL) ^ 0x1E) << 8)) << 8)) << 8)) == 0x672E6B41;
언뜻보면 복잡해 보이지만, 4바이트 씩 0x1E xor 하면 된다.
리틀엔디언 변환 을 [A][B][C][D]로 가정하면
(*(unsigned __int8 *) [D]
(((unsigned __int8)(BYTE1 [C]
(((unsigned __int8)(BYTE2 [B]
((unsigned __int8)(HIBYTE [A] 가 된다.
0x672E6B41 ^ 0x1e1e1e1e = ??
만족한다.
테스트 해보자.
2라운드 너무 어려웠다. 변수 명 몇 번을 고치면서 우왕좌왕 했다.
if ( suntak != 2 )
break;
JUMPOUT(sum, 0x23, &loc_40157D);
v10 = 0;
*(_DWORD *)(arr - 0x41C) = 0x40C211F;
*(_WORD *)(arr - 0x418) = 0x1F18;
JUMPOUT(CALC_VAL, 0, &loc_40157D); // calc_val 즉 더한 값이 0보다 작거나 같으면
// .text:0040144F test eax, eax
// .text:00401451 jle loc_40157D
do
{
COPY_CALC_VAL = CALC_VAL;
CALC_VAL /= 0x23;
v12 = *(unsigned __int8 *)(arr + v10++ - 0x41C);
JUMPOUT(COPY_CALC_VAL % 0x23, v12, &loc_40157D);
}
while ( CALC_VAL > 0 );
JUMPOUT(v10, 6, &loc_40157D);
handle = *(void **)(arr - 0x410);
WriteFile(*(HANDLE *)(arr - 0x410), L"1", 2u, (LPDWORD)(arr - 0x408), 0);
suntak = *(_DWORD *)(arr - 0x420);
1라운드와 연산 과정이 다르다.
#include <stdio.h>
#include <Windows.h>
typedef struct {
int index_1;
int index_2;
} arr_Part; // arr - 0x41C 와 arr - 0x418 구분 짓는 index_1 , 2
// 구조체는 align 가능 !!
void round2();
int main()
{
round2();
}
void round2() {
int limit, index_1, index_2, calc_val, num, val, i;
arr_Part arr_Part;
// 4294967295 (int) => 0xFFFFFFFF
for (i = 0; i < 0xFFFFFFFF; i++) {
num = i; // num으로 반복문 수행하면 나누기 0x23에서 꼬여버림.
limit = 0; // 스코프 안에 있어야 계속 초기화 가능
// fixed
// example arr = 0x420이라면
// 0x41c = > [4]
// 0x418 = > [8]
// [4][5][6][7][8] : 5bytes
arr_Part.index_1 = 0x40C211F;
arr_Part.index_2 = 0x1F18; // limit 증가하면서 이 주소로 갈 수 있음
// debug mode
//printf("%p\n", &arr_Part.index_1);
//printf("%p\n", &arr_Part.index_2);
//system("pause");
if (num > 0)
{
while (1)
{
calc_val = num; // copy
num /= 0x23; // div 0x23
val = *(char*)((size_t)&arr_Part.index_1 + limit++);
if ((calc_val % 0x23) != val) // calc_val 값이 0x23와 나누어 떨어지는지?
break;
if (num <= 0)
{
if (limit != 6) { // -> 위의 limit++에서 6이 되어버리면 fail 고로 5비트
printf("FAIL\n");
break;
}
printf("congratulation %d\n", i);
return;
}
}
}
}
}
2단계 까지 테스트
후 통과 됬다.
if ( suntak != 3 )
break;
JUMPOUT(sum, 0x21, &loc_40157D);
prove_loop = ((sum + CALC_VAL) ^ 0xCAFEBABE) == 0xA8CAD9EF;
LABEL_8:
JUMPOUT(!prove_loop, &loc_40157D);
}
3라운드는 쉬어가는 코너다. 가벼운 역연산 2라운드 고생했다고 상 주나보다 ㅎㅎ
0xA8CAD9EF ^ 0xCAFEBABE - 0x21;
JUMPOUT(suntak, 4, sub_40151E); // 4라운드 아니면 종료 곧 이 말은 여기가 4라운드 지역
JUMPOUT(sum, 34, &loc_40157D);
v13 = make_hash((BYTE *)(arr - 1028));
const_hash = "e861a6e17bd11a7cec8b6c8514728d2b"; // 아까 본 해쉬
v15 = v13;
do
{
a1 = *v15 < (const unsigned __int8)*const_hash;
if ( *v15 != *const_hash )
goto LABEL_1;
if ( !*v15 )
break;
v16 = v15[1];
a1 = v16 < const_hash[1];
if ( v16 != const_hash[1] )
goto LABEL_1;
v15 += 2;
const_hash += 2;
}
볼 필요도 없다.
1835360107 이 녀석이 4라운드 key
text:0040151E
.text:0040151E cmp esi, 5
.text:00401521 jnz loc_401334
.text:00401527 cmp ebx, 2Dh
.text:0040152A jnz short loc_40157D
.text:0040152C add eax, ebx // EAX = EAX+EBX
.text:0040152E xor eax, 0CAFACADAh
.text:00401533 cmp eax, 0FB94F394h
.text:00401538 jnz short loc_40157D
.text:0040153A mov esi, [ebp-410h]
.text:00401540 lea eax, [ebp-408h]
.text:00401546 push 0
.text:00401546 sub_40151E endp ; sp-analysis failed
.text:0040157D mov esi, [ebp-410h] // COPY
.text:00401583 lea eax, [ebp-408h] // COPY
HEXLAY가 안되서 어셈으로 보았다. 해석을 처음에 잘못해서 조금 해멨다. 그리고 3을 E로 봤다.. 이제 그냥 복사해야겠다..여기서10분 이상 멘붕온듯..
보면 ebx가 0x2D 인지 비교하는데 ebx는 0x2d로 고정이기 때문에 add 연산을 수행 할 수 있고
XOR 0xCAFACADA 한 다음 0xFB94F394인지 비교 하는데 만약 0xFB94F394가 아니라면
40157D주소로 분기하여 두가지의 루틴을 수행한다.
ADD EAX, EBX를 보면 EAX는 0x2D를 더하는 것으로 알 수 있고
이를 역 연산 하면 다음과 같다
val + 0x2D ^ 0xCAFACADA = 0xFB94F394
val = 0xCAFACADA ^ 0xFB94F394 - 0x2D
python 코드, c언어 코드에서 나온 값으로 해서 안 되서 계산기로 해보니 값이 다른게 나와서 멘붕 왔었는데 금방 회복했다 휴우..
연산자 우선 순위가 - 가 ^ 보다 높기 때문에 ㅋㅋㅋㅋㅋㅋㅋ 사소한 실수를 하면서 기초를 한번 더 복습 !!
val = (0xCAFACADA ^ 0xFB94F394) - 0x2D 이렇게 해야 한다~
829307169
답
이것으로 생애 처음으로 900점 대 문제를 풀었다.
다음에 이런거 나오면 풀 수 있는 자신감이 생겼다.
[끝]
'0x07 CTF > [스스로 푼 것]' 카테고리의 다른 글
[REV] COMPEST - CreeptiCity (0) | 2020.09.12 |
---|---|
HexionCTF XOR - Crypto (0) | 2020.04.18 |
ISITDTU CTF Reversing cool (0) | 2018.07.30 |
ISITDTU CTF Reversing Embeedding (0) | 2018.07.30 |
ISITDTU PWN Unexploitable 미완성 (0) | 2018.07.30 |