2022. 3. 26. 14:24ㆍ카테고리 없음
main 함수에는 인자 2개가 넘어온다. EDI와 RSI를 스택에 복사하는 것으로 알 수 있다.
mov [rbp+var_54], edi
mov [rbp+var_60], rsi
void *[4] 배열
structure[0]에는 malloc된 배열의 주소가 들어간다.
structure[2]에는 파일 크기 * 8이 들어간다.
void __fastcall sub_561B39A00B2C(void *structure[4], size_t file_size)
{
if ( file_size > 0x1FFFFFFFFFFFFFFFLL )
exit(1);
*structure = malloc(file_size);
structure[2] = (void *)(8 * file_size);
structure[1] = 0LL;
}
structure[1]는 한 비트씩 읽을 때마다 0부터 1씩 증가한다.
bool __fastcall sub_561B39A00AAD(void *structure[4])
{
int v1; // eax
if ( structure[1] >= structure[2] ) // validate_process_1
exit(1);
v1 = (*((char *)*structure + ((unsigned __int64)structure[1] >> 3)) >> ((_BYTE)structure[1] & 7)) & 1;// 비트를 하나씩 끄집어 냄
structure[1] = (char *)structure[1] + 1;
return v1 != 0;
}
v1 = (*((char *)*structure + ((unsigned __int64)structure[1] >> 3)) >> ((_BYTE)structure[1] & 7)) & 1;
이 줄이 굉장히 난해하게 되어있다.
structure[1]은 함수를 한 번 돌때마다 1증가한다.
먼저 structure[1]에 3만큼 오른쪽으로 비트시프트 하라고 되어있으므로 두번째 요소가 8증가할 때마다 1증가한다.
다음으로 structure[1] & 7이므로 structure[1] % 8과 같다.
그러므로 이 줄은 8비트를 받아와서 high bit부터 뽑아내는 기능을 한다.
10010100이 있다면 0 0 1 0 1 0 0 1 순서로 받아오게 된다.
sub_561B39A00C60 (a.k.a encrypt_start)
void __fastcall encrypt_start(void *input[4], void *output[4])
{
int v2; // eax
unsigned __int64 settedMSB; // rax
int j; // [rsp+18h] [rbp-18h]
int k; // [rsp+1Ch] [rbp-14h]
unsigned __int64 i; // [rsp+20h] [rbp-10h]
unsigned __int64 v7; // [rsp+28h] [rbp-8h]
while ( input[1] < input[2] ) // validate_process_1
{
for ( i = 0LL; ; ++i )
{
LOBYTE(v2) = sub_561B39A00AAD(input);
if ( v2 )
break;
if ( input[1] >= input[2] )
return;
}
if ( i )
{
_BitScanReverse64(&settedMSB, i); // 상위비트 부터 검색하여 1로 설정된 비트가 몇번째 비트인지 반환
v7 = 64LL - (int)(settedMSB ^ 0x3F); // v7 = settedMSB + 1
for ( j = 0; j < v7 - 1; ++j )
encrypt_process(output, 1);
encrypt_process(output, 0);
for ( k = 0; v7 > k; ++k )
{
encrypt_process(output, i & 1);
i >>= 1;
}
}
else
{
encrypt_process(output, 0);
encrypt_process(output, 0);
}
}
}
비트를 읽다가 0을 만나면 for문이 종료되고 1의 개수가 i에 들어가게 된다.
_BitScanReverse64(index, mask)
mask의 상위비트부터 검색하여 처음으로 1로 설정된 비트를 만나면 그 자리를 반환한다.
ex. mask가 00 10 00 10 / 01 00 00 00 / 00 11 00 10 / 01 11 10 01 이면, index에는 14가 반환된다.
v7 = 64LL - (int)(settedMSB ^ 0x3F);
settedMSB는 64를 넘을수 없으므로 settedMSB ^ 00111111(0x3F)는 63 - settedMSB와 같다.
64 - (63 - settedMSB) = 1 + settedMSB
v7 = settedMSB + 1;
sub_561B39A0097A (a.k.a encrypt_process)
void __fastcall encrypt_process(void *structure[4], int a2)
{
if ( structure[1] >= structure[2] ) // validate_process_1
{
if ( (unsigned __int64)structure[2] > 0x7FFFFFFFFFFFFFFELL )
exit(1);
structure[2] = (void *)(2LL * (_QWORD)structure[2]);// structure[2] << 1
*structure = realloc(*structure, (size_t)structure[2]);
if ( !*structure ) // NULL_guard
exit(1);
}
*((_BYTE *)*structure + ((unsigned __int64)structure[1] >> 3)) &= ~(unsigned __int8)(1 << ((unsigned __int8)structure[1] & 7));
*((_BYTE *)*structure + ((unsigned __int64)structure[1] >> 3)) |= (a2 != 0) << ((unsigned __int8)structure[1] & 7);
structure[1] = (char *)structure[1] + 1;
}
*(*structure + (structure[1] >> 3)) &= ~(1 << (structure[1] & 7));
*(*structure + (structure[1] >> 3)) |= (a2 != 0) << (structure[1] & 7);
structure[1] = structure[1] + 1;
이 부분이 핵심이다. ~(1 << (structure[1] & 7))
은 structure[1] % 8개 만큼 1비트를 같는 상수를 생성한다. structure[1]이 5이면 0001 1111이 된다.
(a2 != 0)
은 a2가 0이 아니면 1, 0이면 0을 반환 하므로 a2가 0이 아니면 (structure[1] % 8) + 1번째 자리가 1로 바뀐다.
정리해보면 첫 줄은 해당 비트를 0으로 초기화하고, 둘째줄은 두번째 인자로 받은 값을 해당 비트에 저장한다
다시 encrypt_start로 돌아가보자
void __fastcall encrypt_start(void *input[4], void *output[4])
{
// ---중략---
if ( i )
{
_BitScanReverse64(&settedMSB, i); // 상위비트 부터 검색하여 1로 설정된 비트가 몇번째 비트인지 반환
v7 = 64LL - (int)(settedMSB ^ 0x3F); // v7 = settedMSB + 1
for ( j = 0; j < v7 - 1; ++j )
encrypt_process(output, 1);
encrypt_process(output, 0);
for ( k = 0; v7 > k; ++k )
{
encrypt_process(output, i & 1);
i >>= 1;
}
}
else
{
encrypt_process(output, 0);
encrypt_process(output, 0);t
}
}
}
첫 번째 for문은 v7만큼 돈다. 즉, 뒤에서 v7 + 1
번째 비트까지 1이 된다.
for문을 나온뒤 v7 + 2
번째 비트가 0이된다.
두 번째 for문은 v7 - 1만큼 돈다. i의 비트 값이 v7
번 저장된다.
만약 i가 0이라면 output에는 0만 두 번 저장된다.
main
int __fastcall main(int argc, char **argv)
{
// ---중략---
encrypt_start(input, output);
len_file = strlen(argv[1]);
outfile = (char *)malloc(len_file + 5);
if ( !outfile )
exit(1);
sprintf(outfile, "%s.enc", argv[1]);
fd_write = open(outfile, 66, 384LL);
if ( fd_write == -1 )
exit(1);
write(fd_write, &input[2], 8uLL);
sub_561B39A00C08(output, fd_write);
close(fd_write);
free(outfile);
return 0;
}
encrypt_start() 이후 출력파일을 생성하고 처음으로 write하는 부분을 보면, input[2]를 파일의 제일 처음에 기록하는 것을 볼 수 있다.
input[2]는 파일 크기 * 8
이다. 파일의 첫 8바이트는 파일 크기를 의미한다는 것을 기억하자.
sub_561B39A00C08
void __fastcall sub_561B39A00C08(void *output[4], int fd)
{
while ( ((unsigned __int64)output[1] & 7) != 0 )
encrypt_process(output, 1);
write(fd, *output, (unsigned __int64)output[1] >> 3);
}
sub_561B39A00C08()은 8바이트 단위로 맞추기 위해 1로 패딩한 후 전부 write하는 역할을 한다.
Solver 제작
모든 함수가 파악되었으니 solver를 만들어보자!
- 데이터는 비트단위로 처리된다.
- 비트 단위로 스캔하기 위해서 디컴파일한 함수를 그대로 이용한다.
- 비트 단위 출력도 구현되어 있으므로 그대로 사용한다.
데이터는 다음과 같이 암호화되어 있다.
- 실제데이터가 0...01일 경우
LOW ---- HIGH 개수의 유효 비트 수 0 실제 데이터 0의 개수 - 실제 데이터가 1일 경우
LOW HIGH 0 0
위 정보를 바탕으로 다음과 같은 solver를 작성할 수 있다.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
void allocate_structure(void *structure[3], __off64_t size)
{
*structure = malloc(size);
if (*structure == NULL)
exit(1);
structure[1] = 0;
structure[2] = (void *)(size * 8);
}
void read_file(void *structure[3], int fd)
{
__off64_t size;
size = lseek(fd, 0, 2);
allocate_structure(structure, size);
lseek(fd, 0, 0);
printf("size : %ld\n", size);
read(fd, *structure, size);
}
bool read_bit(void *structure[3])
{
int v1;
v1 = (*((char *)*structure + ((__int64_t)structure[1] >> 3)) >> ((__int64_t)structure[1] & 7)) & 1;
structure[1] = (char *)structure[1] + 1;
return v1 != 0;
}
void decrypt_process(void *structure[3], int bit)
{
*((char *)*structure + ((__int64_t)structure[1] >> 3)) &= ~(__int64_t)(1 << ((__int64_t)structure[1] & 7));
*((char *)*structure + ((__int64_t)structure[1] >> 3)) |= (bit != 0) << ((__int64_t)structure[1] & 7);
structure[1] = (char *)structure[1] + 1;
}
void decrypt_start(void *input[2], void *output[2])
{
int i;
__int64_t j;
__int64_t cnt_one;
while (input[1] < input[2])
{
for (i = 0; ; ++i)
{
if (input[1] >= input[2])
break ;
if (!read_bit(input))
break ;
}
if (input[1] >= input[2])
break ;
if (i != 0)
{
cnt_one = 0;
for (j = 0; j < i + 1; j++)
cnt_one |= read_bit(input) << j;
for (j = 0; j < cnt_one; j++)
decrypt_process(output, 0);
decrypt_process(output, 1);
}
else
{
if (!read_bit(input))
decrypt_process(output, 1);
else
{
decrypt_process(output, 0);
decrypt_process(output, 1);
}
}
}
}
int main(int argc, char **argv)
{
int fd_input;
int fd_output;
void *input[3];
void *output[3];
__int64_t output_size;
if (argc != 2)
return (1);
fd_input = open(argv[1], O_RDONLY);
read_file(input, fd_input);
close(fd_input);
output_size = *(__int64_t *)(*input);
output[2] = (void *)output_size;
input[1] = (char *)input[1] + 64;
allocate_structure(output, output_size / 8);
decrypt_start(input, output);
fd_output = open("flag", 66, 384);
printf("output file opened!\n");
write(fd_output, output[0], (size_t)output[2] >> 3);
close(fd_output);
free(input[0]);
free(output[0]);
return (0);
}
read_bit()
와 decrypt_process()
는 디컴파일한 소스를 그대로 이용하여 만들었다.
위 프로그램을 이용하면 flag를 얻을 수 있다!