본문 바로가기

Contact 日本語 English

【국가암호공모전】 (II-A 분야) 문제 06 - 1부 (2016)

 

국가암호공모전 (II-A 분야) 문제 06 - 1부 (2016)

 

추천글 : 【암호론】 암호론 목차


1. 문제 [본문]

2. 풀이 [본문]


a. RSA 알고리즘


 

1. 문제 [목차]

컴퓨터가 랜섬웨어에 감염되어 중요 파일(flag.hwp)이 flag.hwp.enc로 암호화되었다. 랜섬웨어의 동작을 분석하여 암호화된 파일의 내용을 복구하시오. 

 

[힌트] 소인수분해를 이용하여 RSA 암호를 분석할 때 Fermat factorization 등을 시도해 볼 수 있다.

 

2016 국가암호공모전 (Ⅱ-A) 분야 문제 06 문제.pdf
다운로드
ransomware.exe
다운로드
flag.hwp.enc
다운로드
ransomware.c
다운로드

 


2. 풀이
[목차] 

필자는 위 문제에 대해 완전히 문외한이므로 A부터 Z까지 아주 기초적인 수준에서 분석할 필요가 있다.

 

#define _CRT_SECURE_NO_WARNINGS

 

위 코드는 보안상에 문제로 Visual Studio 등에서 요구하는 명령이다. 가령 scanf와 같은 함수는 위 정의가 선행될 필요가 있다. 필자가 사용하는 Dev-cpp과 같은 IDE는 해당되지 않는데, 만약을 위해 미리 정의(스위치 같은 개념으로!)해 놓는 게 좋다.

 

#include <openssl/conf.h>
#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/aes.h>
#include <stdio.h>
#include <string.h>
#include <Windows.h>

 

필자는 openssl 라이브러리를 링킹하는 과정 없이 필요한 함수들만 골라서 main 함수와 같이 둘 것이다.

 

const char* key = "-----BEGIN PUBLIC KEY-----\n"\
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhN+aCi70gZO5wO04V/LA\n"\
"S3Rjb4Jw5yHVj7hZA+t0YOuUCREgVixvf3qBhZuYIrIX4Pu6cD+AV06HtezKS0CV\n"\
"aUv52Z0/jpSYN3CtBEgil+lhyedcqazwhU6RTAeqqABdec8be3lRrTuH32YOcx0J\n"\
"GFlgIGw9LFLg8fd2VWH/drZEfWUJ+XxAczbZ4+0A/e8Bx9gfKRuy9JiJfRvqVSow\n"\
"hpFfqIfQ+q0vdowX2wO2j3QbjuNq//GAnMluIvRv8hkqLz0dXn6Sn+8EFLUgqp01\n"\
"URT3avoP2wzyxqnb7Vet2A7AWYyJKFfJdy2RVStiW+tcE73pTpVSiqdKkEXViTJx\n"\
"eQIDAQAB\n"\
"-----END PUBLIC KEY-----";

 

위와 같은 선언은 key라는 char 형 1차원 배열을 생성하라는 의미이다. "\" 문자는 그냥 무시하고, 통으로 연결한 체인을 연상하면 된다. 위와 같은 선언 시 주의할 점은 "\" 문자 다음에 띄어쓰기나 다른 문자가 오면 안 된다는 점이다. 한편, key는 PEM 형식으로 표현되어 있음을 알 수 있는데, PEM이란 16진수와 1대 1 대응의 관계를 갖는, 숫자를 표현하는 또다른 방법이라고 보면 좋을 것 같다. 위 공개키를 HEX로 변환하면 다음과 같다.

 

30820122300D06092A864886F70D01010105000382010F003082010A028201010084DF9A0A2EF48193B9C0ED3857F2C0\n
4B74636F8270E721D58FB85903EB7460EB94091120562C6F7F7A81859B9822B217E0FBBA703F80574E87B5ECCA4B4095\n
694BF9D99D3F8E94983770AD04482297E961C9E75CA9ACF0854E914C07AAA8005D79CF1B7B7951AD3B87DF660E731D09\n
185960206C3D2C52E0F1F7765561FF76B6447D6509F97C407336D9E3ED00FDEF01C7D81F291BB2F498897D1BEA552A30\n
86915FA887D0FAAD2F768C17DB03B68F741B8EE36AFFF1809CC96E22F46FF2192A2F3D1D5E7E929FEF0414B520AA9D35\n
5114F76AFA0FDB0CF2C6A9DBED57ADD80EC0598C892857C9772D91552B625BEB5C13BDE94E95528AA74A9045D5893271\n
790203010001

 

위 숫자들을 해석해 보면 공개키에서 사용되는 public modulus(n)와 public exponent(e)를 알 수 있다. 해석 방법에 대해서는 여기를 참고하자. 확인해 보니까 public modulus는 256 진법(bytes)으로 256 자릿수이고 , public exponent는 자주 쓰이는 65537이었다.

 

30 82 01 22             ;30=SEQUENCE (0x0122 = 194 bytes)
|  30 0D                ;30=SEQUENCE (0x0D = 13 bytes)
|  |  06 09             ;06=OBJECT_IDENTIFIER (0x09 = 9 bytes)
|  |  2A 86 48 86       ;Hex encoding of 1.2.840.113549.1.1
|  |  F7 0D 01 01 01
|  |  05 00             ;05=NULL (0 bytes)
|  03 82 01 0F 00       ;03=BIT STRING (0x010F = 175 bytes)
|  |  30 82 01 0A       ;30=SEQUENCE (0x010A = 170 bytes)
|  |  |  02 82 01 01    ;02=INTEGER (0x0101 = 161 bytes) - the modulus
|  |  |  00
|  |  |  84 DF 9A 0A 2E F4 81 93 B9 C0 ED 38 57 F2 C0 4B 
|  |  |  74 63 6F 82 70 E7 21 D5 8F B8 59 03 EB 74 60 EB 
|  |  |  94 09 11 20 56 2C 6F 7F 7A 81 85 9B 98 22 B2 17
|  |  |  E0 FB BA 70 3F 80 57 4E 87 B5 EC CA 4B 40 95 69 
|  |  |  4B F9 D9 9D 3F 8E 94 98 37 70 AD 04 48 22 97 E9 
|  |  |  61 C9 E7 5C A9 AC F0 85 4E 91 4C 07 AA A8 00 5D 
|  |  |  79 CF 1B 7B 79 51 AD 3B 87 DF 66 0E 73 1D 09 18 
|  |  |  59 60 20 6C 3D 2C 52 E0 F1 F7 76 55 61 FF 76 B6 
|  |  |  44 7D 65 09 F9 7C 40 73 36 D9 E3 ED 00 FD EF 01 
|  |  |  C7 D8 1F 29 1B B2 F4 98 89 7D 1B EA 55 2A 30 86 
|  |  |  91 5F A8 87 D0 FA AD 2F 76 8C 17 DB 03 B6 8F 74 
|  |  |  1B 8E E3 6A FF F1 80 9C C9 6E 22 F4 6F F2 19 2A 
|  |  |  2F 3D 1D 5E 7E 92 9F EF 04 14 B5 20 AA 9D 35 51 
|  |  |  14 F7 6A FA 0F DB 0C F2 C6 A9 DB ED 57 AD D8 0E 
|  |  |  C0 59 8C 89 28 57 C9 77 2D 91 55 2B 62 5B EB 5C
|  |  |  13 BD E9 4E 95 52 8A A7 4A 90 45 D5 89 32 71 79
|  |  02 03             ;02=INTEGER (0x03 = 3 bytes) - the exponent
|  |  |  01 00 01       ;hex for 65537

 

BIO_METHOD *a;
BIO *b;
RSA* c;
AES_KEY d;
unsigned char aeskey[16];

 

BIO_METHOD* a;

a의 역할은 b를 선언하기 위한 수단처럼 보인다. 이 코드를 보면 언제나 b와 같이 다니고, b를 도와주는 역할만 했기 때문이다.

 

BIO* b;

b의 역할은 PEM 형식으로 돼 있는 key를 숫자로 바꾸어주는 PEM_read_bio_RSA_PUBKEY 함수에서만 쓰이는 것을 알 수 있다.

 

RSA* c;

RSA 구조체 c를 선언한다는 의미이다. 이때 RSA 구조체는 public modulus n, public exponent e, private exponent d, secret prime factor p, secret prime factor q, d mod p-1, d mod q-1, q^(-1) mod p 등 RSA 알고리즘 (ref1, ref2)에서 필요한 중요한 인자들을 저장하는 집합이다. 이들 숫자들은 크기가 굉장히 큰 숫자들이므로 BIGNUM 형 변수로 선언돼 있다.

 

AES_KEY d;

AES_KEY는 aes.h에 저장돼 있는 구조체로 (unsigned int) rd_key[60]과 (int) rounds를 원소로 가진다.

 

unsigned char aeskey[16];

코드를 보면 aeskey에 난수들을 채워놓는 것을 알 수 있다. 하지만 원래는 이것이 바로 메시지를 암호화하기 위한 개인 패스워드와 같은 개념일 것이다. (크래커의 입장에서 1회성 패스워드라면 난수로 아무렇게나 채워도 되긴 하다.) 이 문제를 푸는 데 있어 aeskey를 직접 구하는 과정이 있을 것이다.

 

int main(int argc, char *argv[])
{
    if (argc != 3){
        printf("usage : %s input output\n", argv[0]);
        return 0;
    }
    srand((int)&argc ^ time(NULL));
    ...
}

 

이제 메인함수를 실행시킨다. 평소에는 별로 중요하지 않아 보였던 argc, argv가 쓰이고 있음을 알 수 있다. 이때 argc는 main 함수에 (cmd 등으로) 전달한 정보의 개수이고, argv는 실제 그 정보를 담고 있는 char 형 2차원 배열이다. 만약 아무 정보를 넘겨 주지 않으면 argc는 1이고, argv[0] = "ransomware.exe"이다. 더 자세한 정보를 얻고자 한다면 여기를 참고하자.

 

HANDLE fin = CreateFile(argv[1], GENERIC_READ, 0, NULL, 
    OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (fin == INVALID_HANDLE_VALUE){
    printf("open input failed\n");
    return 1;
}
HANDLE fout = CreateFile(argv[2], GENERIC_WRITE, 0, NULL, 
    CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (fout == INVALID_HANDLE_VALUE){
    printf("create output failed\n");
    return 1;
}

 

CreateFile은 (LPCSTR) FileName, (DWORD) DesiredAccess, (DWORD) ShareMode, (LPSECURITY_ATTRIBUTES) SecurityAttributes, (DWORD) CreationType, (DWORD) FlagsAndAttributes, (HANDLE) TemplateFile를 인자로 한다. DesiredAccess는 접근권한을 의미하는 것으로 GENERIC_READ, GENERIC_WRITE가 대표적이다. ShareMode는 멀티 프로세싱을 하지는 않는 한 0을 적어준다. SecurityAttributes는 보안과 관련된 것이지만 기본적으로 NULL이다. CreationType은 CREATE_NEW, CREATE_ALWAYS, OPEN_EXISTING, OPEN_ALWAYS, TRUNCATE_EXISTING 등이 있다. 또한 FlagsAndAttributes는 16종류의 플래그를 지정하는 것인데, 다른 속성을 가지지 않는 FILE_ATTRIBUTE_NORMAL을 지정해 주었다. 마지막으로 TemplateFile은 일반적으로 NULL을 넣는다. 더 자세한 정보를 원하면 여기를 참고하자.

 

위 소스를 보면 별다른 옵션을 선택하지 않았음을 알 수 있다. 따라서 CreateFile이 fopen과 비슷하게 기능하여 fin은 읽어들일 파일을 위한 변수이고, fout은 출력할 파일을 위한 변수라고 볼 수 있다. 그러므로 argv[1] = "flag.hwp"이고, argv[2] = "flag.hwp.enc"라고 추론하는 것이 합리적이다. HANDLE은 void *와 같은 뜻인데 파일형에 대한 의미 부여 없이 저장공간으로서만 다루겠다는 의미로 보인다. (not sure

 

DWORD size = 0;
DWORD hsize = 0;
size = GetFileSize(fin, &hsize);
if (hsize || size > 0x100000)
{
    printf("file too large\n");
    exit(0);
}

 

DWORD는 unsigned long과 동일한 뜻이다. DWORD형 size와 hsize를 선언한다. GetFileSize는 파일의 사이즈를 구하는 함수로 (HANDLE) file, (LPDWORD) FileSizeHigh를 변수로 가지는 함수이다. (참고로 size는 fin의 바이트 수이다.) 이때 FileSizeHigh는 size가 2^32 바이트 (4 GB)를 넘을 경우 사용하는 인자로, 넘지 않을 경우 NULL로 채운다. 아마도 size가 너무 클 경우 hsize에 데이터를 약간 넘겨서 size의 값을 좀 더 줄이는 듯 하다. 왜냐하면 size가 2^20 바이트보다 작을 조건이 hsize가 0이 아닐 조건에 포함관계가 아니려면 그 경우밖에 없기 때문이다. 하여튼 파일의 크기가 너무 크지는 않을 것이라는 추론을 해볼 수 있다.

 

unsigned char* bufin;
unsigned char* bufout;
bufin = (unsigned char*)HeapAlloc(GetProcessHeap(), NULL, size + 16);
ReadFile(fin, bufin, size, &size, NULL);
bufout = (unsigned char*)HeapAlloc(GetProcessHeap(), NULL, size + 16 + 256);
unsigned char IV[16];

 

HeapAlloc은 Heap 영역에 메모리를 할당해 주는 함수로, (HANDLE) hHeap, (DWORD) Flags, (SIZE_T) Bytes를 인자로 취한다. (RSA 알고리즘은 굉장히 큰 수가 오가므로 메모리를 효율적으로 쓰기 위해 (동적) 메모리 할당에 신경쓸 필요가 있다.) 미리 얘기하자면, bufin은 fin에 padding(1 ~ 16 byte)을 한 것을 저장하기 위한 unsigned int 형 배열이고, bufout은 출력할 파일을 저장하기 위한 배열이다. 한편 ReadFile은 (HANDLE) File, (LPVOID) BufferForFile, (DWORD) NumberOfBytes, (LPDWORD) &NumberOfBytes, (LPOVERLAPPED) Overlapped를 인수로 취한다. 이때, NumberOfBytes는 File의 사이즈이고, Overlapped는 비동기 입출력을 사용하지 않는 경우 NULL을 취한다. IV는 initial vector의 약자로 AES_cbc_encrypt에서 bufout의 값을 바꾸는데 1회 관여한다. 그 이후는 IV가 bufin의 원소를 취하게 된다. 따라서 IV[0] ~ IV[15]를 bufin[-16] ~ bufin[-1]로 이해하면 좋을 듯하다.

 

한편 bufout은 bufin에 비해 256 bytes만큼 메모리를 더 확보했다. 이는 AES_cbc_encrypt에서 bufout[0]에서 256칸 떨어진 지점을 포인터로 잡은 것에서 유래한다. 참고로 AES_cbc_encrypt는 bufin과 d로 bufout을 작성하는 함수다. 그런데 왜 256칸을 떨어트렸을까? 그 이유는 맨 마지막에 밝혀지지만, n의 자릿수가 256 자리라는 것을 상기해 보길 바란다.

 

int pad = 0x10 - size % 0x10;
for (int i = 0; i < pad; i++){
    bufin[size + i] = (char)pad;
}
size += pad;

 

padding은 임의의 길이의 메시지를 적당한 규격에 맞도록 원문에 좀더 추가해 주는 의미없는 메시지이다. 위 소스를 보면 fin의 사이즈인 size가 16의 배수가 되도록 1 ~ 16개의 문자를 추가해 줄 것임을 알 수 있다. 그 배열은 "1", "22", "333" … 과 같이 생겼을 것이다. 이때 각 문자는 ASCII 코드를 확인해 보길 바란다. 이제 우리는 fin으로 읽어들인 원문을 이용하지 않고, padding을 한 bufin으로 암호문을 만들 것이다. 암호화를 복호화했을 때 나오는 메시지에서 padding을 떼어 내야 원문을 얻어낼 수 있을 것이다.

 

memset(IV, 0, 16);
a = BIO_s_mem();
b = BIO_new(a);
c = RSA_new();
for (int i = 0; i < 16; i++){
    aeskey[i] = rand() & 0xff;
}

 

memset은 (void *) str, (int) c, (size_t) n을 인수로 하여, 처음부터 n번째 문자까지 배열 str에 c라는 문자를 채워넣겠다는 의미이다. 이때 c를 char 형으로 변환하는 과정을 거친다. 따라서 IV라는 배열은 모든 공간에 NULL 문자로 채워넣게 된다. 그 뒤 a, b, c를 위한 메모리를 확보한다. 선언과 메모리 확보는 별개였음을 앞서 HeapAlloc에서 확인한 바 있다. 그리고 aeskey를 난수들로 채워넣는다. 이때 255와 AND 연산을 한 이유는 난수의 범위가 unsigned char 형이 되도록 맞춰주어야 했기 때문이다.

 

BIO_puts(b, key);
PEM_read_bio_RSA_PUBKEY(b, &c, 0, 0);
AES_set_encrypt_key(aeskey, 128, &d);
AES_cbc_encrypt(bufin, bufout + 256, size, &d, IV, AES_ENCRYPT);
int clen = RSA_public_encrypt(16, aeskey, bufout, c, RSA_PKCS1_PADDING);
WriteFile(fout, bufout, size + 256, &size, 0);
CloseHandle(fin);
CloseHandle(fout);

return 0;

 

이제 마지막 구절이다. 우선 BIO_puts로 '\n'으로 끝나는 배열인 key를 b에 복사한다. 그 뒤 PEM_read_bio_RSA_PUBKEY로 b에 저장돼 있는 PEM 형식의 key를 BIGNUM의 형태로 c에 저장한다. 이제 사용하게 될 AES_set_encrypt_key는 난수열인 aeskey를 이용해서 AES_KEY 구조체인 d에 암호문을 기입하는 함수이다. 우선 AES_set_encrypt_key의 단순화된 정의를 보자. (더 정확한 정의를 참고하고 싶으면 여기를 참고하자.) 

 

int AES_set_encrypt_key((const unsigned char*) aeskey, (const int) 128, (AES_KEY *) &d) = {
  unsigned int *rk;
  int i = 0, j;
  unsigned int temp; 
  rk = d->rd_key;
  d->rounds = 10;
  for(j = 0; j < 4; j ++)
    rk[i] = (int) rand();  // we should know aeskey to find out rk
  while(1){
    temp = rk[3];
    for(;; j ++){
      rk[j] = rand();  // we should know aeskey to find out rk
      if(j % 4 == 3)
        break;
    }
    j ++;

    i ++;
    if(i == 10) return 0;
  }  
}

 

중요한 것은 unsigned int형 배열 rk와 AES_KEY 구조체 d 내부의 rd_key라는 배열을 동일시한다는 점이다. 그래서 이 함수를 적용하면 aeskey를 이용해서 d 내부의 rd_key의 44개의 원소가 난수로 채워진다. 또한 d 내부의 rounds는 10이라는 값을 갖는다. 

 

한편 AES_cbc_encrypt는 bufout을 bufin과 IV로 채워넣는 함수이다. 실제로 AES_cbc_encrypt에 대한 단순화된 정의를 보면 그것을 쉽게 확인해 볼 수 있다. (보다 정확한 정의를 확인하고자 하면 여기, 여기, 여기를 확인하자.)

 

void AES_cbc_encrypt((const unsigned char*) bufin, (unsigned char*) bufout + 256, 
                     (int) size, (AES_KEY*) &d, 
                     (unsigned char*) IV, (int) AES_ENCRYPT)
{
  // AES_ENCRYPT = 1
  // CRYPTO_cbc_128_encrypt(bufin, bufout + 256, size, d, AES_encrypt)
  {
    unsigned int n;
    const unsigned char* iv = IV;
    while(size >= 16){

      for(n = 0; n < 16; n ++)
        bufout[n] = bufin[n]^iv[n]; 

      AES_ENCRYPT(bufout, bufout, d)

      iv = in;
      size -= 16;
      bufin += 16;
      bufout += 16;
    }
    // size is the multiple of 16, so the latter process can be omitted
    memcpy(IV, iv, 16); // IV[p] = bufin[size-1-16+p], 0 <= p < 16
  }
}

 

만약 AES_ENCRYPT가 없었으면 bufout을 통해 bufin을 알아내는 게 굉장히 쉬울 것이다. 왜냐하면 bufouf[256] ~ buf[256 + 15]를 통해 bufin[0] ~ bufin[15]를 알아내고, 그 뒤 순차적으로 xor의 역연산을 통해 bufin의 값을 알아낼 수 있기 때문이다. 하지만 AES_ENCRYPT가 aeskey로부터 만들어진 d를 가지고 bufout를 상당히 휘저어 놓았다. 그래서 마냥 단순한 것은 아니게 됐다. 그럼에도 불구하고 AES_ENCRYPT의 알고리즘이 결정론적이기 때문에 CRYPTO_cbc_128_decrypt를 이용해서 구해낼 수 있을 것이다. 다만, 그 함수를 이용하려면 암호 d를 구해야 하고, d를 구하려면 aeskey를 구해야 한다.

 

이제 마지막 산, RSA_public_encrypt가 남았다. 이 함수는 (int) flen, (unsigned char*) from, (unsigned char*) to, (RSA *) rsa, (int) padding을 인자로 가져서, from에서 flen만큼의 길이를 RSA를 적용해서 to에 기입하겠다는 의미이다. 요즘은 padding 타입으로 RSA_PKCS1_PADDING만 사용되는 듯 하다. 따라서 aeskey라는 평문을 c라는 RSA 키를 이용하여 16 bytes만큼의 길이를 암호화 작업을 수행하여 bufout이라는 암호문을 생성하게 된다. 더 정확한 정의를 보려면 여기를 참고하자. 

 

RSA_public_encrypt를 처리한 뒤 남은 일은 마무리를 짓는 것이다. 출력파일을 생성하고, 스트림에 있는 파일 변수들을 종료시키는 작업이다. 참고로 WriteFile은 (HANDLE) File, (LPVOID) BufferForFile, (DWORD) NumberOfBytes, (LPDWORD) &NumberOfBytes, (LPOVERLAPPED)overlapped를 인자로 가져서, fout이라는 파일 변수에 bufout이라는 size + 256 bytes 크기의 unsigned char형 배열을 기입하는 것이다. 다시 강조하지만 출력파일의 이름은 argv[2]에 저장돼 있는 것과 같은 "flag.hwp.enc"이다.

 

이제 flag.hwp.enc를 해독하기 위한 전략을 정리할 수 있다. 우선 우리는 키를 알고 있으므로 n을 소인수분해 해볼 것이다. RSA 알고리즘에 따르면 n은 두 소수의 곱일 것이고, 제한시간 내에 Fermat factorization으로 n의 두 약수를 구할 수 있기를 바란다. 그렇게 n의 약수를 구하면 오일러 파이함수 φ(n)를 구할 수 있을 것이다. 그 뒤 유클리드 호제법을 이용하면 private exponent(d)를 구할 수 있겠다. 그러면 암호문을 그 지수만큼 거듭제곱을 해서 원문을 구할 수 있을 것이다. 암호문의 길이가 다소 걸리지만 높은 확률로 n과 자릿수가 같을 것이고, 설사 다르더라도 기껏해야 한두 자리 차이일 것이다. n과 같은 자릿수라고 밀어붙여 보자.

 

한편, 얻어진 원문은 16자리 난수열인 aeskey이다. AES_set_encrypt_key를 이용해서 AES_KEY형 변수 d를 재현해낼 수 있을 것이다. 하지만 aeskey를 알아낸다고 해서 이미 aeskey의 암호문으로 뒤덮인 bufout을 가지고 bufin을 구해낼 수 있을까? 여기서 왜 AES_cbc_encrypt가 bufout + 256으로 포인터를 취했는지에 대한 비밀이 드러난다. 아무리 aeskey를 가지고 bufout에 덮어쓴다고 한들, n의 크기가 256 bytes이므로 bufout + 256 이후의 문자들에 대해서는 영향을 주지 못한다. 따라서 bufout은 사실 멀쩡했다! (사실 문제를 풀 수 있게 이렇게 한 것인데, 해커가 돈을 목적으로 랜섬웨어 복구의 여지를 두었다는 설정이면 충분히 가능한 상황이다!)

 

aeskey와 bufout을 알고 있으므로 AES_cbc_decrypt 함수를 통해 bufin을 구해낼 수 있을 것이다. 그런데 이때 얻어낸 bufin은 padding이 돼 있을 것이다. 아마 끝에 1이 하나, 2가 두 개, 3이 3개, ...와 같이 붙어 있을 텐데, 그만큼 끝에서 떼어내면 flag.hwp를 복원할 수 있을 것이다.

 

입력 : 2016. 08. 15 01:05