├── .gitignore ├── src ├── .gitignore ├── Makefile └── dexor.c ├── Makefile ├── unxor.py ├── scrape.sh ├── libexec └── args.sh ├── download.sh └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.bin 3 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | dexor 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | override DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) 2 | 3 | .PHONY: all 4 | all: print-monolithic-eac-download-script 5 | 6 | .PHONY: print-monolithic-eac-download-script 7 | print-monolithic-eac-download-script: 8 | @cat "$(DIR)libexec/args.sh" && tail -n+4 "$(DIR)download.sh" 9 | -------------------------------------------------------------------------------- /unxor.py: -------------------------------------------------------------------------------- 1 | #curl https://download.eac-cdn.com/api/v1/games/55/client/win64/download/?uuid=0 --output EAC 2 | 3 | # Decode the first kilobyte of binary data 4 | 5 | # ./unxor.py binary_data.eac 6 | 7 | import sys 8 | 9 | data = bytearray(open(sys.argv[1], "rb").read()) 10 | 11 | for i in range(1024): 12 | data[i] ^= (i * 3) & 0xff 13 | 14 | open("./out", "wb").write(data) -------------------------------------------------------------------------------- /scrape.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | unset -n __download_sh && 4 | unset __download_sh && 5 | readonly __download_sh="${0%/*}/download.sh" 6 | 7 | __fetch() { 8 | local os 9 | for os in "$@"; do 10 | "$__download_sh" --os-type="$os" --from-id=1 --to-id=1000 11 | done 12 | } 13 | 14 | __report() { 15 | local os file 16 | local -i real null 17 | for os in "$@"; do 18 | file="eac-games-0001-to-1000-$os.log" 19 | real=$(grep '\.bin$' "$file" | wc -l) 20 | null=$(grep -E '[[:blank:]]0[[:blank:]]' "$file" | wc -l) 21 | echo -e "\n$os" 22 | printf ' %-10s: %3d\n' 'real blobs' "$real" \ 23 | 'null blobs' "$null" \ 24 | 'total' "$((real+$null))" 25 | done 26 | } 27 | 28 | __scrape() { 29 | __fetch "$@" 30 | __report "$@" 31 | } 32 | 33 | __scrape {linux,win{,e}}{32,64} wow64 34 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | override DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) 2 | override SRC := $(wildcard $(DIR)*.c) 3 | override OBJ := $(SRC:.c=.o) 4 | 5 | override HEAD_CFLAGS := -std=gnu11 -Wall -Wextra 6 | override TAIL_CFLAGS := $(if $(strip $(CFLAGS)),$(CFLAGS),-O3 -ggdb) 7 | 8 | override CFLAGS := $(HEAD_CFLAGS) $(TAIL_CFLAGS) 9 | override TARGET := dexor 10 | 11 | CC := gcc 12 | 13 | .PHONY: all clean $(TARGET) 14 | 15 | all: $(TARGET) 16 | 17 | clean: 18 | rm -fr $(DIR)$(TARGET) $(OBJ) 19 | 20 | $(TARGET): $(DIR)$(TARGET) 21 | 22 | override define build-obj = 23 | .PHONY: $$(notdir $(1)) 24 | $$(notdir $(1)): $(1) 25 | 26 | $(1): $(1:.o=.c) 27 | cd "$$(DIR)" && \ 28 | $$(CC) $$(CFLAGS) -c $$(notdir $$^) -o $$(notdir $$@) 29 | endef 30 | 31 | $(foreach obj,$(OBJ),$(eval $(call build-obj,$(obj)))) 32 | 33 | $(DIR)$(TARGET): $(OBJ) 34 | cd "$(DIR)" && $(CC) $(CFLAGS) $(notdir $^) -o $(notdir $@) 35 | -------------------------------------------------------------------------------- /libexec/args.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | unset opts_value 4 | declare -A opts_value 5 | export opts_value 6 | 7 | # 8 | # Show valid command-line options and exit. First argument must be program name. 9 | # 10 | usage() 11 | { 12 | local argv0="$1" retval=$(($2)) i n 13 | echo -n "Usage: $argv0 [ -h | --help ]" >&2 14 | for ((i = 0, n=${#options[@]}; i < n; i++)); do 15 | echo -n " [ --${options[i]}=VALUE ]" >&2 16 | done 17 | echo >&2 18 | exit $retval 19 | } 20 | 21 | # 22 | # Parse command-line arguments. First argument must be program name. 23 | # 24 | parse_args() 25 | { 26 | local argv0="$1" opts_regex arg opt 27 | 28 | opts_regex="${options[@]}" 29 | opts_regex="${opts_regex// /|}" 30 | 31 | while (( $# > 1 )); do 32 | shift 33 | 34 | if [[ "$1" =~ ^--($opts_regex)(=(.*))?$ ]]; then 35 | opt="${BASH_REMATCH[1]}" 36 | 37 | if [[ "${BASH_REMATCH[-2]}" ]]; then 38 | arg="${BASH_REMATCH[-1]}" 39 | else 40 | shift 41 | arg="$1" 42 | fi 43 | 44 | opts_value[$opt]="$arg" 45 | 46 | elif [[ "$1" =~ ^-(h|-help)$ ]]; then 47 | usage "$argv0" 0 48 | 49 | else 50 | usage "$argv0" 1 51 | fi 52 | done 53 | } 54 | -------------------------------------------------------------------------------- /download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . "$(dirname "$0")/libexec/args.sh" 4 | 5 | # Command-line argument list 6 | options=( 7 | 'os-type' 8 | # 'id-list' 9 | 'id' 10 | 'from-id' 11 | 'to-id' 12 | 'uuid' 13 | ) 14 | 15 | # Program name 16 | argv0="$(basename "$0")" 17 | 18 | # Parse command-line arguments 19 | parse_args "$argv0" "$@" 20 | 21 | # Copy arguments to less fugly variables 22 | os_type="${opts_value['os-type']}" 23 | #id_list="${opts_value['id-list']}" 24 | id="${opts_value['id']}" 25 | from_id="${opts_value['from-id']}" 26 | to_id="${opts_value['to-id']}" 27 | uuid="${opts_value['uuid']}" 28 | 29 | # Check arguments' sanity somewhat 30 | if [[ "$id" ]]; then 31 | [[ -z "$from_id$to_id" ]] || { 32 | echo "Mixing '--id' with '--from-id' and/or '--to-id' is forbidden" >&2 33 | usage "$argv0" 1 34 | } 35 | from_id="$id" 36 | to_id="$id" 37 | else 38 | [[ "$from_id" && "$to_id" ]] || { 39 | echo "Not enough arguments: need a game ID or range of IDs" >&2 40 | usage "$argv0" 1 41 | } 42 | fi 43 | 44 | # Fill in default arguments where applicable 45 | [[ "$os_type" ]] || { 46 | os_type=wine64 47 | echo "No '--os-type' argument, defaulting to $os_type" >&2 48 | } 49 | 50 | #[[ "$id_list" ]] || id_list="$(dirname "$0")/game_ids.list" 51 | 52 | # Shim for missing uuidgen 53 | command -v uuidgen >/dev/null || uuidgen() { 54 | hexdump -vn 16 -e '4/1 "%02x" "-" 2/1 "%02x" "-" 2/1 "%02x" "-" 2/1 "%02x" "-" 6/1 "%02x" "\n"' /dev/urandom 2>/dev/null 55 | } 56 | 57 | urlhead='https://download.eac-cdn.com/api/v1/games' 58 | urltail="client/$os_type/download/?uuid=" 59 | 60 | # printf format specifier for zero-padding game id 61 | id_fmt0="%0${#to_id}d" 62 | 63 | if [[ "$id" ]]; then 64 | logfile=$(printf "eac-game-$id_fmt0-$os_type.log" "$id") 65 | else 66 | logfile=$(printf "eac-games-$id_fmt0-to-$id_fmt0-$os_type.log" "$from_id" "$to_id") 67 | fi 68 | 69 | tmpfile="/dev/shm/eac-$os_type.bin" 70 | 71 | rm -f "$tmpfile" 72 | 73 | { 74 | echo -e ' game id\t dl size\tlast modified (UTC)\tdownload saved as' 75 | echo -e ' -------\t -------\t-------------------\t-----------------' 76 | 77 | for ((i = from_id; i <= to_id; i++)); do 78 | url="$urlhead/$i/$urltail$uuid" 79 | [[ "$uuid" ]] || url="$url$(uuidgen)" 80 | 81 | if wget -O "$tmpfile" "$url" 2>/dev/null && 82 | st=$(stat -c '%s %Y' "$tmpfile"); then 83 | 84 | len="${st%% *}" 85 | st=$(date -ud@"${st##* }" '+%Y-%m-%d %H:%M:%S') 86 | 87 | if (( len > 0 )); then 88 | [[ "$(head -c5 "$tmpfile")" == ' 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | enum { 14 | HEADER_SIZE = 1024u, 15 | LINE_LENGTH = 16u 16 | }; 17 | 18 | static bool 19 | read_header (const char *path, 20 | uint8_t *dest); 21 | 22 | static void 23 | print_header (const uint8_t *data); 24 | 25 | int 26 | main (int argc, 27 | char **argv) 28 | { 29 | uint8_t data[1024]; 30 | 31 | if (argc < 2 || argv[1] == NULL) { 32 | fprintf(stderr, "Usage: %s FILENAME\n", argv[0]); 33 | return EXIT_FAILURE; 34 | } 35 | 36 | if (!read_header((const char *)argv[1], data)) { 37 | return EXIT_FAILURE; 38 | } 39 | 40 | print_header((const uint8_t *)data); 41 | 42 | return EXIT_SUCCESS; 43 | } 44 | 45 | static bool 46 | read_header (const char *path, 47 | uint8_t *dest) 48 | { 49 | bool success = false; 50 | int err_code = 0; 51 | const char *err_fn = NULL; 52 | 53 | do { 54 | FILE *stream; 55 | 56 | stream = fopen(path, "rbe"); 57 | if (stream == NULL) { 58 | err_code = errno; 59 | err_fn = ": fopen"; 60 | break; 61 | } 62 | 63 | success = (fread((void *)dest, 1u, HEADER_SIZE, stream) >= HEADER_SIZE); 64 | if (!success) { 65 | err_code = errno; 66 | 67 | if (ferror(stream) != 0) { 68 | /* read error */ 69 | err_fn = ": fread"; 70 | 71 | } else { 72 | /* file is smaller than HEADER_SIZE */ 73 | err_code = ENODATA; 74 | err_fn = ""; 75 | } 76 | } 77 | 78 | if (fclose(stream) != 0 && err_fn == NULL) { 79 | err_code = errno; 80 | err_fn = ": fclose"; 81 | } 82 | } while(0); 83 | 84 | if (err_fn != NULL) { 85 | fprintf(stderr, "%s%s: %s\n", __func__, err_fn, strerror(err_code)); 86 | } 87 | 88 | return success; 89 | } 90 | 91 | __attribute__ ((__always_inline__)) 92 | static inline uint8_t 93 | dexor_byte (const uint8_t *data, 94 | unsigned int offset) 95 | { 96 | return data[offset] ^ ((offset * 3u) & 0xffu); 97 | } 98 | 99 | __attribute__ ((__always_inline__)) 100 | static inline void 101 | print_hex_char (char *dest, 102 | uint8_t value) 103 | { 104 | static const char HEX_DIGIT[16] = { 105 | '0', '1', '2', '3', 106 | '4', '5', '6', '7', 107 | '8', '9', 'a', 'b', 108 | 'c', 'd', 'e', 'f' 109 | }; 110 | dest[0] = HEX_DIGIT[value >> 4]; 111 | dest[1] = HEX_DIGIT[value & 0xfu]; 112 | } 113 | 114 | static void 115 | print_header (const uint8_t *data) 116 | { 117 | char line_buf[LINE_LENGTH * 3u]; 118 | unsigned int data_left = HEADER_SIZE; 119 | unsigned int i = 0; 120 | 121 | while (data_left > 0u) { 122 | char *ptr = line_buf; 123 | unsigned int end = (data_left < LINE_LENGTH) ? data_left : LINE_LENGTH; 124 | data_left -= end; 125 | end += i; 126 | for (;;) { 127 | print_hex_char(ptr, dexor_byte(data, i)); 128 | ptr += 2; 129 | if (++i >= end) { 130 | break; 131 | } 132 | *ptr++ = ' '; 133 | } 134 | *ptr = '\0'; 135 | puts((const char *)line_buf); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EAC Tools 2 | 3 | *"Making Easy Anti-Cheat work in Wine one ragequit at a time."* 4 | 5 | A little something to help figure out the games and platforms EAC serves binary blobs for on their CDN. 6 | 7 | ## Usage 8 | 9 | Download the 64-bit Wine binary blob for game ID 55 (implicit `--os-type=wine64`): 10 | 11 | $ ./download.sh --id=55 12 | 13 | Download win64 binary blobs for game IDs beginning from 1 up to and including 550: 14 | 15 | $ ./download.sh --os-type=win64 --from-id=1 --to-id=550 16 | 17 | Confirmed valid `--os-type` values are `win32`, `win64`, `wow64`, `wine32`, and `wine64`. Others may also exist. 18 | 19 | ### Fun things to try 20 | 21 | * Scrape wine64 binary blob downloads for game IDs 20 to 50 (inclusive): 22 | 23 | $ ./download.sh --os-type=wine64 --from-id=20 --to-id=50 24 | game id dl size last modified (UTC) download saved as 25 | ------- ------- ------------------- ----------------- 26 | 21 218624 2019-04-11 13:04:43 eac-game-021-wine64.bin 27 | 36 242688 2019-04-11 12:52:23 eac-game-036-wine64.bin 28 | 43 0 2020-06-18 09:38:26 29 | 45 0 2019-11-13 11:39:44 30 | 49 0 2019-07-08 10:54:32 31 | 32 | * Try the same with `--os-type=win64` to realize how badly EAC neglects Wine: 33 | 34 | $ ./download.sh --os-type=win64 --from-id=20 --to-id=50 35 | game id dl size last modified (UTC) download saved as 36 | ------- ------- ------------------- ----------------- 37 | 20 3543680 2020-06-09 08:27:24 eac-game-20-win64.bin 38 | 21 3503232 2020-06-08 10:00:17 eac-game-21-win64.bin 39 | 24 770320 2019-04-11 12:56:41 eac-game-24-win64.bin 40 | 28 1616512 2019-09-25 08:41:30 eac-game-28-win64.bin 41 | 29 3481728 2020-07-31 13:31:58 eac-game-29-win64.bin 42 | 32 728848 2019-04-11 12:56:50 eac-game-32-win64.bin 43 | 35 2501744 2019-08-13 13:33:24 eac-game-35-win64.bin 44 | 36 3494528 2020-07-31 13:52:46 eac-game-36-win64.bin 45 | 37 2412656 2019-08-27 14:49:04 eac-game-37-win64.bin 46 | 38 782608 2019-04-11 13:00:46 eac-game-38-win64.bin 47 | 39 806672 2019-04-11 13:07:30 eac-game-39-win64.bin 48 | 40 708368 2019-04-11 13:10:37 eac-game-40-win64.bin 49 | 42 740624 2019-04-11 13:09:47 eac-game-42-win64.bin 50 | 43 2963072 2020-06-25 11:40:10 eac-game-43-win64.bin 51 | 45 2926720 2020-06-09 14:13:15 eac-game-45-win64.bin 52 | 46 1523240 2019-04-11 13:07:54 eac-game-46-win64.bin 53 | 47 1722496 2019-04-11 13:00:51 eac-game-47-win64.bin 54 | 48 3492480 2020-06-09 07:45:46 eac-game-48-win64.bin 55 | 49 2491504 2019-08-13 13:34:14 eac-game-49-win64.bin 56 | --------------------------------------------------------------------------------