Skip to content

Instantly share code, notes, and snippets.

@s1037989
Created February 5, 2026 02:36
Show Gist options
  • Select an option

  • Save s1037989/a0a05335533ffa7a85ad44dc8be0ce82 to your computer and use it in GitHub Desktop.

Select an option

Save s1037989/a0a05335533ffa7a85ad44dc8be0ce82 to your computer and use it in GitHub Desktop.
DRM C app

Simple demonstration of a C program (only depends on glibc) that uses a statically-linked library for handling DRM protection of software.

$ make
$ ./hello1
DRM failure: license: license file not found or unreadable at /tmp/demo_license.txt
$ ./hello2
DRM failure: license: license file not found or unreadable at /tmp/demo_license.txt
$ make sig
Created /tmp/demo_license.txt
$ cat /tmp/demo_license.txt 
expires=2030-12-31
bind_type=hostname
bind_value=darter
sig=b46dafc7ea953f816569de8885b897747f65738481d8fa8fe284feb52e7a2b61
$ ./hello1
Hello1, world!
$ ./hello2
Hello2, world!

There are plenty of more sophisticated ways to handle DRM.

  1. Nothing is completely crack-proof especially when the customer owns the machines (root). The objective is to make cracking more expensive. The objective is to require some level of effort, versus none at all. Any level of effort will stop honest people for accidentally or unknowingly misusing the software.
  2. Anything beyond that is an arms race. All kinds of interesting techniques to employ.
  3. Use a TPM which is essentially a dongle license but permanently attached to the PC. It's a much more sophisticated machine-id / serial number.
  4. Use signatures instead of a pre-shared key like in this sample app: way too easy to extract the key and create your own signed license file. But, again, a PSK may already be enough to deter honest customers from doing the wrong thing since they have to go out of their way to extract the key and create their own license file.
  5. Use commerical software for this purpose: https://go.wibu.us/CodeMeter-software-licensing-and-protection-platform.html
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "license.h"
/* Encrypted payload: "hello world (from hello1)\n"
This is XORed with a derived key stream (demo only). */
static const uint8_t CIPHERTEXT[] = {
0x2b,0x1c,0x0a,0x15,0x5d,0x19,0x1f,0x1b,0x11,0x59,0x17,0x1b,0x15,0x13,0x4d,0x5c,
0x1c,0x0b,0x16,0x10,0x59,0x12,0x1f,0x16,0x16,0x14,0x6c
};
static void xor_stream(uint8_t *dst, const uint8_t *src, size_t n, const uint8_t key[32]) {
for (size_t i = 0; i < n; i++) dst[i] = (uint8_t)(src[i] ^ key[i % 32]);
}
int main(void) {
if (license_check() != 0) {
fprintf(stderr, "DRM failure: %s\n", license_last_error());
return 1;
}
uint8_t key[32];
if (license_derive_key(key) != 0) {
fprintf(stderr, "DRM failure: %s\n", license_last_error());
return 1;
}
uint8_t out[sizeof(CIPHERTEXT) + 1];
xor_stream(out, CIPHERTEXT, sizeof(CIPHERTEXT), key);
out[sizeof(CIPHERTEXT)] = '\0';
/* The “success path” output is produced only after license-gated decrypt. */
//fputs((const char*)out, stdout);
fprintf(stderr, "Hello1, %s!\n", "world");
return 0;
}
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include "license.h"
/* Encrypted payload: "hello world (from hello2)\n" */
static const uint8_t CIPHERTEXT[] = {
0x2b,0x1c,0x0a,0x15,0x5d,0x19,0x1f,0x1b,0x11,0x59,0x17,0x1b,0x15,0x13,0x4d,0x5c,
0x1c,0x0b,0x16,0x10,0x59,0x12,0x1f,0x16,0x16,0x14,0x6f
};
static void xor_stream(uint8_t *dst, const uint8_t *src, size_t n, const uint8_t key[32]) {
for (size_t i = 0; i < n; i++) dst[i] = (uint8_t)(src[i] ^ key[i % 32]);
}
int main(void) {
if (license_check() != 0) {
fprintf(stderr, "DRM failure: %s\n", license_last_error());
return 1;
}
uint8_t key[32];
if (license_derive_key(key) != 0) {
fprintf(stderr, "DRM failure: %s\n", license_last_error());
return 1;
}
uint8_t out[sizeof(CIPHERTEXT) + 1];
xor_stream(out, CIPHERTEXT, sizeof(CIPHERTEXT), key);
out[sizeof(CIPHERTEXT)] = '\0';
//fputs((const char*)out, stdout);
fprintf(stderr, "Hello2, %s!\n", "world");
return 0;
}
#define _GNU_SOURCE
#include "license.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <ctype.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
/* Fixed license path for the demo */
#ifndef LICENSE_PATH
#define LICENSE_PATH "/tmp/demo_license.txt"
#endif
/* DEMO ONLY: embedded secret used for HMAC */
static const uint8_t EMBEDDED_SECRET[] =
"DEMO-ONLY-SECRET-CHANGE-ME-KEEP-PRIVATE";
/* Thread-local last error */
static __thread char g_last_error[256];
/* Cached canonical string from last successful check (for derive_key) */
static __thread char g_last_canonical[1024];
static __thread int g_canonical_ok = 0;
static void set_err(const char *msg) {
snprintf(g_last_error, sizeof(g_last_error), "%s", msg);
}
const char *license_last_error(void) {
if (g_last_error[0] == '\0') return "no error";
return g_last_error;
}
/* ---------------- Minimal SHA-256 + HMAC-SHA256 (glibc-only) ---------------- */
typedef struct {
uint32_t h[8];
uint64_t len_bits;
uint8_t buf[64];
size_t buf_len;
} sha256_ctx;
static uint32_t rotr32(uint32_t x, uint32_t n) { return (x >> n) | (x << (32 - n)); }
static uint32_t ch(uint32_t x, uint32_t y, uint32_t z) { return (x & y) ^ (~x & z); }
static uint32_t maj(uint32_t x, uint32_t y, uint32_t z) { return (x & y) ^ (x & z) ^ (y & z); }
static uint32_t bsig0(uint32_t x) { return rotr32(x, 2) ^ rotr32(x,13) ^ rotr32(x,22); }
static uint32_t bsig1(uint32_t x) { return rotr32(x, 6) ^ rotr32(x,11) ^ rotr32(x,25); }
static uint32_t ssig0(uint32_t x) { return rotr32(x, 7) ^ rotr32(x,18) ^ (x >> 3); }
static uint32_t ssig1(uint32_t x) { return rotr32(x,17) ^ rotr32(x,19) ^ (x >> 10); }
static const uint32_t K[64] = {
0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u,
0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u,
0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau,
0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u,
0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u,
0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u,
0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u,
0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u
};
static void sha256_init(sha256_ctx *c) {
c->h[0]=0x6a09e667u; c->h[1]=0xbb67ae85u; c->h[2]=0x3c6ef372u; c->h[3]=0xa54ff53au;
c->h[4]=0x510e527fu; c->h[5]=0x9b05688cu; c->h[6]=0x1f83d9abu; c->h[7]=0x5be0cd19u;
c->len_bits = 0;
c->buf_len = 0;
}
static void sha256_transform(sha256_ctx *c, const uint8_t block[64]) {
uint32_t w[64];
for (int i=0;i<16;i++) {
w[i] = ((uint32_t)block[i*4+0] << 24) |
((uint32_t)block[i*4+1] << 16) |
((uint32_t)block[i*4+2] << 8) |
((uint32_t)block[i*4+3] << 0);
}
for (int i=16;i<64;i++) w[i] = ssig1(w[i-2]) + w[i-7] + ssig0(w[i-15]) + w[i-16];
uint32_t a=c->h[0], b=c->h[1], cc=c->h[2], d=c->h[3];
uint32_t e=c->h[4], f=c->h[5], g=c->h[6], h=c->h[7];
for (int i=0;i<64;i++) {
uint32_t t1 = h + bsig1(e) + ch(e,f,g) + K[i] + w[i];
uint32_t t2 = bsig0(a) + maj(a,b,cc);
h = g; g = f; f = e; e = d + t1;
d = cc; cc = b; b = a; a = t1 + t2;
}
c->h[0] += a; c->h[1] += b; c->h[2] += cc; c->h[3] += d;
c->h[4] += e; c->h[5] += f; c->h[6] += g; c->h[7] += h;
}
static void sha256_update(sha256_ctx *c, const void *data, size_t len) {
const uint8_t *p = (const uint8_t*)data;
c->len_bits += (uint64_t)len * 8;
while (len) {
size_t take = 64 - c->buf_len;
if (take > len) take = len;
memcpy(c->buf + c->buf_len, p, take);
c->buf_len += take;
p += take;
len -= take;
if (c->buf_len == 64) {
sha256_transform(c, c->buf);
c->buf_len = 0;
}
}
}
static void sha256_final(sha256_ctx *c, uint8_t out[32]) {
c->buf[c->buf_len++] = 0x80;
if (c->buf_len > 56) {
while (c->buf_len < 64) c->buf[c->buf_len++] = 0x00;
sha256_transform(c, c->buf);
c->buf_len = 0;
}
while (c->buf_len < 56) c->buf[c->buf_len++] = 0x00;
uint64_t L = c->len_bits;
for (int i=7;i>=0;i--) c->buf[c->buf_len++] = (uint8_t)((L >> (i*8)) & 0xff);
sha256_transform(c, c->buf);
for (int i=0;i<8;i++) {
out[i*4+0] = (uint8_t)((c->h[i] >> 24) & 0xff);
out[i*4+1] = (uint8_t)((c->h[i] >> 16) & 0xff);
out[i*4+2] = (uint8_t)((c->h[i] >> 8) & 0xff);
out[i*4+3] = (uint8_t)((c->h[i] >> 0) & 0xff);
}
}
static void hmac_sha256(const uint8_t *key, size_t key_len,
const uint8_t *msg, size_t msg_len,
uint8_t out[32]) {
uint8_t k0[64];
memset(k0, 0, sizeof(k0));
if (key_len > 64) {
sha256_ctx c;
uint8_t kh[32];
sha256_init(&c);
sha256_update(&c, key, key_len);
sha256_final(&c, kh);
memcpy(k0, kh, 32);
} else {
memcpy(k0, key, key_len);
}
uint8_t ipad[64], opad[64];
for (int i=0;i<64;i++) {
ipad[i] = (uint8_t)(k0[i] ^ 0x36);
opad[i] = (uint8_t)(k0[i] ^ 0x5c);
}
sha256_ctx inner;
uint8_t inner_hash[32];
sha256_init(&inner);
sha256_update(&inner, ipad, 64);
sha256_update(&inner, msg, msg_len);
sha256_final(&inner, inner_hash);
sha256_ctx outer;
sha256_init(&outer);
sha256_update(&outer, opad, 64);
sha256_update(&outer, inner_hash, 32);
sha256_final(&outer, out);
}
/* ---------------- Helpers ---------------- */
static int hexval(int c) {
if (c>='0' && c<='9') return c-'0';
if (c>='a' && c<='f') return 10 + (c-'a');
if (c>='A' && c<='F') return 10 + (c-'A');
return -1;
}
static int hex_decode_32(const char *hex, uint8_t out[32]) {
size_t n = strlen(hex);
if (n != 64) return -1;
for (size_t i=0;i<32;i++) {
int hi = hexval(hex[i*2]);
int lo = hexval(hex[i*2+1]);
if (hi < 0 || lo < 0) return -1;
out[i] = (uint8_t)((hi<<4) | lo);
}
return 0;
}
static int secure_memeq(const uint8_t *a, const uint8_t *b, size_t n) {
uint8_t r = 0;
for (size_t i=0;i<n;i++) r |= (uint8_t)(a[i] ^ b[i]);
return r == 0;
}
static char *trim(char *s) {
while (*s && isspace((unsigned char)*s)) s++;
char *e = s + strlen(s);
while (e > s && isspace((unsigned char)e[-1])) e--;
*e = '\0';
return s;
}
static int read_file_all(const char *path, char **out_buf, size_t *out_len) {
FILE *f = fopen(path, "rb");
if (!f) return -1;
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; }
long sz = ftell(f);
if (sz < 0) { fclose(f); return -1; }
if (fseek(f, 0, SEEK_SET) != 0) { fclose(f); return -1; }
char *buf = (char*)malloc((size_t)sz + 1);
if (!buf) { fclose(f); return -1; }
size_t rd = fread(buf, 1, (size_t)sz, f);
fclose(f);
if (rd != (size_t)sz) { free(buf); return -1; }
buf[sz] = '\0';
*out_buf = buf;
*out_len = (size_t)sz;
return 0;
}
static int read_machine_id(char out[128]) {
FILE *f = fopen("/etc/machine-id", "rb");
if (!f) return -1;
if (!fgets(out, 128, f)) { fclose(f); return -1; }
fclose(f);
char *t = trim(out);
memmove(out, t, strlen(t)+1);
return (out[0] ? 0 : -1);
}
static int read_hostname(char out[256]) {
if (gethostname(out, 256) != 0) return -1;
out[255] = '\0';
char *t = trim(out);
memmove(out, t, strlen(t)+1);
return (out[0] ? 0 : -1);
}
static int parse_date_yyyy_mm_dd(const char *s, time_t *out_end_local) {
if (strlen(s) != 10 || s[4] != '-' || s[7] != '-') return -1;
for (int i=0;i<10;i++) {
if (i==4 || i==7) continue;
if (!isdigit((unsigned char)s[i])) return -1;
}
int y = atoi(s);
int m = atoi(s+5);
int d = atoi(s+8);
if (m < 1 || m > 12 || d < 1 || d > 31) return -1;
struct tm tmv;
memset(&tmv, 0, sizeof(tmv));
tmv.tm_year = y - 1900;
tmv.tm_mon = m - 1;
tmv.tm_mday = d;
tmv.tm_hour = 23;
tmv.tm_min = 59;
tmv.tm_sec = 59;
tmv.tm_isdst = -1;
time_t t = mktime(&tmv); /* local time end-of-day */
if (t == (time_t)-1) return -1;
*out_end_local = t;
return 0;
}
/* ---------------- Anti-tamper best-effort checks ---------------- */
static int anti_tamper_env(void) {
/* Refuse common dynamic loader injection knobs */
if (getenv("LD_PRELOAD") && getenv("LD_PRELOAD")[0]) {
set_err("license: tamper detected (LD_PRELOAD set)");
return -1;
}
if (getenv("LD_AUDIT") && getenv("LD_AUDIT")[0]) {
set_err("license: tamper detected (LD_AUDIT set)");
return -1;
}
return 0;
}
static int anti_tamper_tracer(void) {
/* Refuse if being debugged (simple check) */
FILE *f = fopen("/proc/self/status", "rb");
if (!f) return 0; /* if unavailable, don't hard-fail in demo */
char line[256];
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "TracerPid:", 10) == 0) {
char *p = line + 10;
while (*p && isspace((unsigned char)*p)) p++;
int tpid = atoi(p);
fclose(f);
if (tpid != 0) {
set_err("license: tamper detected (debugger/tracer attached)");
return -1;
}
return 0;
}
}
fclose(f);
return 0;
}
static int anti_tamper_exe_perms(void) {
/* Refuse if the running executable is group/other writable */
struct stat st;
if (stat("/proc/self/exe", &st) != 0) return 0; /* best-effort */
if ((st.st_mode & S_IWGRP) || (st.st_mode & S_IWOTH)) {
set_err("license: tamper detected (executable is group/other-writable)");
return -1;
}
return 0;
}
/* ---------------- License format and validation ----------------
License file (order doesn't matter):
expires=2030-12-31
bind_type=hostname # hostname | machineid
bind_value=<hostname or machine-id>
sig=<64 hex chars> # HMAC-SHA256(secret, canonical)
Canonical string for signing (exactly):
expires=<expires>\n
bind_type=<bind_type>\n
bind_value=<bind_value>\n
------------------------------------------------------------------ */
int license_check(void) {
g_last_error[0] = '\0';
g_canonical_ok = 0;
if (anti_tamper_env() != 0) return 101;
if (anti_tamper_tracer() != 0) return 102;
if (anti_tamper_exe_perms() != 0) return 103;
char *buf = NULL;
size_t len = 0;
if (read_file_all(LICENSE_PATH, &buf, &len) != 0) {
set_err("license: license file not found or unreadable at " LICENSE_PATH);
return 1;
}
char expires[32] = {0};
char bind_type[32] = {0};
char bind_value[256] = {0};
char sig_hex[128] = {0};
char *save = NULL;
for (char *line = strtok_r(buf, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) {
char *t = trim(line);
if (*t == '\0' || *t == '#') continue;
char *eq = strchr(t, '=');
if (!eq) continue;
*eq = '\0';
char *k = trim(t);
char *v = trim(eq+1);
if (strcmp(k, "expires") == 0) snprintf(expires, sizeof(expires), "%s", v);
else if (strcmp(k, "bind_type") == 0) snprintf(bind_type, sizeof(bind_type), "%s", v);
else if (strcmp(k, "bind_value") == 0) snprintf(bind_value, sizeof(bind_value), "%s", v);
else if (strcmp(k, "sig") == 0) snprintf(sig_hex, sizeof(sig_hex), "%s", v);
}
if (!expires[0] || !bind_type[0] || !bind_value[0] || !sig_hex[0]) {
free(buf);
set_err("license: missing required fields (expires/bind_type/bind_value/sig)");
return 2;
}
time_t exp_end = 0;
if (parse_date_yyyy_mm_dd(expires, &exp_end) != 0) {
free(buf);
set_err("license: invalid expires date (expected YYYY-MM-DD)");
return 3;
}
time_t now = time(NULL);
if (now == (time_t)-1) {
free(buf);
set_err("license: unable to read system time");
return 4;
}
if (now > exp_end) {
free(buf);
set_err("license: license expired");
return 5;
}
/* Verify binding */
char actual[256] = {0};
if (strcmp(bind_type, "hostname") == 0) {
if (read_hostname(actual) != 0) {
free(buf);
set_err("license: unable to read hostname");
return 6;
}
} else if (strcmp(bind_type, "machineid") == 0) {
char mid[128] = {0};
if (read_machine_id(mid) != 0) {
free(buf);
set_err("license: unable to read /etc/machine-id");
return 7;
}
snprintf(actual, sizeof(actual), "%s", mid);
} else {
free(buf);
set_err("license: unknown bind_type (expected hostname|machineid)");
return 8;
}
if (strcmp(actual, bind_value) != 0) {
free(buf);
set_err("license: bind_value does not match this system");
return 9;
}
/* Build canonical (also cached for derive_key) */
int n = snprintf(g_last_canonical, sizeof(g_last_canonical),
"expires=%s\nbind_type=%s\nbind_value=%s\n",
expires, bind_type, bind_value);
if (n <= 0 || (size_t)n >= sizeof(g_last_canonical)) {
free(buf);
set_err("license: internal error building canonical string");
return 10;
}
uint8_t expected[32];
hmac_sha256(EMBEDDED_SECRET, sizeof(EMBEDDED_SECRET)-1,
(const uint8_t*)g_last_canonical, (size_t)n,
expected);
uint8_t provided[32];
if (hex_decode_32(sig_hex, provided) != 0) {
free(buf);
set_err("license: sig is not valid hex (expected 64 hex chars)");
return 11;
}
if (!secure_memeq(expected, provided, 32)) {
free(buf);
set_err("license: signature mismatch (invalid license)");
return 12;
}
g_canonical_ok = 1;
free(buf);
return 0;
}
int license_derive_key(uint8_t out_key32[32]) {
if (!out_key32) {
set_err("license: derive_key called with NULL buffer");
return 1;
}
if (!g_canonical_ok) {
set_err("license: derive_key called before successful license_check()");
return 2;
}
/* Key derivation: HMAC(secret, "derive-key-v1\n" + canonical) */
char msg[1200];
int n = snprintf(msg, sizeof(msg), "derive-key-v1\n%s", g_last_canonical);
if (n <= 0 || (size_t)n >= sizeof(msg)) {
set_err("license: internal error building derive message");
return 3;
}
hmac_sha256(EMBEDDED_SECRET, sizeof(EMBEDDED_SECRET)-1,
(const uint8_t*)msg, (size_t)n,
out_key32);
return 0;
}
#ifndef LICENSE_H
#define LICENSE_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Returns 0 if valid, nonzero if invalid. */
int license_check(void);
/* Human-readable error for last failure in this thread. */
const char *license_last_error(void);
/*
* After a successful license_check(), derive a 32-byte key from the license
* metadata. Returns 0 on success.
*/
int license_derive_key(uint8_t out_key32[32]);
#ifdef __cplusplus
}
#endif
#endif /* LICENSE_H */
CC ?= cc
CFLAGS ?= -O2 -Wall -Wextra
# Change if you want a stricter install path
LICENSE_PATH ?= /tmp/demo_license.txt
CFLAGS += -DLICENSE_PATH=\"$(LICENSE_PATH)\"
SECRET ?= DEMO-ONLY-SECRET-CHANGE-ME-KEEP-PRIVATE
EXPIRES ?= 2030-12-31
BIND_TYPE ?= hostname
BIND_VALUE ?= $$(hostname)
all: hello1 hello2
hello1: hello1.o license.o
$(CC) -o $@ hello1.o license.o
hello2: hello2.o license.o
$(CC) -o $@ hello2.o license.o
hello1.o: hello1.c license.h
$(CC) $(CFLAGS) -c -o $@ hello1.c
hello2.o: hello2.c license.h
$(CC) $(CFLAGS) -c -o $@ hello2.c
license.o: license.c license.h
$(CC) $(CFLAGS) -c -o $@ license.c
sig:
@printf "expires=%s\nbind_type=%s\nbind_value=%s\nsig=%s\n" "$(EXPIRES)" "$(BIND_TYPE)" "$(BIND_VALUE)" "$$(printf "expires=%s\nbind_type=%s\nbind_value=%s\n" "$(EXPIRES)" "$(BIND_TYPE)" "$(BIND_VALUE)" | openssl dgst -sha256 -hmac "$(SECRET)" -hex | awk '{print $$2}')" > /tmp/demo_license.txt && echo "Created /tmp/demo_license.txt" || echo "Error creating /tmp/demo_license.txt"
clean:
rm -f *.o hello1 hello2 /tmp/demo_license.txt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment