├── .gitattributes ├── .gitignore ├── Dockerfile.build ├── Dockerfile.extract ├── README.md ├── checksum.c ├── docker ├── build_dol.sh ├── build_qemu.sh └── extract.sh ├── docs ├── Defeating Devolution.md └── devo_patches.txt ├── extract_standalone.sh ├── patches ├── devo_patch.xdelta3 ├── donor.bca ├── keygen_only_patch.xdelta3 ├── main.patch └── preloader.bin ├── put_gc_devo_src_zip_here └── put_gc_devo_src_zip_here ├── qemu ├── add_broadway.patch └── broadway.c ├── run_docker.bat ├── run_docker.sh └── scripts ├── decode_ghidra.py ├── decrypt_dsp_payload.py ├── decrypt_dvv.py ├── devo_go.gdb ├── devo_go_standalone.gdb ├── devo_unpack.py ├── devo_unpack_gdb.py ├── munge_ghidra.py └── wifilog.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | *.swp 50 | *.swo 51 | put_gc_devo_src_zip_here/* 52 | !put_gc_devo_src_zip_here/put_gc_devo_src_zip_here 53 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM devkitpro/devkitppc 2 | 3 | ARG USER_ID 4 | ARG GROUP_ID 5 | RUN addgroup --gid $GROUP_ID user 6 | RUN adduser --disabled-password --gecos '' --uid $USER_ID --gid $GROUP_ID user 7 | 8 | ENV LANG C.UTF-8 9 | ENV LC_ALL C.UTF-8 10 | ARG DEBIAN_FRONTEND=noninteractive 11 | 12 | RUN set -x && \ 13 | apt-get update -y && \ 14 | apt-get install -y --no-install-recommends patch 15 | 16 | WORKDIR /devo 17 | RUN chown user:user /devo 18 | USER user 19 | ADD ./docker/build_dol.sh . 20 | ADD ./patches/main.patch . 21 | RUN mkdir shared 22 | -------------------------------------------------------------------------------- /Dockerfile.extract: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ARG USER_ID 4 | ARG GROUP_ID 5 | 6 | RUN addgroup --gid $GROUP_ID user 7 | RUN adduser --disabled-password --gecos '' --uid $USER_ID --gid $GROUP_ID user 8 | 9 | ENV LANG C.UTF-8 10 | ENV LC_ALL C.UTF-8 11 | ARG DEBIAN_FRONTEND=noninteractive 12 | 13 | RUN set -x && \ 14 | apt-get update -y && \ 15 | apt-get install -y --no-install-recommends build-essential wget gdb-multiarch ca-certificates python3 python-is-python3 ninja-build pkg-config libglib2.0-dev libpixman-1-dev xdelta3 unzip 16 | 17 | WORKDIR /qemu 18 | ADD ./qemu/add_broadway.patch . 19 | ADD ./docker/build_qemu.sh . 20 | RUN ./build_qemu.sh 21 | 22 | WORKDIR /devo 23 | RUN chown user:user /devo 24 | USER user 25 | ADD ./docker/extract.sh . 26 | ADD ./patches ./patches 27 | ADD ./scripts ./scripts 28 | RUN mkdir shared 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unpacking Devolution (PowerPC instrumentation on PC) 2 | 3 | ----- 4 | 5 | Originally from [this](http://oct0xor.github.io/2018/05/06/unpacking_devolution/) blog post and [this](https://github.com/oct0xor/unpacking_devolution) github repo. 6 | 7 | Updated with automated docker scripts to automatically extract and patch Devolution. 8 | -------------------------------------------------------------------------------- /checksum.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // Program to bruteforce Devolution's anti-tamper checksum by fiddling the 4-5 bytes at the end of it 11 | // As far as I can tell I've never needed more than 34 bits to bruteforce the checksum 12 | // Set N_THREADS to the number of threads you want this to use 13 | 14 | typedef union { 15 | struct __attribute__((packed)) { 16 | uint16_t cpu_to_dsp_h; // 0x400 17 | uint16_t cpu_to_dsp_l; // 0x401 18 | uint16_t unk_0; // 0x402 19 | uint16_t unlock_len; // 0x403 20 | uint16_t aram_addr_h; // 0x404 21 | uint16_t aram_addr_l; // 0x405 22 | uint16_t dsp_to_cpu_h; // 0x406 23 | uint16_t dsp_to_cpu_l; // 0x407 24 | }; 25 | uint16_t work[0x12]; 26 | } data_u; 27 | 28 | typedef struct { 29 | uint8_t *buffer; 30 | uint32_t addr; 31 | uint32_t size; 32 | uint16_t gain; 33 | uint16_t a1; 34 | uint16_t a2; 35 | uint16_t yn1; 36 | uint16_t yn2; 37 | } accel_s; 38 | 39 | void card4_subfunc(data_u *data, uint32_t acc0) 40 | { 41 | uint32_t acc1; 42 | acc0 = acc0 + (data->work[0x0e] << 16 | data->work[0x0f]); 43 | acc1 = (acc0 & 0xFFFF) << 16; 44 | acc1 ^= data->work[0x0d] << 16; 45 | acc1 >>= 16; 46 | acc1 = (acc0 & 0xFFFF0000) | (acc1 & 0xFFFF); 47 | acc1 ^= data->work[0x0c] << 16; acc0 = (acc0 & 0xFFFF0000) | (data->work[0x11]); 48 | acc0++; 49 | data->work[0x11] = acc0 & 0xFFFF; 50 | acc0 += data->work[0x10]; 51 | acc0 &= 0x1f; acc0 <<= 16; 52 | uint16_t shift0 = acc0 >> 16; 53 | acc0 = ((acc0 & 0xFFFF0000) + (int32_t)((uint32_t)-0x20 << 16)) | (acc0 & 0xFFFF); 54 | uint16_t shift1 = acc0 >> 16; acc0 = acc1; 55 | if (shift0 & 64) 56 | { 57 | if (shift0 & 63) 58 | { 59 | uint16_t true_shift = 64 - (shift0 & 0x3f); 60 | if (true_shift >= 32) 61 | { 62 | acc0 = 0; 63 | } 64 | else 65 | { 66 | acc0 >>= true_shift; 67 | } 68 | } 69 | } 70 | else 71 | { 72 | uint16_t true_shift = shift0 & 0x3f; 73 | if (true_shift >= 32) 74 | { 75 | acc0 = 0; 76 | } 77 | else 78 | { 79 | acc0 <<= shift0 & 0x3f; 80 | } 81 | } 82 | if (shift1 & 64) 83 | { 84 | if (shift1 & 63) 85 | { 86 | uint16_t true_shift = 64 - (shift1 & 0x3f); 87 | if (true_shift >= 32) 88 | { 89 | acc1 = 0; 90 | } 91 | else 92 | { 93 | acc1 >>= true_shift; 94 | } 95 | } 96 | } 97 | else 98 | { 99 | uint16_t true_shift = shift1 & 0x3f; 100 | if (true_shift >= 32) 101 | { 102 | acc1 = 0; 103 | } 104 | else 105 | { 106 | acc1 <<= shift1 & 0x3f; 107 | } 108 | } 109 | acc0 += acc1; 110 | acc0 = acc0 + (data->work[0x0a] << 16 | data->work[0x0b]); 111 | data->work[0x0b] = acc0 & 0xFFFF; 112 | data->work[0x0a] = acc0 >> 16; 113 | acc0 ^= data->work[0x08] << 16; 114 | acc0 ^= data->work[0x0e] << 16; 115 | data->work[0x0c] = acc0 >> 16; acc0 <<= 16; 116 | acc0 ^= data->work[0x09] << 16; 117 | acc0 ^= data->work[0x0f] << 16; 118 | data->work[0x0d] = acc0 >> 16; 119 | acc1 = ((~data->work[0x09] & 0xFFFF) << 16) | (acc1 & 0xFFFF); 120 | acc1 &= (acc0 & 0xFFFF0000) | 0xFFFF; 121 | acc0 = (data->work[0x09] << 16) | (acc0 & 0xFFFF); 122 | acc0 &= (data->work[0x0b] << 16) | 0xFFFF; 123 | acc0 |= acc1 & 0xFFFF0000; acc1 = ((~data->work[0x08] & 0xFFFF) << 16) | (acc1 & 0xFFFF); 124 | acc0 >>= 16; 125 | acc1 &= (data->work[0x0c] << 16) | 0xFFFF; acc0 = (data->work[0x08] << 16) | (acc0 & 0xFFFF); 126 | acc0 &= (data->work[0x0a] << 16) | 0xFFFF; 127 | acc0 |= (acc1 & 0xFFFF0000); acc1 = (data->work[0x0e] << 16) | (acc1 & 0xFFFF); 128 | acc1 = (acc1 & 0xFFFF0000) | (data->work[0x0f]); 129 | acc0 += acc1; 130 | data->work[0x0e] = (acc0 >> 16) & 0xFFFF; 131 | data->work[0x0f] = acc0 & 0xFFFF; 132 | } 133 | 134 | uint16_t read_accel(accel_s *accel) 135 | { 136 | uint16_t data; 137 | if (accel->addr & 1) 138 | { 139 | data = accel->buffer[accel->addr/2] & 0xF; 140 | } 141 | else 142 | { 143 | data = accel->buffer[accel->addr/2] >> 4; 144 | } 145 | accel->addr++; 146 | data = (data * accel->gain + accel->yn1 * accel->a1 + accel->yn2 * accel->a2); 147 | accel->yn2 = accel->yn1; 148 | accel->yn1 = data; 149 | return data; 150 | } 151 | 152 | void card4_part3(data_u *data, accel_s *accel, uint32_t bytes_to_read) 153 | { 154 | uint32_t new = ((data->work[0x08] << 16) | data->work[0x09]) + ((read_accel(accel) << 16) | read_accel(accel)); 155 | data->work[0x08] = new >> 16; 156 | data->work[0x09] = new & 0xFFFF; 157 | for (int i = 0; i < bytes_to_read - 1; i++) 158 | { 159 | card4_subfunc(data, new); 160 | new = ((data->work[0x08] << 16) | data->work[0x09]) + ((read_accel(accel) << 16) | read_accel(accel)); 161 | data->work[0x08] = new >> 16; 162 | data->work[0x09] = new & 0xFFFF; 163 | } 164 | card4_subfunc(data, new); 165 | } 166 | 167 | void card4_part2(data_u *data, accel_s *accel, uint32_t acc0, uint16_t byte_len) 168 | { 169 | uint32_t acc1 = acc0; 170 | acc0 = (int32_t)acc0 + (int32_t)0x4ea21e71; 171 | data->work[0x08] = acc0 >> 16; 172 | data->work[0x09] = acc0 & 0xFFFF; 173 | data->work[0x0a] = 0xcc0a; 174 | data->work[0x0b] = 0x144b; 175 | data->work[0x0c] = 0xf541; 176 | data->work[0x0d] = 0x878d; 177 | data->work[0x0e] = 0xa3bc; 178 | acc1 += 3; data->work[0x0f] = 0x64e4; 179 | data->work[0x10] = acc1 & 0xFFFF; 180 | data->work[0x11] = 0; 181 | accel->addr = 0; 182 | accel->gain = 0xfc82; 183 | accel->a1 = 0x978; 184 | accel->a2 = 0xe541; 185 | accel->yn1 = 0x0000; 186 | accel->yn2 = 0x0000; 187 | card4_part3(data, accel, byte_len); 188 | } 189 | 190 | typedef struct { 191 | uint64_t id; 192 | uint32_t n; 193 | uint32_t bytes; 194 | uint32_t payload_len; 195 | data_u *data_bak; 196 | accel_s *accel_bak; 197 | } parallel_info; 198 | 199 | atomic_bool found = ATOMIC_VAR_INIT(false); 200 | uint8_t patch_buf[8]; 201 | 202 | void* bruteforce_func(void* usrdata) 203 | { 204 | data_u ldata; 205 | accel_s laccel; 206 | parallel_info *info = (parallel_info*)usrdata; 207 | uint8_t dumb_buf[8]; 208 | uint32_t magic_other_half = 0x735f0000 + info->payload_len; 209 | for (uint64_t val = info->id; val < (1ULL << info->bytes * 8); val += info->n) 210 | { 211 | memcpy(&ldata, info->data_bak, sizeof(data_u)); 212 | memcpy(&laccel, info->accel_bak, sizeof(accel_s)); 213 | laccel.buffer = &dumb_buf[0]; 214 | laccel.addr = 0; 215 | for (int o = 0; o < info->bytes; o++) 216 | { 217 | laccel.buffer[o] = val >> ((info->bytes*8) - ((o+1)*8)); 218 | } 219 | card4_part3(&ldata, &laccel, info->bytes); 220 | uint32_t checksum = (ldata.work[0x0a] << 16) | ldata.work[0x0b]; 221 | // DSP check on the left (lower 16-bits must be 0xFFFF), PPC check on the right 222 | if (((checksum + info->payload_len) & 0xFFFF) == 0xFFFF && (checksum ^ magic_other_half) == 0x7FFFFFFF) 223 | { 224 | printf("Magic value is %d bytes: 0x%08lx (0x%08x 0x%08x)\n", info->bytes, val, info->payload_len, checksum + info->payload_len); 225 | found = true; 226 | memcpy(&patch_buf[8-info->bytes], &dumb_buf[0], 8); 227 | break; 228 | } 229 | if (found) 230 | { 231 | break; 232 | } 233 | } 234 | 235 | return NULL; 236 | } 237 | 238 | int main(int argc, char *argv[]) 239 | { 240 | if (argc < 2) 241 | { 242 | printf ("checksum requires one file argument\n"); 243 | return 1; 244 | } 245 | FILE *ppc_payload = fopen(argv[1], "rb"); 246 | if (ppc_payload == NULL) 247 | { 248 | printf("Unable to open payload %s\n", argv[1]); 249 | return 2; 250 | } 251 | fseek(ppc_payload, 0L, SEEK_END); 252 | uint32_t payload_len = ftell(ppc_payload); 253 | rewind(ppc_payload); 254 | data_u data; 255 | accel_s accel; 256 | accel.buffer = malloc(payload_len); 257 | uint64_t jeff; 258 | if (fread(accel.buffer, payload_len, 1, ppc_payload) != 1) 259 | { 260 | printf("Didn't read enough bytes!\n"); 261 | return 3; 262 | } 263 | fclose(ppc_payload); 264 | payload_len = 0x30974; 265 | card4_part2(&data, &accel, 0, payload_len % 0x10000); 266 | for (int i = 0; i < payload_len / 0x10000 - 1; i++) 267 | { 268 | card4_part3(&data, &accel, 0x10000); 269 | } 270 | card4_part3(&data, &accel, 0x10000-5); 271 | accel.buffer[0x3096F] = 0xFF; // Try to bruteforce with this byte as 0xFF first 272 | data_u data_bak_5; 273 | accel_s accel_bak_5; 274 | memcpy(&data_bak_5, &data, sizeof(data_u)); 275 | memcpy(&accel_bak_5, &accel, sizeof(accel_s)); 276 | card4_part3(&data, &accel, 1); 277 | data_u data_bak_4; 278 | accel_s accel_bak_4; 279 | memcpy(&data_bak_4, &data, sizeof(data_u)); 280 | memcpy(&accel_bak_4, &accel, sizeof(accel_s)); 281 | 282 | // Bruteforce the checksum 283 | #ifndef N_THREADS 284 | #define N_THREADS 8 285 | #endif 286 | memset(&patch_buf[0], 0xFF, 8); 287 | uint8_t n_bytes = 4; 288 | pthread_t threads[N_THREADS]; 289 | parallel_info info[N_THREADS]; 290 | for (int i = 0; i < N_THREADS; i++) 291 | { 292 | info[i].id = i; 293 | info[i].n = N_THREADS; 294 | info[i].bytes = n_bytes; 295 | info[i].payload_len = payload_len; 296 | info[i].data_bak = &data_bak_4; 297 | info[i].accel_bak = &accel_bak_4; 298 | pthread_create(&threads[i], NULL, bruteforce_func, &info[i]); 299 | } 300 | for (int i = 0; i < N_THREADS; i++) 301 | { 302 | pthread_join(threads[i], NULL); 303 | } 304 | if (!found) { 305 | n_bytes = 5; 306 | printf("Doing 5-byte bruteforce. This will take a while.\n"); 307 | pthread_t threads[N_THREADS]; 308 | parallel_info info[N_THREADS]; 309 | for (int i = 0; i < N_THREADS; i++) 310 | { 311 | info[i].id = i; 312 | info[i].n = N_THREADS; 313 | info[i].bytes = n_bytes; 314 | info[i].payload_len = payload_len; 315 | info[i].data_bak = &data_bak_5; 316 | info[i].accel_bak = &accel_bak_5; 317 | pthread_create(&threads[i], NULL, bruteforce_func, &info[i]); 318 | } 319 | for (int i = 0; i < N_THREADS; i++) 320 | { 321 | pthread_join(threads[i], NULL); 322 | } 323 | } 324 | if (!found) { 325 | printf("Warning: could not find checksum!\n"); 326 | } 327 | else 328 | { 329 | FILE *ppc_payload = fopen(argv[1], "r+b"); 330 | if (ppc_payload == NULL) 331 | { 332 | printf("Unable to open payload %s for writing\n", argv[1]); 333 | return 4; 334 | } 335 | fseek(ppc_payload, 0x30974 - 8, SEEK_SET); 336 | fwrite(&patch_buf[0], 8, 1, ppc_payload); 337 | fclose(ppc_payload); 338 | } 339 | free(accel.buffer); 340 | return 0; 341 | } 342 | -------------------------------------------------------------------------------- /docker/build_dol.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname ${BASH_SOURCE[0]})" 3 | cp -R ./shared/work/ . 4 | pushd ./work/gc_devo 5 | pushd ./source 6 | patch -p1 < ../../../main.patch 7 | popd 8 | mkdir -p apps/gc_devo 9 | mkdir -p apps/gc_devo_keygen 10 | cp icon.png apps/gc_devo 11 | cp icon.png apps/gc_devo_keygen 12 | cp meta.xml apps/gc_devo 13 | cp meta.xml apps/gc_devo_keygen 14 | sed -i 's/Devolution<\/name>/Devolution Cracked<\/name>/g' apps/gc_devo/meta.xml 15 | sed -i 's/Devolution<\/name>/Devolution Keygen<\/name>/g' apps/gc_devo_keygen/meta.xml 16 | cp ../cracked_loader.bin data/loader.bin 17 | make 18 | cp boot.dol apps/gc_devo/ 19 | cp ../cracked_loader.bin apps/gc_devo/loader.bin 20 | cp ../keygen_loader.bin data/loader.bin 21 | make 22 | cp boot.dol apps/gc_devo_keygen/ 23 | popd 24 | mkdir ./shared/output 25 | cp -R ./work/gc_devo/apps ./shared/output 26 | rm -r ./shared/work/ 27 | -------------------------------------------------------------------------------- /docker/build_qemu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname ${BASH_SOURCE[0]})" 3 | wget -q https://download.qemu.org/qemu-7.0.0.tar.xz 4 | tar -xf qemu-7.0.0.tar.xz 5 | rm qemu-7.0.0.tar.xz 6 | pushd qemu-7.0.0 7 | patch -p1 < ../add_broadway.patch 8 | ./configure --target-list=ppc-softmmu 9 | make -j$(nproc) 10 | make install 11 | -------------------------------------------------------------------------------- /docker/extract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname ${BASH_SOURCE[0]})" 3 | sha256sum ./shared/gc_devo_src.zip | grep "af8c4c0aa62adb1559c7809180f0d1624fb0094b00c4248b9a9c427555d10971" 4 | if [ $? -ne 0 ]; then 5 | echo "Bad gc_devo_src.zip checksum, r266 only" 6 | exit 1 7 | fi 8 | 9 | unzip ./shared/gc_devo_src.zip 10 | 11 | tail -c +353 ./gc_devo/data/loader.bin | head -c -8 > ./payload.bin 12 | qemu-system-ppc -display none -S -gdb tcp::20000,ipv4 -M broadway & 13 | QEMU_PID=$! 14 | sleep 5 15 | gdb-multiarch -x ./scripts/devo_go.gdb 16 | kill $QEMU_PID 17 | 18 | # Decrypt the DSP firmware cuz why not 19 | tail -c +87457 devo_dump_0x80021470.bin | head -c 3072 > enc_dsp_payload.bin 20 | python3 ./scripts/decrypt_dsp_payload.py 21 | 22 | # Build the cracked loader.bin 23 | cp ./patches/preloader.bin ./cracked_loader.bin 24 | # Copy the name, rev, and date 25 | dd if=./devo_dump_0x80021470.bin skip=$((0x3394)) bs=1 count=$((0x48)) of=./cracked_loader.bin conv=notrunc seek=4 26 | # Copy a donor BCA needed 27 | dd if=./patches/donor.bca of=./cracked_loader.bin conv=notrunc seek=$((0x240)) bs=1 28 | 29 | # Apply the no-disc patches 30 | xdelta3 -d -s devo_dump_0x80021470.bin patches/devo_patch.xdelta3 devo_dump_cracked.bin 31 | dd if=devo_dump_cracked.bin of=cracked_loader.bin conv=notrunc seek=$((0x280)) bs=1 32 | 33 | # Apply the keygen-only patch for Wii U 34 | cp cracked_loader.bin keygen_loader.bin 35 | xdelta3 -d -s devo_dump_cracked.bin patches/keygen_only_patch.xdelta3 devo_dump_keygen.bin 36 | dd if=devo_dump_keygen.bin of=keygen_loader.bin conv=notrunc seek=$((0x280)) bs=1 37 | 38 | # Ship it 39 | mkdir -p ./shared/work 40 | cp -R ./gc_devo ./shared/work 41 | cp cracked_loader.bin ./shared/work 42 | cp keygen_loader.bin ./shared/work 43 | 44 | mkdir -p ./shared/for_devs 45 | cp devo_dump_0x80021470.bin ./shared/for_devs 46 | cp dec_dsp_payload.bin ./shared/for_devs 47 | -------------------------------------------------------------------------------- /docs/Defeating Devolution.md: -------------------------------------------------------------------------------- 1 | # Defeating Devolution 2 | ### Unpacking loader.bin 3 | Devolution features many layers of payload obfuscation, anti-tamper, and funny math to prevent the user from easily defeating its DRM mechanisms. As it turns out, someone else had already unpacked Devolution's first stage loader.bin to obtain the main PowerPC payload back in 2018: https://oct0xor.github.io/2018/05/06/unpacking_devolution/ 4 | I cannot emphasize enough how helpful this writeup was, and if nothing else it sped things up by a few weeks. 5 | I first adapted their QEMU Broadway patch and IDA instrumentation to QEMU 7.0 and standalone GDB scripts respectively to simiplify things. 6 | They evidently worked with Devolution r200, so I also adapted the tooling to the last public release of Devolution, r266. 7 | As addresses had shifted around, it was not 100% clear which CRC function call I was supposed to dump, so I just dumped all of the ones it could be to different files, and found the correct one from there. 8 | My GDB adaptation of the IDA script can be found [here](https://github.com/xperia64/unpacking_devolution/blob/master/scripts/devo_unpack_gdb.py). 9 | ### Analyzing the Main Payload 10 | ##### Running the Unpacked Payload 11 | Ok, so we got what seems to be the main PowerPC payload, now what? 12 | The initial loader.bin is supposed to copy this payload into MEM2 @ `0x93200000`, so let's try modifying Devolution's sample loader to place the decrypted payload there and boot directly into it. 13 | Just a simple memcpy and flush, and everything still seems to work when we branch to `0x93200000`. Great! 14 | Let's try messing with the version string just for fun, maybe changing "Devolution " to "Evolution ". 15 | Uh oh. It doesn't boot anymore. I knew this was going way too smoothly. 16 | We've learned two things: 17 | 1. The initial loader.bin is *not* needed for Devolution to work and only exists for compression and obfuscation of the main payload, so we can ignore it 18 | 2. The main payload has some sort of anti-tamper, because of course it does 19 | 20 | ##### Finding `main` 21 | Time for some good old-fashioned static analysis. 22 | With the [Ghidra GameCube Loader](https://github.com/Cuyler36/Ghidra-GameCube-Loader), we can load our raw binary payload at `0x93200000` and get to work. 23 | The first thing we see is a short sequence of instructions that end with an `rfi` instruction. 24 | Between this and the initial loader.bin, it seems the author loves using `rfi` for branching. 25 | We can see this `rfi` branches to `0x13200040` (the physical-address-space equivalent of `0x93200040`), so lets mark this instruction as a `CALL_RETURN` in Ghidra to make things look nice, and resume analysis there. 26 | Devolution proceeds to reset/configure a bunch of registers. 27 | Ghidra is apparently not aware of all of [Broadway's SPRs](https://fail0verflow.com/media/files/ppc_750cl.pdf#G6.1103629), so we add comments to the SPRs that don't make sense, denoting the actual register name and what the configuration does. 28 | Most of the register setting is uninteresting, but we make note of the IBAT/DBATs as they are reconfigured here (remember we are running from physical address space). 29 | We can see that MEM1 is now available at `0x80000000`, `0xa0000000` and `0xc0000000` instead of just its usual spot at `0x80000000`, and that MEM2 is available at `0xb0000000` and `0xd0000000` instead of `0x90000000`. 30 | There's some caching and length details that vary among the assignments, and some slightly different IBAT mappings (for example, there's a 128k I-mapping from `0x10000000` to `0xb0000000` and a 1MB I-mapping from `0x13200000 to 0xb3200000`, but the rest of MEM2 is excluded from I-mappings). 31 | Finally, we jump into a very large function at `0xb3200260` which we can think of as `main`. 32 | With the BAT mappings, we load another copy of our payload at `0xb3200000` in Ghidra as there's no real clean way to do mirrors. 33 | The payload at `0xb3200000` will be our main working copy from now on. 34 | 35 | ##### Funny Strings 36 | Scrolling through this function, we see many variable initializations and many function calls. 37 | One function that catches our eye first is one that takes the Devolution version string as its second argument, with a pointer to some strange bytes as the first argument. 38 | This function calls a second function with some more arguments, and in the second function, we see what appears to be a `printf`-style format string parser. 39 | This seems useful, but what are the other arguments, and what are those strange bytes? 40 | The other arguments appear to be function pointers, one for outputting a character from the `printf`-like function (`putc`?), and the other takes in the strange bytes and returns a normal `char` that gets parsed in the format string parser. 41 | Calling this function `obfuscated_printf_fmt_getter`, we see that it dereferences a byte from the strange string, XORs it with it's address, and then XORs that with the value of some array that's indexed by the previous char in the string and the value of an SPR? 42 | Seems a bit execessive, but let's go back to where that array is initialized. 43 | This array is near the end of the PPC payload, and is filled with zeros. 44 | According to Ghidra, the array is only reinitialized if some conditional bit is set that hasn't been initialized, so let's assume this array is filled with zeros. 45 | At `0xb320099b`, we locate the byte sequence used by this printf function call, `BE EF 97 9E`. Let's XOR each letter with its address and see what we get. 46 | ``` 47 | 0xBE ^ 0x9B = 0x25, '%' 48 | 0xEF ^ 0x9C = 0x73, 's' 49 | 0x97 ^ 0x9D = 0x0a, '\n' 50 | 0x9E ^ 0x9E = 0x00, '\0' 51 | ``` 52 | As you can see, we end up with a nice null-terminated "%s\n". 53 | By finding the uses of this obfuscated printf, we can locate other obfuscated format strings. 54 | Because decoding these strings by hand is tedious, I've written a [Ghidra python script](https://github.com/xperia64/unpacking_devolution/blob/master/scripts/decode_ghidra.py) which can be pasted into the python console. Just run `deobf()` in the console when you've selected what you think is an obfuscated string in the Listing view. 55 | ##### Naming Things 56 | Now that we have the log strings, we can go through each function that calls our obfuscated printf, and try to name it based on what strings it uses. 57 | Doing this, we locate many important functions: 58 | * An `abort_handler` at `0xb320210c` 59 | * What's likely an IOS `ipc_response_handler` at `0xb3207ee4` 60 | * An `exception_handler` at `0xb3202450` 61 | * USB Gecko initialization at `0xb32034cc` 62 | * SD card initialization at `0xb320f5b8` 63 | * USB storage initialization at `0xb321dd90` 64 | * And finally, a very interesting string: `Unable to verify ISO image - .DVV file error`, used in a function that also calls the SD/USB initialization functions; let's call this `important_boot_function` 65 | 66 | ##### Storage and Loading the DRM Validation Files 67 | Going through `important_boot_function`, we discover code that tries to locate the FAT partition on the storage device, and a custom FAT driver (which only supports 8.3 short names). 68 | We can see boring things like memcard emulation, and then the more interesting part where it tries to load loads the DVV DRM file for the game you've selected. 69 | The code here actually cares about the DVV file being "read-only" and "hidden" (I believe it cares about the archive bit being unset too, but I forgot). 70 | If the file doesn't exist, it attempts to allocate a placeholder, and if any of these steps fail, this is where we get the "DVV file error" message. 71 | Next, Devolution calls an interesting function that takes in part of the DVV buffer, which we soon identify as an IOS_Ioctlv implementation. 72 | This particular implementation again has a custom format string parser, but the use of IOS_Ioctlv is strange as it passes a hardcoded file descriptor of 0x10000. 73 | Thankfully, [someone helpfully documented this value and its usage](https://wiibrew.org/wiki//dev/aes) around two weeks before I started this project. Talk about good timing. 74 | The first 32 bytes of the DVV file are very clearly the first 32 bytes of your GameCube disc, and with this Ioctlv call, we can see that the remaining 224 bytes are encrypted with a fixed 16-byte key located at `0xb3215560`, while the IV is the first 12 bytes of the DVV concatenated with the last 4 bytes of the plaintext part (those magic 4 bytes all GameCube discs have). 75 | I've included a script that dumps the encrypted portion of a DVV [here](https://github.com/xperia64/unpacking_devolution/blob/master/scripts/decrypt_dvv.py) (need to replace the encryption key), and the Wii evidently uses standard AES CBC. 76 | The decrypted DVV section doesn't mean much as there's more high-entropy data after another 32-byte header-like structure. 77 | The second 16-byte struct of the decrypted section seem to be related to the Wiimote validation, with 4 bytes per Wii remote. The values are consistent, but change in order depending on which Wii remote connects first. 78 | The full 256-byte decrypted DVV is then passed to another function which makes absolutely no sense for now, so let's ignore it and move on to naming other functions. 79 | 80 | ##### Other Functions 81 | Going through and naming more functions by strings or memory address, we find: 82 | * A config struct validator/initializer 83 | * Video register configuration 84 | * A reboot function 85 | * HID initialization 86 | * Bluetooth initialization 87 | * lwIP web server initialization 88 | * Code which runs the game's apploader and hooks up osreport to Devolution's USB Gecko/WiFi logging 89 | * A function which blinks the disc slot LED N times 90 | * A function that encrypts the DVV buffer before writing it to disc 91 | 92 | So far, some things are suspiciously absent: 93 | * No sign of that anti-tamper which we know exists 94 | * No code which accesses the Disc Interface 95 | 96 | However, there are a few functions which perform reads and writes to addresses specified as offsets to base register `r2`. 97 | Doing a bruteforce Listing search in Ghidra, we discover that `r2` is set in only a couple places: 98 | * Near the start of `main`, `r2 = 0xcc010000` 99 | * In the function that initializes the USB Gecko, `r2 = r2 - 0x8000` 100 | 101 | This results in a final `r2` of `0xcc008000` for the remainder of the part we care about. 102 | Unfortunately, Ghidra is not smart enough to assign references to the Wii hardware registers accessed via `r2`, so instead we must go through our Listing search and look for interesting uses of `r2`. 103 | 104 | ##### Overengineered Disc Validation 105 | This brings us to a function at `0xb3209a10` which does some math on `r2` and throws that into `r13` which is used as a final base register value of `0xcd808000`. 106 | This function uses negative offsets from `r13`, which ultimately shows us that it's poking a mirror of the disc interface at `0xcd806000`. 107 | This function performs many many tests on the disc drive to make sure it's not an ODE, that you're using original discs and not burnt media, that the game ID matches, etc. 108 | This process is similar in concept to these checks in the now-public [Riivolution source](https://github.com/AerialX/rawksd/blob/4ef79e2e234d7c2f19d030c2d9440852b5c73b7a/launcher/source/launcher.cpp#L219) 109 | This seems like a great place to attack if it wasn't for that pesky anti-tamper. 110 | 111 | ##### LED-as-a-Debugger (LaaD) 112 | After all this, the PowerPC anti-tamper is still nowhere to be found. 113 | We've named pretty much everything we could, and nothing. 114 | I don't have a USB Gecko, so my options for debugging the PowerPC payload on real hardware are very limited, but Devolution loves to blink the LED, so I thought, why not use that to see where the anti-tamper kicks in and halts execution? 115 | By writing a snippet of PowerPC assembly that turns the LED on, then runs a do-nothing loop forever, we can perform a binary search on the boot process and narrow down where execution gets stuck. After many iterations pasting our PowerPC snippet in various places, we narrow down the problem to the first function after initializing the USB Gecko at `0xb3203a28`. We skipped this function in our first static analysis pass as the control flow made no sense. 116 | Additionally at this time, I also determined whether Devolution cares about the whole PowerPC payload or only part of it. It seems that only the first `0x30974` bytes are validated. 117 | 118 | ##### Oops, I Accidentally Flipped the Endianness of my CPU 119 | As it turns out, the first thing this function does is twiddle with the PowerPC MSRs to enable PowerPC little-endian mode. 120 | That still doesn't explain the funky control flow.... [Oh.](http://web.archive.org/web/20220921155414/https://www.nxp.com/files-static/product/doc/MPCFPE32B.pdf#G9.105725). 121 | PowerPC little-endian mode is incredibly misleading and only messes with how data addresses are interpreted rather than the data itself. Evidently this also applies to instruction fetches, so instructions that would normally run `0, 1, 2, 3` instead run `1, 0, 3, 2` in little-endian mode. 122 | I've written a Ghidra script to flip 4-byte words around to untangle this function [here](https://github.com/xperia64/unpacking_devolution/blob/master/scripts/munge_ghidra.py) (note that you must clear the code first before running it). 123 | 124 | ##### DSP Anti-Tamper 125 | Now that we have this function untangled, let's see what it's doing. 126 | First it runs some loop doing some funny math on a blob from `0xb32155a0`, and places its output at `0xc1000000`. 127 | Next, it twiddles a bunch of DSP registers. 128 | Referencing a game with debug symbols, we can see that this function is bootloading the DSP, and the blob we did the math on is DSP firmware. 129 | A script to decrypt the 3072-byte blob from `0xb32155a0` is [here](https://github.com/xperia64/unpacking_devolution/blob/master/scripts/decrypt_dsp_payload.py). 130 | There's a lot of back and forth between the PowerPC and the DSP at this stage, some of it standard, some of it strange. 131 | By using [this wonderful Ghidra plugin](https://github.com/Pokechu22/ghidra-gcdsp-lang), we can attempt to analyze what the DSP firmware is doing. 132 | The first set of DSP control register writes automagically DMA's 1024 bytes of the decrypted payload into DSP IRAM, and it then evidently boots into IRAM `0x0000`. 133 | In Devolution's DSP payload, this sets up a few registers, and then sends mail with the value of `0x0000 0080`. 134 | The PPC payload then reads this value back and does more DSP control register twiddling, which appears to cause the DSP to boot from IROM. 135 | This is part of the normal DSP bootloading process, however, Devolution specifies IRAM and DRAM payloads of length 0, and uses `0x0080` from the mailbox as the DSP boot address. 136 | We now assume that the DSP continues execution from IRAM at `0x0080`, and by observing the sequence of mailbox accesses, this seems correct. 137 | The DSP payload stores a constant value of `0x1320 0000` somewhere, so it seems this could be our anti-tamper. Why else would the DSP be referencing the start of the PowerPC payload? 138 | 139 | Next, the DSP jumps into the middle of an IROM function once, then jumps further down into the same function multiple times in a loop. 140 | By going through the IROM, we locate a total of 4 functions that all seem to do weird math with slight variation, at IROM `0x854b`, `0x8644`, `0x8726`, and `0x880c`. What the heck are these? 141 | After quite a bit of searching, I located [this comment](https://github.com/dolphin-emu/dolphin/pull/5617#issuecomment-309256012) on a Dolphin PR which indicates that this function is related to memory card unlocking. 142 | For convenience, I've named these functions CARD1 through CARD4, and that Devolution's DSP payload jumps into the middle of CARD4. 143 | This code sets up some random-looking constants, and then configures the DSP's accelerator, setting the current address to 0x13200000 * 2 = 0x26400000. This part of CARD4 loops some number of times. 144 | The number of times to loop is sent by the PowerPC payload to the DSP's mailbox, and I determined that that value is `0xc25d`. 145 | The DSP payload then multiplies that value by 4, and would you look at that, we get `0x30974`. 146 | The first call to the middle of the function loops `0x974` times, and the loop that calls the later part of the function runs `0x30000/0x10000` times, with the loop in the called function running `0x10000` times. 147 | By examining the called function's loop, we can see that it reads the accelerator twice. 148 | Further DSP hardware tests determined that the Accelerator format used is 4-bit, so reading one byte requires 2 reads, and that's also why the accelerator address was multiplied by 2. 149 | 150 | Ultimately, this function as used by Devolution does a proprietary obfuscated hash of the PowerPC binary, and this gets mailed to the PowerPC. 151 | The DSP firmware requires that the lower 16-bits of the sum of the hash and the length of the payload equal `0xffff`, and DSP DMA of the rest of the DSP payload fails when the lower 16 bits are any other value. 152 | The PowerPC payload goes a step further and requires that the hash plus payload length XOR'd with `0x735f0000` equals `0x7FFFFFFF`. In other words, the final hash must equal `0x0c9df68b` 153 | Fudging the payload to only satisfy the lower 16 bits and skipping the check for the upper 16 bits on the PowerPC side does not work, so the upper bits evidently matter somewhere else too. 154 | Unless I'm missing some simplified math, it would seem that bruteforcing the checksum to be this specific value was simply part of the build process for devolution. 155 | There are 4 bytes at the end of the checksummmed area which I assumed are used to force the hash to be a specific value. There's another 4 bytes before it which are 0xFFFFFFFF, which I'm assuming are used if the required checksum cannot be achieved in 4 bytes. 156 | After a lot of tedious static and dynamic analysis of the DSP ROM functions using [DSPSpy](https://github.com/dolphin-emu/dolphin/tree/master/Source/DSPSpy), I was able to write a C implementation of the hash function, and wrote a multi-threaded bruteforcer to fix the checksum on a tampered payload, which can be seen [here](https://github.com/xperia64/unpacking_devolution/blob/master/checksum.c). 157 | 158 | ##### More DSP Shenanigans 159 | After the anti-tamper, it became clear that the DSP was being used as a flat-out security coprocessor. 160 | I went back and located all of the PowerPC functions which ran in little-endian mode, and all of them send information to the DSP using mailed commands: 161 | * Command 0 sends the DVV buffer over to the DSP 162 | * Command 1 is used right before Devolution hands over execution to the actual game, maybe some "request boot" command 163 | * Command 2 sends "short" security metadata 164 | * Command 3 sends disc-related metadata during disc validation 165 | * Command 4 sends the time-base register of the PowerPC (this is done immediately after a successful DSP boot) 166 | * Command 5 sends "long" security metadata 167 | * Commands 6-9 appear to receive Wiimote metadata for Wiimotes 1-4 168 | 169 | Command 4 is fun because the time-base register is used to scramble all of the other DRM info (the time gets stored in the DVV so it can decode the DRM info later). 170 | My first real attempt at cracking devolution at this point was chasing a lot of the security metadata down and attempting to zero all of that out. 171 | Devolution uses pretty much every unique identifier a Wii has as part of its DRM, including your NG_ID, NAND encryption key, ECID (thanks Extrems), processor version, Bluetooth MAC address, and probably some other things I missed, as zeroing out this information did not result in transferable DVVs. 172 | Additionally, the ECID reads are scattered throughout other unrelated functions, and the reads themselves are also obfuscated. 173 | Rather than reading the ECID SPRs directly, Devolution intentionally reads invalid SPRs (which always throws an exception on Broadway), and then fixes up the reads to the real ECID SPRs in its exception handler. Lovely. 174 | And of course, the DSP code that receives all this data is even more painful to read than the initial bootload and anti-tamper. 175 | 176 | ### Cracking Devolution 177 | Ultimately I stopped trying to completely understand how the DRM worked and took a different approach. 178 | The disc drive validation function from before sends two pieces of information about the disc to the DSP, specifically the first 32 bytes of the disc header, and the BCA. 179 | As it turns out, Devolution doesn't care what the BCA data *is* as long as it meets certain conditions. 180 | From Dolphin developer pokechu22's analysis of this part: 181 | ``` 182 | pokechu22 OK, I looked into the BCA check, and I think I understand it: I assume that JMPR $ar2 (=> 0202) is the "you've messed up" path for the most part. 183 | pokechu22 015b-015f reads the accelerator 0x1a times or 0x34 bytes assuming a 16-bit format. It ors each read value together, and then tests if the result is zero, i.e. all bytes are zero, i.e. the BCA starts with 0x34 zeros. 184 | pokechu22 0161-017a checks if the next value (0x34/0x35) is one of 0x474b = GK, 0x4b47 = KG, 0x4d41 = MA, 0x4d42 = MB, 0x4d44 = MD, 0x4d55 = MU, 0x5044 = PD, 0x504D = PM, or 0x7064 = pd (converting pd to PD if found). 185 | pokechu22 017c-0191 checks if the next value (0x36/0x37) is one of 0x4b44 = KD, 0x4b47 = KG, 0x4d41 = MA, 0x4d43 = MC, 0x5343 = SC, 0x534b = SK, 0x554b = UK, or 0x5544 = UT. 186 | pokechu22 0192-0195 checks if the next value (0x38/0x39) has the top nybbles cleared (i.e. masking with 0xf0f0 results in zero, i.e. the number is of the form 0X0X). 187 | pokechu22 0196-0197 skips one value (0x3a/0x3b) and loads the next. 188 | pokechu22 0198-019a checks that that value (0x3c/0x3d) and 0xfc is zero (i.e. offset 0x3d is 0, 1, 2, or 3). 0x3e/0x3f never seem to be read. 189 | pokechu22 It then writes some stuff into the accelerator (which would clobber the 0x3e/0x3f values?), which only happens if these tests pass, and then jumps to 0202. 190 | pokechu22 If I'm understanding things correctly, a BCA composed of 0x34 zeros followed by 'PDMC' (50 44 4d 43) followed by 8 more zeros would pass. 191 | ``` 192 | 193 | And indeed, Devolution accepts a blank BCA except for `PDMC`. 194 | Since we already have the disc header in memory (as Devolution requires us to pass when we boot it from our launcher), I picked a completely random address in MEM1 (`0xa1466d80`) and placed a buffer with the disc header and our BCA in the modified launcher, and told the disc validation function to pass that to the DSP instead. 195 | After verifying this worked, I wiped all the code in this disc validation function except for the part where it sends our buffer, and it still worked. 196 | For whatever reason placing the buffer into MEM2 *does not* seem to work, but whatever. 197 | Ultimately I probably could've generated a buffer with these contents in the disc validation function, but I chose to instead write a preloader stub and do it there as I needed to anyway so my payload would behave the same as the vanilla loader.bin for use in other launchers like USB Loader GX. 198 | I was not done yet as it turns out there was a little more disc information that was sent over to the DSP after the initial basic metadata checks. 199 | Devolution appears to read various sectors of the disc into a buffer, and the DSP hashes them. 200 | *However*, depending on whether the DVV looks good on the DSP side, Devolution will read these sectors from the emulated disc instead, because it obviously has to if you're playing on a Family Wii or Wii U. 201 | And as it turns out, this is actually done as a nice bit of abstraction with a function pointer table (if DVV good, read from USB/SD storage; else try to read from the real disc). 202 | By forcing this function to always read from USB/SD storage, Devolution passes its own validation. 203 | I quickly wrote a small [PowerPC assembly memcpy program](https://github.com/xperia64/unpacking_devolution/blob/master/patches/preloader.bin) to copy Devolution's payload to 0x93200000 just like the real loader.bin, and also copy the game disc header+dummy BCA to `0x81466d80`, and just like that, I have a drop-in compatible loader.bin without the pesky disc check. 204 | 205 | All it took for Devolution's DRM to fall were two minimally-invasive patches and a checksum fix. The rest of the crazy DSP DRM math and other protections were for naught. 206 | What this means is that Family Wii's and Wii U's can use Devolution without a 1st party Wii remote or disc validation on a backwards compatible Wii. Honestly I was surprised there weren't additional checks to break generating DVVs for these console IDs specifically. 207 | These patches work well on at least backwards-compatible Wii's, but I encountered some issues where games would not boot the first time you ran/validated them on the Wii U (and indeed, I also noticed some instability in games on the Wii when booting them for the first time), so I wrote a 3rd patch which produces a variant of Devolution which generates the DVV file, and exits immediately. 208 | Additionally, the generated DVVs are 100% compatible with an unmodified stock Devolution, so should any other issues appear with my patched version, the user can choose to use their original copy of Devolution r266. 209 | 210 | ### Ship It 211 | To make these patches relatively accessible while not infringing on the original author's copyright, I have put together Docker containers with all the tools and patches, and written appropriate *nix/Windows scripts to automagically decrypt, patch, and rebuild the binary. 212 | Just grab [gc_devo_src.zip](https://web.archive.org/web/20191026015213/http://www.tueidj.net/gc_devo_src.zip), put it in the appropriate folder, and assuming Docker is set up properly, run `run_docker.sh` or `run_docker.bat` 213 | Is Docker overkill? Absolutely. But the build process is very messy and I don't trust QEMU and gdb to run properly on Windows. 214 | Should I have made prebuilt images and pushed them to DockerHub? Probably but I'm too lazy to make an account. If anyone wants to fork this and provide prebuilt images they're free to do so. 215 | 216 | Huge thanks to the Dolphin IRC for being very helpful throughout this and putting up with my questions (I'll try to get around to that DSP PR I swear), and specifically pokechu22 and Extrems. 217 | -------------------------------------------------------------------------------- /docs/devo_patches.txt: -------------------------------------------------------------------------------- 1 | For Wii: 2 | disc validation patch @ 0x9f34: 3F 40 01 46 63 5A 6D 80 67 5B A0 00 3C 60 00 00 7F 64 DB 78 38 A0 00 03 42 80 9A 81 38 60 00 03 42 80 04 1C 3 | disc access table patch @ 0xb8c4: B3 20 A7 74 4 | For Wii U (keygen variant): 5 | exit to HBC instead of booting the game @ 0x1546C: 60 00 00 00 60 00 00 00 60 00 00 00 60 00 00 00 60 00 00 00 60 00 00 00 60 00 00 00 60 00 00 00 60 00 00 00 6 | -------------------------------------------------------------------------------- /extract_standalone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname ${BASH_SOURCE[0]})" 3 | sha256sum "$1" | grep "af8c4c0aa62adb1559c7809180f0d1624fb0094b00c4248b9a9c427555d10971" 4 | if [ $? -ne 0 ]; then 5 | echo "Bad gc_devo_src.zip checksum, r266 only" 6 | exit 1 7 | fi 8 | 9 | unzip "$1" 10 | 11 | mkdir work 12 | pushd work 13 | wget https://download.qemu.org/qemu-7.0.0.tar.xz 14 | tar -xvf qemu-7.0.0.tar.xz 15 | rm qemu-7.0.0.tar.xz 16 | pushd qemu-7.0.0 17 | patch -p1 < ../../qemu/add_broadway.patch 18 | ./configure --target-list=ppc-softmmu 19 | make -j$(nproc) 20 | pushd build 21 | tail -c +353 ../../../loader.bin | head -c -8 > payload.bin 22 | ./qemu-system-ppc -display none -S -gdb tcp::20000,ipv4 -M broadway & 23 | QEMU_PID=$! 24 | sleep 1 25 | gdb-multiarch -x ../../../scripts/devo_go_standalone.gdb 26 | kill $QEMU_PID 27 | cp devo_dump_0x80021470.bin ../../../ 28 | popd 29 | popd 30 | popd 31 | rm -rf work 32 | 33 | # Decrypt the DSP firmware cuz why not 34 | tail -c +87457 devo_dump_0x80021470.bin | head -c 3072 > enc_dsp_payload.bin 35 | python3 scripts/decrypt_dsp_payload.py 36 | 37 | # Build the cracked loader.bin 38 | cp patches/preloader.bin cracked_loader.bin 39 | # Copy the name, rev, and date 40 | dd if=devo_dump_0x80021470.bin skip=$((0x3394)) bs=1 count=$((0x48)) of=cracked_loader.bin conv=notrunc seek=4 41 | # Copy a donor BCA needed 42 | dd if=patches/donor.bca of=cracked_loader.bin conv=notrunc seek=$((0x240)) bs=1 43 | 44 | # Apply the no-disc patches 45 | xdelta3 -d -s devo_dump_0x80021470.bin patches/devo_patch.xdelta3 devo_dump_cracked.bin 46 | dd if=devo_dump_cracked.bin of=cracked_loader.bin conv=notrunc seek=$((0x280)) bs=1 47 | 48 | # Apply the keygen-only patch for Wii U 49 | xdelta3 -d -s devo_dump_cracked.bin patches/keygen_only_patch.xdelta3 devo_dump_keygen.bin 50 | dd if=devo_dump_keygen.bin of=keygen_loader.bin conv=notrunc seek=$((0x280)) bs=1 51 | 52 | -------------------------------------------------------------------------------- /patches/devo_patch.xdelta3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xperia64/unpacking_devolution/132d53de8e270164c7c0fe848e5b3aa26ff2672c/patches/devo_patch.xdelta3 -------------------------------------------------------------------------------- /patches/donor.bca: -------------------------------------------------------------------------------- 1 | PDMC -------------------------------------------------------------------------------- /patches/keygen_only_patch.xdelta3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xperia64/unpacking_devolution/132d53de8e270164c7c0fe848e5b3aa26ff2672c/patches/keygen_only_patch.xdelta3 -------------------------------------------------------------------------------- /patches/main.patch: -------------------------------------------------------------------------------- 1 | diff -Naur a/main.c b/main.c 2 | --- a/main.c 2015-08-16 23:51:12.000000000 +0000 3 | +++ b/main.c 2022-06-30 02:36:00.453876388 +0000 4 | @@ -105,6 +105,7 @@ 5 | int select=0; 6 | DIR *dir; 7 | struct dirent *entry; 8 | + char dir_path[256]; 9 | char full_path[256]; 10 | int data_fd; 11 | u8 *lowmem = (u8*)0x80000000; 12 | @@ -120,12 +121,25 @@ 13 | // count how many .iso files exist 14 | while ((entry = readdir(dir))) 15 | { 16 | - size_t length = strlen(entry->d_name); 17 | - if (length > 4 && stricmp(entry->d_name+length-4, ".ISO")==0) 18 | + if (entry->d_type == DT_REG) 19 | { 20 | - if (!count) 21 | - strcpy(full_path, entry->d_name); 22 | - count++; 23 | + size_t length = strlen(entry->d_name); 24 | + if (length > 4 && strcasecmp(entry->d_name+length-4, ".ISO")==0) 25 | + { 26 | + if (!count) 27 | + strcpy(full_path, entry->d_name); 28 | + count++; 29 | + } 30 | + } 31 | + else if (entry->d_type == DT_DIR) 32 | + { 33 | + snprintf(dir_path, sizeof(dir_path), "disk:/%s/%s/game.iso", dirname, entry->d_name); 34 | + if (!stat(dir_path, &st)) 35 | + { 36 | + if (!count) 37 | + snprintf(full_path, sizeof(full_path), "%s/game.iso", entry->d_name); 38 | + count++; 39 | + } 40 | } 41 | } 42 | 43 | @@ -167,11 +181,25 @@ 44 | index = 0; 45 | while ((entry = readdir(dir))) 46 | { 47 | - size_t length = strlen(entry->d_name); 48 | - if (length > 4 && stricmp(entry->d_name+length-4, ".ISO")==0 && select==index++) 49 | - break; 50 | + if (entry->d_type == DT_REG) 51 | + { 52 | + size_t length = strlen(entry->d_name); 53 | + if (length > 4 && strcasecmp(entry->d_name+length-4, ".ISO")==0 && select==index++) 54 | + { 55 | + printf("\rSelect Game: <%s>\x1b[K", entry->d_name); 56 | + break; 57 | + } 58 | + } 59 | + else if (entry->d_type == DT_DIR) 60 | + { 61 | + snprintf(dir_path, sizeof(full_path), "disk:/%s/%s/game.iso", dirname, entry->d_name); 62 | + if (!stat(dir_path, &st) && select==index++) 63 | + { 64 | + printf("\rSelect Game: <%s/game.iso>\x1b[K", entry->d_name); 65 | + break; 66 | + } 67 | + } 68 | } 69 | - printf("\rSelect Game: <%s>\x1b[K", entry->d_name); 70 | } while (!(pressed & INPUT_BUTTON_OK)); 71 | printf("\n"); 72 | 73 | @@ -201,7 +229,14 @@ 74 | if (CONF_GetAspectRatio() == CONF_ASPECT_16_9) 75 | DEVO_CONFIG->options |= CONFIG_CROP_OVERSCAN; 76 | 77 | - sprintf(full_path, "disk:/%s/%s", dirname, entry->d_name); 78 | + if (entry->d_type == DT_REG) 79 | + { 80 | + sprintf(full_path, "disk:/%s/%s", dirname, entry->d_name); 81 | + } 82 | + else 83 | + { 84 | + strcpy(full_path, dir_path); 85 | + } 86 | 87 | // get the FAT starting cluster (and device ID) for the ISO file 88 | stat(full_path, &st); 89 | -------------------------------------------------------------------------------- /patches/preloader.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xperia64/unpacking_devolution/132d53de8e270164c7c0fe848e5b3aa26ff2672c/patches/preloader.bin -------------------------------------------------------------------------------- /put_gc_devo_src_zip_here/put_gc_devo_src_zip_here: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xperia64/unpacking_devolution/132d53de8e270164c7c0fe848e5b3aa26ff2672c/put_gc_devo_src_zip_here/put_gc_devo_src_zip_here -------------------------------------------------------------------------------- /qemu/add_broadway.patch: -------------------------------------------------------------------------------- 1 | diff --git a/configs/devices/ppc-softmmu/default.mak b/configs/devices/ppc-softmmu/default.mak 2 | index 658a45442..ffa540e20 100644 3 | --- a/configs/devices/ppc-softmmu/default.mak 4 | +++ b/configs/devices/ppc-softmmu/default.mak 5 | @@ -5,6 +5,7 @@ CONFIG_E500=y 6 | CONFIG_PPC405=y 7 | CONFIG_PPC440=y 8 | CONFIG_VIRTEX=y 9 | +CONFIG_BROADWAY=y 10 | 11 | # For Sam460ex 12 | CONFIG_SAM460EX=y 13 | diff --git a/hw/ppc/Kconfig b/hw/ppc/Kconfig 14 | index 400511c6b..99a6b27a4 100644 15 | --- a/hw/ppc/Kconfig 16 | +++ b/hw/ppc/Kconfig 17 | @@ -142,6 +142,11 @@ config VIRTEX 18 | select XILINX_ETHLITE 19 | select FDT_PPC 20 | 21 | +config BROADWAY 22 | + bool 23 | + select PPC4XX 24 | + select SERIAL 25 | + 26 | # Only used by 64-bit targets 27 | config FW_CFG_PPC 28 | bool 29 | diff --git a/hw/ppc/broadway.c b/hw/ppc/broadway.c 30 | new file mode 100644 31 | index 000000000..c1b47f4f9 32 | --- /dev/null 33 | +++ b/hw/ppc/broadway.c 34 | @@ -0,0 +1,150 @@ 35 | +#include "qemu/osdep.h" 36 | +#include "cpu.h" 37 | +#include "qapi/error.h" 38 | +#include "qemu-common.h" 39 | +#include "hw/hw.h" 40 | +#include "hw/loader.h" 41 | +#include "hw/boards.h" 42 | +#include "exec/address-spaces.h" 43 | +#include "hw/ppc/ppc.h" 44 | + 45 | +#define MAX_CPUS 1 46 | + 47 | +#define MEM1PH_ADDR 0 // Physical 48 | +#define MEM1PH_SIZE 0x017FFFFF 49 | + 50 | +#define MEM2PH_ADDR 0x10000000 // Physical 51 | +#define MEM2PH_SIZE 0x03FFFFFF 52 | + 53 | +#define MEM1C_ADDR 0x80000000 // Physical Address = 0x00000000 54 | +#define MEM1C_SIZE 0x017FFFFF // (Cached) 55 | + 56 | +#define MEM1C_ALT_ADDR 0xA0000000 57 | +#define MEM1C_ALT_SIZE 0x017FFFFF 58 | + 59 | +#define MEM1U_ADDR 0xC0000000 // Physical Address = 0x00000000 60 | +#define MEM1U_SIZE 0x017FFFFF // (Uncached) 61 | + 62 | +#define MEM2C_ADDR 0x90000000 // Physical Address = 0x10000000 63 | +#define MEM2C_SIZE 0x03FFFFFF // (Cached) 64 | + 65 | +#define MEM2C_ALT_ADDR 0xB0000000 66 | +#define MEM2C_ALT_SIZE 0x03FFFFFF 67 | + 68 | +#define MEM2U_ADDR 0xD0000000 // Physical Address = 0x10000000 69 | +#define MEM2U_SIZE 0x03FFFFFF // (Uncached) 70 | + 71 | +#define REGS_ADDR 0xCD000000 // Physical Address = 0x0D000000 72 | +#define REGS_SIZE 0x00008000 // Hollywood Registers 73 | + 74 | +#define LOCKED_CACHE_ADDR 0xF0000000 75 | +#define LOCKED_CACHE_SIZE 0x8000 76 | + 77 | +//#define LOADADDR 0xb3200000 78 | +//#define FILENAME "devo_load.bin" 79 | + 80 | +#define LOADADDR 0x80004000 81 | +#define FILENAME "payload.bin" 82 | +//#define LOADADDR 0x80040ac0 83 | +//#define FILENAME "loader.bin" 84 | + 85 | +typedef struct 86 | +{ 87 | + PowerPCCPU *cpu; 88 | + MemoryRegion *system_mem; 89 | + MemoryRegion *mem1ph; 90 | + MemoryRegion *mem1c; 91 | + MemoryRegion *mem1ca; 92 | + MemoryRegion *mem1u; 93 | + MemoryRegion *mem2ph; 94 | + MemoryRegion *mem2c; 95 | + MemoryRegion *mem2ca; 96 | + MemoryRegion *mem2u; 97 | + MemoryRegion *regs; 98 | + MemoryRegion *locked_cache; 99 | +} BroadwayState; 100 | + 101 | +static void broadway_load_image(BroadwayState *s, const char* file, uint32_t addr) 102 | +{ 103 | + unsigned int size = get_image_size(file); 104 | + 105 | + if (load_image_targphys(file, addr, size) != size) 106 | + { 107 | + fprintf(stderr, "%s: error loading '%s'\n", __FUNCTION__, file); 108 | + abort(); 109 | + } 110 | +} 111 | + 112 | +static void main_cpu_reset(void *opaque) 113 | +{ 114 | + PowerPCCPU *cpu = opaque; 115 | + 116 | + cpu_reset(CPU(cpu)); 117 | + 118 | + cpu->env.nip = LOADADDR; 119 | +} 120 | + 121 | +static void broadway_init_cpu(MachineState *machine) 122 | +{ 123 | + Error *err = NULL; 124 | + BroadwayState *s = g_new(BroadwayState, 1); 125 | + 126 | + fprintf(stdout, "Get Memory\n"); 127 | + 128 | + s->system_mem = get_system_memory(); 129 | + 130 | + s->mem1ph = g_new(MemoryRegion, 1); 131 | + s->mem1c = g_new(MemoryRegion, 1); 132 | + s->mem1ca = g_new(MemoryRegion, 1); 133 | + s->mem1u = g_new(MemoryRegion, 1); 134 | + s->mem2ph = g_new(MemoryRegion, 1); 135 | + s->mem2c = g_new(MemoryRegion, 1); 136 | + s->mem2ca = g_new(MemoryRegion, 1); 137 | + s->mem2u = g_new(MemoryRegion, 1); 138 | + s->regs = g_new(MemoryRegion, 1); 139 | + s->locked_cache = g_new(MemoryRegion, 1); 140 | + 141 | + memory_region_init_ram(s->mem1c, NULL, "broadway.mem1c", MEM1C_SIZE, &err); 142 | + memory_region_init_alias(s->mem1ca, NULL, "broadway.mem1ca", s->mem1c, 0, MEM1C_ALT_SIZE); 143 | + memory_region_init_alias(s->mem1u, NULL, "broadway.mem1u", s->mem1c, 0, MEM1U_SIZE); 144 | + memory_region_init_alias(s->mem1ph, NULL, "broadway.mem1ph", s->mem1c, 0, MEM1PH_SIZE); 145 | + memory_region_add_subregion(s->system_mem, MEM1C_ADDR, s->mem1c); 146 | + memory_region_add_subregion(s->system_mem, MEM1C_ALT_ADDR, s->mem1ca); 147 | + memory_region_add_subregion(s->system_mem, MEM1U_ADDR, s->mem1u); 148 | + memory_region_add_subregion(s->system_mem, MEM1PH_ADDR, s->mem1ph); 149 | + 150 | + memory_region_init_ram(s->mem2c, NULL, "broadway.mem2c", MEM2C_SIZE, &err); 151 | + memory_region_init_alias(s->mem2ca, NULL, "broadway.mem2ca", s->mem2c, 0, MEM2C_ALT_SIZE); 152 | + memory_region_init_alias(s->mem2u, NULL, "broadway.mem2u", s->mem2c, 0, MEM2U_SIZE); 153 | + memory_region_init_alias(s->mem2ph, NULL, "broadway.mem2ph", s->mem2c, 0, MEM2PH_SIZE); 154 | + memory_region_add_subregion(s->system_mem, MEM2C_ADDR, s->mem2c); 155 | + memory_region_add_subregion(s->system_mem, MEM2C_ALT_ADDR, s->mem2ca); 156 | + memory_region_add_subregion(s->system_mem, MEM2U_ADDR, s->mem2u); 157 | + memory_region_add_subregion(s->system_mem, MEM2PH_ADDR, s->mem2ph); 158 | + 159 | + memory_region_init_ram(s->locked_cache, NULL, "broadway.locked_cache", LOCKED_CACHE_SIZE, &err); 160 | + memory_region_add_subregion(s->system_mem, LOCKED_CACHE_ADDR, s->locked_cache); 161 | + 162 | + fprintf(stdout, "Load Image\n"); 163 | + 164 | + broadway_load_image(s, FILENAME, LOADADDR); 165 | + 166 | + fprintf(stdout, "Create CPU\n"); 167 | + 168 | + s->cpu = POWERPC_CPU(cpu_create(machine->cpu_type)); 169 | + 170 | + cpu_ppc_tb_init(&s->cpu->env, 100UL * 1000UL * 1000UL); 171 | + 172 | + qemu_register_reset(main_cpu_reset, s->cpu); 173 | + 174 | + fprintf(stdout, "Done\n"); 175 | +} 176 | + 177 | +static void broadway_init(MachineClass *mc) 178 | +{ 179 | + mc->desc = "broadway"; 180 | + mc->init = broadway_init_cpu; 181 | + mc->default_cpu_type = POWERPC_CPU_TYPE_NAME("750cl_v2.0"); 182 | +} 183 | + 184 | +DEFINE_MACHINE("broadway", broadway_init) 185 | diff --git a/hw/ppc/meson.build b/hw/ppc/meson.build 186 | index aa4c8e6a2..db982a213 100644 187 | --- a/hw/ppc/meson.build 188 | +++ b/hw/ppc/meson.build 189 | @@ -87,4 +87,6 @@ ppc_ss.add(when: 'CONFIG_PEGASOS2', if_true: files('pegasos2.c')) 190 | ppc_ss.add(when: 'CONFIG_VOF', if_true: files('vof.c')) 191 | ppc_ss.add(when: ['CONFIG_VOF', 'CONFIG_PSERIES'], if_true: files('spapr_vof.c')) 192 | 193 | +ppc_ss.add(when: 'CONFIG_BROADWAY', if_true: files('broadway.c')) 194 | + 195 | hw_arch += {'ppc': ppc_ss} 196 | -------------------------------------------------------------------------------- /qemu/broadway.c: -------------------------------------------------------------------------------- 1 | #include "qemu/osdep.h" 2 | #include "cpu.h" 3 | #include "qapi/error.h" 4 | #include "qemu-common.h" 5 | #include "hw/hw.h" 6 | #include "hw/loader.h" 7 | #include "hw/boards.h" 8 | #include "exec/address-spaces.h" 9 | #include "hw/ppc/ppc.h" 10 | 11 | #define MAX_CPUS 1 12 | 13 | #define MEM1PH_ADDR 0 // Physical 14 | #define MEM1PH_SIZE 0x017FFFFF 15 | 16 | #define MEM2PH_ADDR 0x10000000 // Physical 17 | #define MEM2PH_SIZE 0x03FFFFFF 18 | 19 | #define MEM1C_ADDR 0x80000000 // Physical Address = 0x00000000 20 | #define MEM1C_SIZE 0x017FFFFF // (Cached) 21 | 22 | #define MEM1U_ADDR 0xC0000000 // Physical Address = 0x00000000 23 | #define MEM1U_SIZE 0x017FFFFF // (Uncached) 24 | 25 | #define MEM2C_ADDR 0x90000000 // Physical Address = 0x10000000 26 | #define MEM2C_SIZE 0x03FFFFFF // (Cached) 27 | 28 | #define MEM2U_ADDR 0xD0000000 // Physical Address = 0x10000000 29 | #define MEM2U_SIZE 0x03FFFFFF // (Uncached) 30 | 31 | #define REGS_ADDR 0xCD000000 // Physical Address = 0x0D000000 32 | #define REGS_SIZE 0x00008000 // Hollywood Registers 33 | 34 | #define LOCKED_CACHE_ADDR 0xF0000000 35 | #define LOCKED_CACHE_SIZE 0x8000 36 | 37 | typedef struct 38 | { 39 | PowerPCCPU *cpu; 40 | MemoryRegion *system_mem; 41 | MemoryRegion *mem1ph; 42 | MemoryRegion *mem1c; 43 | MemoryRegion *mem1u; 44 | MemoryRegion *mem2ph; 45 | MemoryRegion *mem2c; 46 | MemoryRegion *mem2u; 47 | MemoryRegion *regs; 48 | MemoryRegion *locked_cache; 49 | } BroadwayState; 50 | 51 | static void broadway_load_image(BroadwayState *s, const char* file, uint32_t addr) 52 | { 53 | unsigned int size = get_image_size(file); 54 | 55 | if (load_image_targphys(file, addr, size) != size) 56 | { 57 | fprintf(stderr, "%s: error loading '%s'\n", __FUNCTION__, file); 58 | abort(); 59 | } 60 | } 61 | 62 | static void main_cpu_reset(void *opaque) 63 | { 64 | PowerPCCPU *cpu = opaque; 65 | 66 | cpu_reset(CPU(cpu)); 67 | 68 | cpu->env.nip = 0x80004000; 69 | } 70 | 71 | static void broadway_init_cpu(MachineState *machine) 72 | { 73 | Error *err = NULL; 74 | BroadwayState *s = g_new(BroadwayState, 1); 75 | 76 | fprintf(stdout, "Get Memory\n"); 77 | 78 | s->system_mem = get_system_memory(); 79 | 80 | s->mem1ph = g_new(MemoryRegion, 1); 81 | s->mem1c = g_new(MemoryRegion, 1); 82 | s->mem1u = g_new(MemoryRegion, 1); 83 | s->mem2ph = g_new(MemoryRegion, 1); 84 | s->mem2c = g_new(MemoryRegion, 1); 85 | s->mem2u = g_new(MemoryRegion, 1); 86 | s->regs = g_new(MemoryRegion, 1); 87 | s->locked_cache = g_new(MemoryRegion, 1); 88 | 89 | memory_region_init_ram(s->mem1c, NULL, "broadway.mem1c", MEM1C_SIZE, &err); 90 | memory_region_init_alias(s->mem1u, NULL, "broadway.mem1u", s->mem1c, 0, MEM1U_SIZE); 91 | memory_region_init_alias(s->mem1ph, NULL, "broadway.mem1ph", s->mem1c, 0, MEM1PH_SIZE); 92 | memory_region_add_subregion(s->system_mem, MEM1C_ADDR, s->mem1c); 93 | memory_region_add_subregion(s->system_mem, MEM1U_ADDR, s->mem1u); 94 | memory_region_add_subregion(s->system_mem, MEM1PH_ADDR, s->mem1ph); 95 | 96 | memory_region_init_ram(s->mem2c, NULL, "broadway.mem2c", MEM2C_SIZE, &err); 97 | memory_region_init_alias(s->mem2u, NULL, "broadway.mem2u", s->mem2c, 0, MEM2U_SIZE); 98 | memory_region_init_alias(s->mem2ph, NULL, "broadway.mem2ph", s->mem2c, 0, MEM2PH_SIZE); 99 | memory_region_add_subregion(s->system_mem, MEM2C_ADDR, s->mem2c); 100 | memory_region_add_subregion(s->system_mem, MEM2U_ADDR, s->mem2u); 101 | memory_region_add_subregion(s->system_mem, MEM2PH_ADDR, s->mem2ph); 102 | 103 | memory_region_init_ram(s->locked_cache, NULL, "broadway.locked_cache", LOCKED_CACHE_SIZE, &err); 104 | memory_region_add_subregion(s->system_mem, LOCKED_CACHE_ADDR, s->locked_cache); 105 | 106 | fprintf(stdout, "Load Image\n"); 107 | 108 | broadway_load_image(s, "payload.bin", 0x80004000); 109 | 110 | fprintf(stdout, "Create CPU\n"); 111 | 112 | s->cpu = POWERPC_CPU(cpu_create(machine->cpu_type)); 113 | 114 | cpu_ppc_tb_init(&s->cpu->env, 100UL * 1000UL * 1000UL); 115 | 116 | qemu_register_reset(main_cpu_reset, s->cpu); 117 | 118 | fprintf(stdout, "Done\n"); 119 | } 120 | 121 | static void broadway_init(MachineClass *mc) 122 | { 123 | mc->desc = "broadway"; 124 | mc->init = broadway_init_cpu; 125 | mc->default_cpu_type = POWERPC_CPU_TYPE_NAME("750cl_v2.0"); 126 | } 127 | 128 | DEFINE_MACHINE("broadway", broadway_init) -------------------------------------------------------------------------------- /run_docker.bat: -------------------------------------------------------------------------------- 1 | set batch_path=%~dp0% 2 | cd "%batch_path%" 3 | set shared_path=%batch_path%put_gc_devo_src_zip_here 4 | set shared_path=%shared_path:\=/% 5 | echo "%shared_path%" 6 | docker build --build-arg USER_ID=1000 --build-arg GROUP_ID=1000 -f Dockerfile.extract -t devo_extract . 7 | docker build --build-arg USER_ID=1000 --build-arg GROUP_ID=1000 -f Dockerfile.build -t devo_build . 8 | docker run --rm -v "%shared_path%":/devo/shared devo_extract ./extract.sh 9 | docker run --rm -v "%shared_path%":/devo/shared devo_build ./build_dol.sh 10 | echo "Complete!" 11 | pause -------------------------------------------------------------------------------- /run_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname ${BASH_SOURCE[0]})" 3 | docker build --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) -f Dockerfile.extract -t devo_extract . 4 | docker build --build-arg USER_ID=$(id -u) --build-arg GROUP_ID=$(id -g) -f Dockerfile.build -t devo_build . 5 | docker run --rm -v $PWD/put_gc_devo_src_zip_here:/devo/shared devo_extract ./extract.sh 6 | docker run --rm -v $PWD/put_gc_devo_src_zip_here:/devo/shared devo_build ./build_dol.sh 7 | -------------------------------------------------------------------------------- /scripts/decode_ghidra.py: -------------------------------------------------------------------------------- 1 | # Paste this into Ghidra's python console at a string 2 | from ghidra.program.model.data import ArrayDataType 3 | from ghidra.program.model.data import CharDataType 4 | 5 | def get_string(addr): 6 | """Get strings one byte at a time""" 7 | output = b'' 8 | localAddr = addr.add(0) 9 | while True: 10 | output += chr((getByte(localAddr) ^ int(str(localAddr), 16)) & 0xFF) 11 | localAddr = localAddr.add(1) 12 | if output[-1] == '\x00': 13 | break 14 | return output 15 | 16 | def deobf(): 17 | real_string = get_string(currentAddress) 18 | listing = currentProgram.getListing() 19 | codeUnit = listing.getCodeUnitAt(currentAddress) 20 | codeUnit.setComment(codeUnit.PRE_COMMENT, '"' + real_string[:-1].replace('\n', '\\n') + '"') 21 | createLabel(currentAddress, "s_%s" % (real_string[:-1].replace('\n','').replace(' ', '')), True) 22 | createData(currentAddress, ArrayDataType(CharDataType.dataType, len(real_string), 1)) 23 | setCurrentLocation(currentAddress.add(len(real_string))) 24 | -------------------------------------------------------------------------------- /scripts/decrypt_dsp_payload.py: -------------------------------------------------------------------------------- 1 | ctr = 0x300 2 | r3 = -4 3 | r5 = 0 4 | out = [None] * 0x300 5 | 6 | with open("enc_dsp_payload.bin", "rb") as f: 7 | b = f.read() 8 | while ctr != 0: 9 | r3 += 4 10 | r0 = b[r3 ^ 0b111] 11 | print(hex(r0)) 12 | r7 = b[(r3 + 1) ^ 0b111] 13 | print(hex(r7)) 14 | r8 = b[(r3 + 2) ^ 0b111] 15 | print(hex(r8)) 16 | r7 = ((r0 << 8) & 0xFF00) + (r7 & 0xFFFF00FF) 17 | print(hex(r7)) 18 | r0 = b[(r3 + 3) ^ 0b111] 19 | print(hex(r0)) 20 | r7 = ((r8 << 24) & 0xFF000000) + (r7 & 0xFFFFFF) 21 | print(hex(r7)) 22 | r7 = ((r7 << 17) | ((r7 >> (32 - 17)) & ~(-1 << 17))) & 0xFFFFFFFF 23 | print(hex(r7)) 24 | r7 = ((r0 << 1) & 0x1FE) + (r7 & 0xFFFFFE01) 25 | print(hex(r7)) 26 | r5 = r5 ^ r7 27 | print(hex(r5)) 28 | out[(0x300 - ctr) ^ 0b1] = r5 29 | ctr -= 1 30 | 31 | with open("dec_dsp_payload.bin", "wb") as f: 32 | for i in out: 33 | f.write(i.to_bytes(4, 'big')) 34 | -------------------------------------------------------------------------------- /scripts/decrypt_dvv.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from Crypto.Cipher import AES 3 | dvv = open(sys.argv[1], "rb") 4 | d = dvv.read() 5 | dvv.close() 6 | iv = d[:12] 7 | iv = iv+d[0x1c:0x20] 8 | encrypted = d[0x20:] 9 | # Put devo key from main PPC payload @ 0xb3215560 here 10 | key = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 11 | cipher = AES.new(key, AES.MODE_CBC, iv=iv) 12 | decrypted = cipher.decrypt(encrypted) 13 | decf = open(sys.argv[2], "wb") 14 | decf.write(decrypted) 15 | decf.close() 16 | -------------------------------------------------------------------------------- /scripts/devo_go.gdb: -------------------------------------------------------------------------------- 1 | set architecture powerpc:750 2 | set endian big 3 | set pagination off 4 | target extended-remote localhost:20000 5 | source ./scripts/devo_unpack_gdb.py 6 | quit 7 | -------------------------------------------------------------------------------- /scripts/devo_go_standalone.gdb: -------------------------------------------------------------------------------- 1 | set architecture powerpc:750 2 | set endian big 3 | set pagination off 4 | target extended-remote localhost:20000 5 | source ../../../scripts/devo_unpack_gdb.py 6 | quit 7 | -------------------------------------------------------------------------------- /scripts/devo_unpack.py: -------------------------------------------------------------------------------- 1 | import idc 2 | import idaapi 3 | 4 | def SetBp(ea): 5 | AddBptEx(ea, 1, BPT_EXEC) 6 | 7 | def Wait(): 8 | GetDebuggerEvent(WFNE_SUSP, -1) 9 | 10 | def Go(): 11 | ResumeProcess() 12 | Wait() 13 | 14 | def SetPC(ea): 15 | SetRegValue(ea, "PC") 16 | 17 | def DMA(dmau, dmal): 18 | 19 | DMA_T = (dmal >> 1) & 1 20 | 21 | if (DMA_T): 22 | 23 | MEM_ADDR = (dmau >> 5) << 5 24 | LC_ADDR = (dmal >> 5) << 5 25 | 26 | MEM_ADDR |= 0x80000000 27 | 28 | DMA_LEN_U = (dmau & 0x1F) << 8 29 | DMA_LEN_L = (dmal >> 2) & 3 30 | 31 | LEN = DMA_LEN_U | DMA_LEN_L 32 | 33 | if (LEN == 0): 34 | LEN = 0x80 35 | 36 | DMA_LD = (dmal >> 4) & 1 37 | 38 | print "DMA: mem = 0x%X, cache = 0x%X, len = 0x%X, LD = %d\n" % (MEM_ADDR, LC_ADDR, LEN, DMA_LD) 39 | 40 | if (DMA_LD): 41 | 42 | buf = idaapi.dbg_read_memory(MEM_ADDR, LEN) 43 | 44 | for i in range(len(buf)): 45 | idaapi.dbg_write_memory(LC_ADDR+i, buf[i]) 46 | else: 47 | 48 | buf = idaapi.dbg_read_memory(LC_ADDR, LEN) 49 | 50 | for i in range(len(buf)): 51 | idaapi.dbg_write_memory(MEM_ADDR+i, buf[i]) 52 | 53 | SetBp(0x80004198) 54 | Go() 55 | 56 | SetPC(0x800041D8) 57 | 58 | DMAU = 0x8000A4A4 59 | SetBp(DMAU) 60 | DMAL = 0x8000A4C0 61 | SetBp(DMAL) 62 | 63 | dmau = 0 64 | dmal = 0 65 | 66 | CRC = 0x80006358 67 | SetBp(CRC) 68 | 69 | BCTRL = 0x8000A984 70 | SetBp(BCTRL) 71 | 72 | f = open("devo_dump.bin", "wb") 73 | 74 | while(True): 75 | 76 | Go() 77 | 78 | PC = GetRegValue("PC") 79 | 80 | if (PC == DMAU): 81 | 82 | dmau = GetRegValue("R0") 83 | 84 | elif (PC == DMAL): 85 | 86 | dmal = GetRegValue("R0") 87 | 88 | DMA(dmau, dmal) 89 | 90 | elif (PC == CRC): 91 | 92 | R3 = GetRegValue("R3") 93 | R4 = GetRegValue("R4") 94 | 95 | print "DUMP: 0x%X 0x%X" % (R3, R4) 96 | 97 | data = idaapi.dbg_read_memory(R3, R4) 98 | f.write(data) 99 | 100 | else: 101 | 102 | break 103 | 104 | f.close() -------------------------------------------------------------------------------- /scripts/devo_unpack_gdb.py: -------------------------------------------------------------------------------- 1 | import gdb 2 | 3 | def SetBp(ea): 4 | gdb.Breakpoint("*0x{:X}".format(ea)) 5 | 6 | def Go(): 7 | gdb.execute("continue"); 8 | 9 | def SetPC(ea): 10 | gdb.execute("set $pc = 0x{:X}".format(ea)) 11 | 12 | def GetRegValue(reg): 13 | return gdb.selected_frame().read_register(reg) 14 | 15 | def DMA(dmau, dmal): 16 | DMA_T = (dmal >> 1) & 1 17 | if (DMA_T): 18 | MEM_ADDR = (dmau >> 5) << 5 19 | LC_ADDR = (dmal >> 5) << 5 20 | 21 | MEM_ADDR |= 0x80000000 22 | 23 | DMA_LEN_U = (dmau & 0x1F) << 8 24 | DMA_LEN_L = (dmal >> 2) & 3 25 | 26 | LEN = DMA_LEN_U | DMA_LEN_L 27 | 28 | if (LEN == 0): 29 | LEN = 0x80 30 | 31 | DMA_LD = (dmal >> 4) & 1 32 | 33 | print("DMA: mem = 0x%X, cache = 0x%X, len = 0x%X, LD = %d\n" % (MEM_ADDR, LC_ADDR, LEN, DMA_LD)) 34 | 35 | if (DMA_LD): 36 | buf = gdb.inferiors()[0].read_memory(MEM_ADDR, LEN) 37 | for i in range(len(buf)): 38 | gdb.inferiors()[0].write_memory(LC_ADDR+i, buf[i]) 39 | else: 40 | buf = gdb.inferiors()[0].read_memory(LC_ADDR, LEN) 41 | for i in range(len(buf)): 42 | gdb.inferiors()[0].write_memory(MEM_ADDR+i, buf[i]) 43 | 44 | # SetBp(0x80004198) 45 | SetBp(0x80004230) 46 | Go() 47 | 48 | # SetPC(0x800041D8) 49 | SetPC(0x8000423C) 50 | 51 | # DMAU = 0x8000A4A4 52 | DMAU = 0x80025B14 53 | SetBp(DMAU) 54 | # DMAL = 0x8000A4C0 55 | DMAL = 0x80025B30 56 | SetBp(DMAL) 57 | 58 | dmau = 0 59 | dmal = 0 60 | 61 | # CRC = 0x80006358 62 | 63 | # New 64 | #CRCS = [0x80020DB0, 0x800210D8, 0x8002118C, 0x80021470, 0x80021568, 0x80021720, 0x80021890] 65 | CRCS = [0x80021470] 66 | 67 | for CRC in CRCS: 68 | SetBp(CRC) 69 | 70 | # BCTRL = 0x8000A984 71 | BCTRL = 0x800260F4 # Probably this 72 | SetBp(BCTRL) 73 | 74 | files = [open("devo_dump_0x{:X}.bin".format(x), "wb") for x in CRCS] 75 | 76 | while(True): 77 | Go() 78 | PC = GetRegValue("pc") 79 | if (PC == DMAU): 80 | dmau = GetRegValue("r0") 81 | elif (PC == DMAL): 82 | dmal = GetRegValue("r0") 83 | DMA(dmau, dmal) 84 | elif (PC in CRCS): 85 | idx = 999999 86 | for i in range(0, len(CRCS)): 87 | if PC == int(CRCS[i]): 88 | idx = i 89 | break 90 | R3 = GetRegValue("r3") 91 | R4 = GetRegValue("r4") 92 | print("DUMP: 0x%X 0x%X" % (R3, R4)) 93 | data = gdb.inferiors()[0].read_memory(R3, R4).tobytes() 94 | files[idx].write(data) 95 | else: 96 | break 97 | 98 | for f in files: 99 | f.close(); 100 | -------------------------------------------------------------------------------- /scripts/munge_ghidra.py: -------------------------------------------------------------------------------- 1 | # Paste this into Ghidra's python console with a range of addresses to dword swap 2 | def munge(): 3 | mem = currentProgram.getMemory() 4 | addr_iter = currentSelection.getAddresses(True) 5 | lo_addrs = [None] * 4 6 | hi_addrs = [None] * 4 7 | for addr in addr_iter: 8 | if None in lo_addrs: 9 | lo_addrs[addr.getOffset() & 3] = addr 10 | else: 11 | hi_addrs[addr.getOffset() & 3] = addr 12 | if not None in hi_addrs: 13 | lo_data = [mem.getByte(a) for a in lo_addrs] 14 | hi_data = [mem.getByte(a) for a in hi_addrs] 15 | for a, b in zip(hi_addrs, lo_data): 16 | mem.setByte(a, b) 17 | for a, b in zip(lo_addrs, hi_data): 18 | mem.setByte(a, b) 19 | lo_addrs = [None] * 4 20 | hi_addrs = [None] * 4 21 | -------------------------------------------------------------------------------- /scripts/wifilog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # For BSD netcat 3 | nc -k -l -p 64444 -u -v -w 1 4 | --------------------------------------------------------------------------------