EHAX CTF 2026 RE Pathfinder
5555555556d0 int32_t main(int32_t argc, char** argv, char** envp)
5555555556db void* fsbase
5555555556db int64_t rax = *(fsbase + 0x28)
5555555556ea sub_5555555555a1()
5555555556fe printf(format: "Are you a pathfinder?\n[y/n]: ")
55555555570d fflush(fp: stdout)
555555555738 char buf[0x40]
555555555738 int32_t result
555555555738
555555555738 if (read(fd: 0, &buf, nbytes: 1) != 0)
55555555576b int32_t i
55555555576b
55555555576b do
555555555763 i = getchar()
55555555576b while (i != 0xa)
55555555576b
555555555776 if (buf[0] == 'n')
555555555782 puts(str: "Understandable. See you again, maybe..")
555555555791 fflush(fp: stdout)
555555555796 result = 1
555555555776 else if (buf[0] == 'y')
5555555557e2 printf(format: "Ok, tell me the best path: ")
5555555557f1 fflush(fp: stdout)
๐55555555580a memset(&buf, 0, 0x40)
๐55555555580a
555555555835 if (read(fd: 0, &buf, nbytes: 0x40) != 0)
555555555878 buf[strcspn(&buf, "\n")] = 0
555555555878
๐555555555891 if (sub_555555555444(&buf) == 0)
5555555558ea puts(str: "Better luck next time.")
5555555558f9 fflush(fp: stdout)
5555555558fe result = 1
๐555555555891 else
5555555558a7 char flag[0x108]
5555555558a7 sub_555555555602(&buf, &flag)
5555555558c5 printf(format: "You have what it takes. Flag: %s\nBye.\n",
5555555558c5 &flag, "You have what it takes. Flag: %s\nBye.\n")
5555555558d4 fflush(fp: stdout)
5555555558d9 result = 0
555555555835 else
555555555841 puts(str: "Where dat path tho?")
555555555850 fflush(fp: stdout)
555555555855 result = 1
5555555557a9 else
5555555557b5 puts(str: "Invalid option")
5555555557c4 fflush(fp: stdout)
5555555557c9 result = 1
555555555738 else
555555555744 puts(str: "No input?")
555555555753 fflush(fp: stdout)
555555555758 result = 1
555555555758
555555555907 *(fsbase + 0x28)
555555555907
555555555910 if (rax == *(fsbase + 0x28))
555555555918 return result
555555555918
555555555912 __stack_chk_fail()
555555555912 noreturn
Nothing too interesting in main, its a maze and you have to find the path through it
The function, that could be an initialization is not setting anything interesting
5555555555a1 int64_t sub_5555555555a1()
5555555555be setvbuf(fp: stdin, buf: nullptr, mode: 2, size: 0)
5555555555dc setvbuf(fp: stdout, buf: nullptr, mode: 2, size: 0)
555555555601 return setvbuf(fp: stderr, buf: nullptr, mode: 2, size: 0)
The check path function
555555555444 uint64_t sub_555555555444(char* arg1)
555555555450 int32_t var_30 = 0
555555555457 int32_t var_2c = 0
555555555462 char* var_20 = arg1
555555555462
55555555556f while (*var_20 != 0)
55555555546f char rax_2 = *var_20
555555555485 int64_t rax_7 = sx.q(zx.d(rax_2)) * 0xc
555555555493 int64_t rcx_1 = *(rax_7 + &data_555555558120)
55555555549b int24_t rax_8 = (*(rax_7 + 0x555555558128)).3
55555555549b
๐5555555554d8 if (rax_8:2.b == 0)
5555555554da return 0
5555555554da
5555555554ea int32_t rax_20 = var_30 + rcx_1.d
5555555554f5 int32_t rax_22 = var_2c + rcx_1:4.d
5555555554f5
555555555508 if (rax_20 u> 9 || rax_22 u> 9)
55555555550a return 0
55555555550a
55555555551e char rax_26 = sub_55555555523f(var_30, var_2c)
55555555551e
55555555554c if (((sub_55555555523f(rax_20, rax_22)
55555555554c & ((rax_2 * 0x6b) ^ rax_8:1.b ^ 0x3c))
55555555554c | (rax_26 & ((rax_2 * 0x6b) ^ rax_8.b ^ 0x3c))) == 0)
55555555554e return 0
55555555554e
555555555558 var_30 = rax_20
55555555555e var_2c = rax_22
555555555561 var_20 = &var_20[1]
555555555561
55555555557f if (var_30 != 9 || var_2c != 9)
555555555581 return 0
555555555581
555555555599 int32_t rax_37
555555555599 rax_37.b = sub_55555555526b(arg1) == 0x86ba520c
55555555559c return zx.q(rax_37.b)
is referring to 0x555555558128 but in the binary its only zeroes. There is no obvious place to look for the initialization of the maze
The answer is inside of .init_array section
.init_array section started {0x555555557dc0-0x555555557dd8}
555555557dc0 void (* init_array[0x3])() =
555555557dc0 {
555555557dc0 [0x0] = _INIT_0
555555557dc8 [0x1] = _INIT_1
555555557dd0 [0x2] = _INIT_2
function _INIT_1 sets up the maze
555555555235 for (int32_t i = 0; i s<= 0x63; i += 1)
55555555522a *(sx.q(i) + &data_5555555580a0) =
55555555522a *(sx.q(i) + &data_555555556020) ^ sub_5555555551c9(i)
Claude wrote the BFS script to find the way around the maze
`EESSSWWSSSSSSEEEEEEEENNESS`
EHAX{2E3S2W6S8E2NE2S}
WEB
Borderline Personality
Inside of the handout directory there are two subdirectories backend and haproxy
The admin route is very straightforward
@app.route('/admin/flag', methods=['GET', 'POST'])
def flag():
return "EHAX{TEST_FLAG}\n", 200
But there is a haproxy in front of it
frontend http-in
bind *:8080
acl restricted_path path -m reg ^/+admin
http-request deny if restricted_path
default_backend application_backend
backend application_backend
server backend1 backend:5000
URL encoding of a in admin is enough to fool that rule
http://chall.ehax.in:9098/%61dmin/flag
EH4X{BYP4SSING_R3QU3S7S_7HR0UGH_SMUGGLING__IS_H4RD}