악성코드와 백신/악성코드 개발일지

[악성코드 개발](9)[페이로드 암호화]

황올뱀 2026. 3. 11. 21:43

Payload encryption

난독화와 마찬가지로 페이로드를 저장할 때 백신이 알아차리지 못하게 하는 방법이다.

  • 장점: 시그니처 기반 탐지를 우회할 수 있다
    단, 행동으로 판단하는 휴리스틱 탐지에서는 소용없음...
  • 단점: 암호화된 데이터가 많을수록 엔트로피가 증가한다
    따라서 엔트로피도 고려하면 걸릴수도 있다

 

XOR 암호화

가장 단순하고 가벼운 암호 기법
    문자열 같은거는 걍 이런걸로 숨기자

 

암호 해독도 해당 키로 다시 XOR해주면 되는 만큼 쉽고 간편하다
    원문 -XOR-> 암호문 -XOR-> 원문

 

1byte key인 bKey를 이용해 shellcode를 암호화 하기

Void XOR(IN PBYTE pShellcode, IN SIZE_T size, IN BYTE bKey){
    for (int i = 0 ; i < size ; i++){
        pShellcode[i] = pShellcode[i] ^ bKey;
    }
}

-> bKey가 너무 작아 brute force로 금방 뚫릴듯...

 

임의 바이트의 Key를 입력받아 반복 암호화 하기

Void adv_XOR(IN PBYTE pShellcode, IN SIZE_T shellcodeSize, IN PBYTE key, IN SIZE_T keySize){
    for (int i = 0, j = 0 ; i < shellcodeSize ; i++, j++){
        if (j >= keySize) {
            j = 0;
        }
        pShellcode[i] = pShellcode[i] ^ key[j];
    }
}

 

RC4 암호화

빠르고 효율적인 스트림 암호의 일종
key와 256바이트 state(S-box)를 통해 평문과 동일한 길이의 key stream을 만들어 XOR로 암호화 함.
    즉, keystream만 알면 XOR의 성질인
    원문 -XOR-> 암호문 -XOR-> 원문
    을 이용해 복호화가 가능하다!

// RC4 구조체 정의
typedef struct {
    unsigned char state[256]; // S-box 
    int x, y; // <- 뽑은 위치 기억용
} RC4_CONTEXT;

S-box를 key를 이용해 뒤섞기

// 1. 키 초기화 (KSA)
void rc4_init(RC4_CONTEXT* ctx, const unsigned char* key, int key_len) {
    for (int i = 0; i < 256; i++) ctx->state[i] = i;

    int j = 0;
    for (int i = 0; i < 256; i++) {
        j = (j + ctx->state[i] + key[i % key_len]) % 256;
        // Swap
        unsigned char temp = ctx->state[i];
        ctx->state[i] = ctx->state[j];
        ctx->state[j] = temp;
    }
    ctx->x = 0;
    ctx->y = 0;
}

j = (j + ctx->state[i] + key[i % key_len]) % 256로 새로운 위치(j)를 계산하고
이걸 i번째 값과 Swap한다.
그리고 이걸 256번 반복하면 S-box 내용이 무작위가 된다.

키 스트림 생성

// 2. 키 스트림 생성 (PRGA)
void generate_keystream(RC4_CONTEXT* ctx, unsigned char* stream, int len) { 
    int x = ctx->x; 
    int y = ctx->y; 
    for (int i = 0; i < len; i++) { 
        x = (x + 1) % 256; 
        y = (y + ctx->state[x]) % 256; 
        // Swap
        unsigned char temp = ctx->state[x]; 
        ctx->state[x] = ctx->state[y]; 
        ctx->state[y] = temp; 

        // 무작위 바이트 하나 추출해서 stream에 저장 
        int xor_index = (ctx->state[x] + ctx->state[y]) % 256;
        stream[i] = ctx->state[xor_index]; 
    } 
    ctx->x = x; 
    ctx->y = y; 
}

S-box를 알기 어렵게 하기 위해 x, y로 다시 뒤섞고 무작위 바이트를 추출해 keystream에 저장한다
이 과정을 평문만큼 반복하면 keystream이 완성된다!

암호화

// 3. 암호화
void xor_cipher(unsigned char* data, unsigned char* keystream, int len) { 
    for (int i = 0; i < len; i++) { 
        data[i] ^= keystream[i]; // 원본과 키를 겹침 
    } 
}

keystream과 평문을 XOR하여 암호화 한다.

int main() {
    unsigned char key[] = "bigsilver";
    unsigned char data[] = "you are cooked";
    int data_len = sizeof(data);

    RC4_CONTEXT ctx;
    unsigned char* keystream = (unsigned char*)malloc(data_len);

    printf("plain text: %s\n", data);

    // --- 암호화 단계 ---
    rc4_init(&ctx, key, sizeof(key) - 1);
    rc4_generate_keystream(&ctx, keystream, data_len);
    xor_cipher(data, keystream, data_len);

    printf("encryted: %.*s\n", data_len, data);

    // --- 복호화 단계  ---
    rc4_init(&ctx, key, sizeof(key) - 1);
    rc4_generate_keystream(&ctx, keystream, data_len);
    xor_cipher(data, keystream, data_len);

    printf("decrypted: %s\n", data);

    free(keystream);
    return 0;
}

 

 

AES 암호화

블록 암호희 일종으로 항상 128비트(16바이트)의 입력을 필요로 한다
    따라서 패딩을 넣기도 한다
(암호학 둘러보기 6 참고)

#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>

#pragma comment(lib, "bcrypt.lib")

// NT_SUCCESS 매크로 (성공 여부 확인용)
#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif

BOOL Aes256Decrypt(
    PBYTE key, DWORD keyLen, 
    PBYTE iv, DWORD ivLen, 
    PBYTE data, DWORD dataLen
) {
    BCRYPT_ALG_HANDLE hAlg = NULL;
    BCRYPT_KEY_HANDLE hKey = NULL;
    DWORD cbKeyObject = 0, cbResult = 0;
    PBYTE pbKeyObject = NULL;
    BOOL success = FALSE;

    // 1. 알고리즘 핸들 열기 (AES)
    if (!NT_SUCCESS(BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0))) goto Cleanup;

    // 2. 블록 체이닝 모드 설정 (CBC)
    if (!NT_SUCCESS(BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE, (PBYTE)BCRYPT_CHAIN_MODE_CBC, sizeof(BCRYPT_CHAIN_MODE_CBC), 0))) goto Cleanup;

    // 3. 키 객체를 위한 메모리 할당
    if (!NT_SUCCESS(BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (PBYTE)&cbKeyObject, sizeof(DWORD), &cbResult, 0))) goto Cleanup;
    pbKeyObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbKeyObject);

    // 4. 실제 키 생성
    if (!NT_SUCCESS(BCryptGenerateSymmetricKey(hAlg, &hKey, pbKeyObject, cbKeyObject, key, keyLen, 0))) goto Cleanup;

    // 5. 복호화 수행 (In-place: data 배열의 내용이 직접 바뀜)
    if (!NT_SUCCESS(BCryptDecrypt(hKey, data, dataLen, NULL, iv, ivLen, data, dataLen, &cbResult, 0))) goto Cleanup;

    success = TRUE;

Cleanup:
    if (hKey) BCryptDestroyKey(hKey);
    if (hAlg) BCryptCloseAlgorithmProvider(hAlg, 0);
    if (pbKeyObject) HeapFree(GetProcessHeap(), 0, pbKeyObject);
    return success;
}
int main() {
    // AES-256 키 (32바이트)
    unsigned char key[32] = "mysecretkey12345678901234567890"; 
    // 초기화 벡터 (16바이트)
    unsigned char iv[16]  = "initialvector123"; 

    // 데이터 (16바이트 배수여야 함)
    unsigned char data[16] = { 
        0x5c, 0x3d, 0x4e, 0x... 
    };

    printf("복호화 시도 중...\n");

    if (Aes256Decrypt(key, sizeof(key), iv, sizeof(iv), data, sizeof(data))) {
        printf("복호화 결과: %.*s\n", 16, data);
    } else {
        printf("복호화 실패!\n");
    }

    return 0;
}

tiny-AES 기반
소스코드만으로 의존성 없이 AES를 구현
    단, 패딩 기능이 없고
    백신들은 이미 tiny-AES를 사용한 것을 눈치챌 수 있다

#include <stdio.h>
#include <string.h>
#include "aes.h" // tiny-aes 헤더 포함

void TinyAesDecrypt(
    unsigned char* key,    // 32바이트 (AES-256 기준)
    unsigned char* iv,     // 16바이트
    unsigned char* data,   // 복호화할 데이터 (16의 배수여야 함)
    int data_len
) {
    struct AES_ctx ctx;

    // 1. AES 컨텍스트 초기화 (키와 IV 설정)
    AES_init_ctx_iv(&ctx, key, iv);

    // 2. CBC 모드로 복호화 수행 (In-place 연산)
    // 데이터 버퍼의 내용이 직접 복호화된 내용으로 바뀝니다.
    AES_CBC_decrypt_buffer(&ctx, data, data_len);
}
int main() {
    // AES-256 키 (32바이트)
    unsigned char key[32] = { 
        0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 
        0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, 0x77, 0x81,
        0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7,
        0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4 
    };

    // 초기화 벡터 (16바이트)
    unsigned char iv[16] = { 
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 
        0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f 
    };

    // 암호화된 데이터 (16바이트 배수)
    // "you are cooked!!" (15자) + NULL(1자) = 16바이트 예시
    unsigned char data[16] = { 
        0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 
        0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a 
    };

    printf("복호화 전 (깨진 데이터)\n");

    // 복호화 실행
    TinyAesDecrypt(key, iv, data, 16);

    // 가변 길이 출력 활용
    printf("복호화 완료: %.*s\n", 16, data);

    return 0;
}
반응형