Write-Ups

21 min read

CA CTF 2022: 5 languages, 1 binary - FFI

Write-up covering the Hard Reversing challenge ‘Freaky Forum Interception’ from Cyber Apocalypse CTF 2022.

clubby789,
Jul 19
2022

This challenge involved a C program reading a flag from the user and feeding each chunk of it to Golang, Python, Java and Rust programs. Users were required to solve the flag piece-by-piece.

Description

You've arrived on the planet Drion-1, where the high galactic court is based. You've come to spread the word about the dangers of Draeger, and gather support, but thousands upon thousands of other lifeforms are also here to argue, squabble and dispute, speaking a thousand different languages. Can you make yourself heard above the babble?

Initial Analysis 

We can first open up the program in a decompiler and have a look at the main function.

00030740 int32_t main(int32_t argc, char** argv, char** envp)

00030751 char* rax = calloc(nmemb: 0x80, size: 1)

00030768 fgets(buf: rax, n: 0x80, fp: stdin)

00030777 char* rax_1 = calloc(nmemb: 0x80, size: 1)

00030796 if (__isoc99_sscanf(s: rax, format: "HTB{%[^}]}", rax_1) != 1)

000308f4 fwrite(buf: "Invalid flag\n", size: 1, count: 0xd, fp: stderr)

000308fc exit(status: 0xffffffff)

000308fc noreturn

0003079c bool rax_4 = *rax_1

000307a0 void* rdx_2 = &rax_1[1]

000307a4 int32_t rcx = 0

000307a8 if (rax_4 != 0)

000307c4 do

000307b2 rax_4 = rax_4 == 0x5f

000307b5 rdx_2 = rdx_2 + 1

000307bc rcx = rcx + zx.d(rax_4)

000307be rax_4 = *(rdx_2 - 1)

000307be while (rax_4 != 0)

000307c9 if (rcx == 3 && strchr(rax_1, 0x5f) != 0)

000307f3 if (GoCheck(rax_1) == 0)

0003098e fwrite(buf: "Golang says no!\n", size: 1, count: 0x10, fp: stderr)

00030998 exit(status: 0xfffffffd)

00030998 noreturn

0003080b void* rbp_1 = &strchr(rax_1, 0x5f)[1]

00030812 char* rax_9 = strchr(rbp_1, 0x5f)

0003081a if (rax_9 != 0)

00030830 if (rust_check(rbp_1, rax_9 - rbp_1) == 0)

00030967 fwrite(buf: "Rust says no!\n", size: 1, count: 0xe, fp: stderr)

00030971 exit(status: 0xfffffffc)

00030971 noreturn

00030848 void* rbp_2 = &strchr(rbp_1, 0x5f)[1]

0003084f char* rax_13 = strchr(rbp_2, 0x5f)

00030857 if (rax_13 != 0)

00030869 if (python_check(rbp_2, rax_13 - rbp_2) == 0)

00030940 fwrite(buf: "Python says no!\n", size: 1, count: 0x10, fp: stderr)

0003094a exit(status: 0xfffffffb)

0003094a noreturn

0003087c void* rbp_3 = &strchr(rbp_2, 0x5f)[1]

00030883 strlen(rbp_3)

00030895 if (java_check(rbp_3) == 0)

00030919 fwrite(buf: "Java says no!\n", size: 1, count: 0xe, fp: stderr)

00030923 exit(status: 0xfffffffb)

00030923 noreturn

0003089a free(ptr: rax)

000308a6 puts(str: "\x1b[0;32mCorrect!\x1b[0m")

000308b4 return 0

000308cd fwrite(buf: "Invalid flag\n", size: 1, count: 0xd, fp: stderr)

000308d7 exit(status: 0xfffffffe)

000308d7 noreturn

The program begins by reading a flag from stdin. It then passes each chunk of the flag (split by _ characters) into four functions - GoCheck, rust_check, python_check, and java_check. With a quick look, we can confirm that this program is using FFI (foreign function interfacing) with the 4 languages to check the flag.

Golang

Go is a strongly-typed compiled language which has a runtime, builtin concurrency (‘goroutines’) and garbage collection.

GoCheck sets up the cgo runtime before calling into the _cgoexp_a885985053cf_GoCheck function, which itself calls main.GoCheck. If we search for the golang functions by filtering for those with main., we can see:

main.Oracle

main.Waiter

main.GoCheck

main.GoCheck.func2

main.GoCheck.func1

main.main

Golang has an awkward calling convention which mixes use of the stack and registers, so we'll begin with skimming over the code to try and understand the rough structure.

000afe00 int64_t main.GoCheck.func1(int64_t arg1, int64_t arg2, int64_t arg3, int64_t arg4, int32_t* arg5, int32_t arg6, int64_t* arg7 @ rax,

000afe00 int32_t arg8 @ rbx, void* arg9 @ r14)

000afe04 while (&__return_addr u<= *(arg9 + 0x10))

000afe58 arg4, arg3, arg2, arg1, arg5, arg6 = runtime.morestack_noctxt.abi0(arg1, arg2, arg3, arg4, arg5, arg6)

000afe5d arg7 = arg7

000afe62 arg8 = arg8

000afe0f int64_t __saved_rbp

000afe0f void* rbp = &__saved_rbp

000afe2e int64_t rcx

000afe2e void* rsi

000afe2e int32_t* rdi

000afe2e int32_t* r8

000afe2e uint64_t r9

000afe2e int128_t zmm15

000afe2e rcx, rsi, rdi, r8, r9, zmm15 = runtime.cgoCheckPointer(0, arg2, arg7, 0, arg5, arg6, &data_1d9da0, arg7, rbp, arg9)

000afe33 uint64_t rdx_1 = zx.q(arg8)

000afe4e return runtime.gobytes(rdi, rsi, rdx_1, rcx, r8, r9, arg7, sx.q(rdx_1.d), rbp, arg9, zmm15)

With debugging, we can see that GoCheck is passed a pointer to the flag fragment and a length. gobytes is likely C.GoBytes, a Golang library function which takes a pointer and a length and returns a Golang slice (slices are pointers with associated sizes).

000afda0 int64_t* main.GoCheck.func2(int64_t arg1, int64_t arg2, void* arg3, int64_t arg4, int32_t* arg5, int32_t arg6, void* arg7 @ r14)

000afda4 int64_t rbp

000afda4 while (&__return_addr u<= *(arg7 + 0x10))

000afde1 arg4, arg3, arg2, arg1, arg5, arg6 = runtime.morestack.abi0(arg1, arg2, arg3, arg4, arg5, arg6, rbp)

000afdaa int64_t var_8 = rbp

000afdb4 int64_t* r12 = *(arg7 + 0x20)

000afdf1 void var_30

000afdf1 if (r12 != 0 && *r12 == &arg_8)

000afdf3 *r12 = &var_30

000afdbd *(arg3 + 0x10)

000afdcd int64_t* rax = *(arg3 + 8)

000afdd1 main.Waiter(zx.q(*(arg3 + 0x20)), *(arg3 + 0x28), arg3, *(arg3 + 0x18), arg5, arg6, rax, arg7)

000afde0 return rax

GoCheck.func2 prepares stack size and calls main.Waiter, while GoCheck itself calls main.Oracle

rsi_3, rdi_3, r8_3, r9_3 = runtime.newproc(rdi_3, rcx_4, rdx_5, main.GoCheck.func2, r8_3, r9_4, rax_6, arg9)

newproc is used to start a new goroutine - an application-level managed thread, running func2. This is called in a loop, spawning multiple threads executing the Waiter function.

000af9c0 void main.Oracle(int64_t arg1, int64_t arg2, int64_t arg3, int64_t arg4, int32_t* arg5, int32_t arg6, int64_t* arg7 @ rax, int64_t arg8 @ r14)

000af9c4 int64_t rbx

000af9c4 while (&__return_addr u<= *(arg8 + 0x10))

000afa81 arg4, arg3, arg2, arg1, arg5, arg6 = runtime.morestack_noctxt.abi0(arg1, arg2, arg3, arg4, arg5, arg6)

000afa86 arg7 = arg7

000afa8b rbx = rbx

000af9d8 arg_8 = arg7

000af9dd int64_t var_10 = rbx

000af9e6 while (*arg7 s> 0)

000af9e8 int64_t rcx = data_248b38

000af9ef void* rdx = main.g

000af9f9 if (rcx != 0)

000afa00 int64_t rsi = 0

000afa38 while (true)

000afa38 uint64_t rcx_1 = zx.q(*(rdx + 8))

000afa3c int64_t rdi_2 = *rdx

000afa3f int64_t var_28_1 = rdi_2

000afa44 char var_20_1 = rcx_1.b

000afa50 int64_t rax_1

000afa50 rax_1, arg5, arg6 = runtime.selectnbsend(rdi_2, rsi, rdx, rcx_1, arg5, arg6, arg8)

000afa57 if (rax_1.b == 0)

000afa76 return

000afa5e int64_t rcx_3 = rsi + 1

000afa69 if (rcx s<= rcx_3)

000afa69 break

000afa28 rdx = rdx + 0x10

000afa2b rsi = rcx_3

000afa04 arg7 = arg_8

Oracle runs in a loop, sending some kind of data down a channel. Channels are a Golang concept that allows sending data structures between threads.

000afaa0 int64_t* main.Waiter(uint64_t arg1, void* arg2, int64_t arg3, int64_t arg4, int32_t* arg5, int32_t arg6, int64_t* arg7 @ rax, void* arg8 @ r14)

000afaa4 int64_t* rbx

000afaa4 while (&__return_addr u<= *(arg8 + 0x10))

000afbda arg3, arg5, arg6 = runtime.morestack_noctxt.abi0(arg1, arg2, arg3, arg4, arg5, arg6)

000afbdf arg7 = arg7

000afbe4 rbx = rbx

000afbe9 arg4 = arg4

000afbee arg1 = zx.q(arg1.b)

000afbf3 arg2 = arg2

000afac0 arg_28 = arg2

000afac8 bool var_51 = arg1.b

000afacd int64_t var_48 = arg4

000afad2 int64_t* var_30 = rbx

000afae3 while (true)

000afae3 int64_t var_40

000afae3 char rax_1

000afae3 int64_t rcx

000afae3 int64_t* rdx

000afae3 void* rsi

000afae3 int32_t* rdi

000afae3 int32_t* r8

000afae3 uint64_t r9

000afae3 int128_t zmm15_1

000afae3 rax_1, rcx, rdx, rsi, rdi, r8, r9, zmm15_1 = runtime.selectnbrecv(arg1, arg2, arg3, arg4, arg5, arg6, &var_40, rbx, arg8)

000afaea if (rax_1 == 0)

000afaf1 arg3, arg2, arg1, arg5, arg6 = time.Sleep(rdi, rsi, rdx, rcx, r8, r9, 10000000, arg8)

000afaf6 arg4 = var_48

000afafd else

000afafd int64_t rax_2 = var_40

000afb07 char var_38

000afb07 uint64_t rcx_1 = zx.q(var_38)

000afb0c char var_52_1 = rcx_1.b

000afb10 int128_t var_28 = zmm15_1

000afb16 int128_t var_18_1 = zmm15_1

000afb20 void* rax_3

000afb20 int64_t* rdx_1

000afb20 void* rsi_1

000afb20 int32_t* rdi_1

000afb20 int32_t* r8_1

000afb20 uint64_t r9_1

000afb20 rax_3, rdx_1, rsi_1, rdi_1, r8_1, r9_1 = runtime.convT64(rdi, rsi, rdx, rcx_1, r8, r9, rax_2, arg8)

000afb2c var_28.q = &data_1d95a0

000afb31 var_28:8.q = rax_3

000afb40 void* rax_5

000afb40 int64_t rdx_2

000afb40 int32_t* r8_2

000afb40 uint64_t r9_2

000afb40 rax_5, rdx_2, r8_2, r9_2 = runtime.convT64(rdi_1, rsi_1, rdx_1, &data_1d95a0, r8_1, r9_1, var_48, arg8)

000afb4c var_18_1.q = &data_1d95a0

000afb51 var_18_1:8.q = rax_5

000afb56 os.Stdout

000afb71 arg2, arg1, arg5, arg6 = fmt.Fprintln(&(*nullptr->ident.signature)[2], &(*nullptr->ident.signature)[2], rdx_2, &var_28, r8_2, r9_2, &go.itab.*os.File,io.Writer, arg8)

000afb76 arg4 = var_48

000afb7b arg3 = rax_2

000afb83 if (arg4 == arg3)

000afb94 bool rcx_3

000afb94 if (*arg_28 == 0)

000afba7 rcx_3 = false

000afba2 else

000afba2 rcx_3 = var_51 == var_52_1

000afba9 *arg_28 = rcx_3

000afbb3 *arg7 = *arg7 - 1

000afbc0 return arg7

000afad9 rbx = var_30
000afae3 rax_1, rcx, rdx, rsi, rdi, r8, r9, zmm15_1 = runtime.selectnbrecv(arg1, arg2, arg3, arg4, arg5, arg6, &var_40, rbx, arg8)

000afaea if (rax_1 == 0)

000afaf1 arg3, arg2, arg1, arg5, arg6 = time.Sleep(rdi, rsi, rdx, rcx, r8, r9, 10000000, arg8)

000afaf6 arg4 = var_48

000afafd else

000afafd int64_t rax_2 = var_40

This is called in a loop, and appears to receive from the channel. If the receive succeeds, the rest of the function is run - otherwise, it sleeps for 1 second and tries again. We therefore have a number of Waiters racing to read values written to the channel by Oracle.

000afb83 if (arg4 == arg3)

000afb94 bool rcx_3

000afb94 if (*arg_28 == 0)

000afba7 rcx_3 = false

000afba2 else

000afba2 rcx_3 = var_51 == var_52_1

000afba9 *arg_28 = rcx_3

000afbb3 *arg7 = *arg7 - 1

000afbc0 return arg7

000afad9 rbx = var_30

Two values are compared - if they are equal, *arg28 = *arg28 && var_51 == var_52_1, and arg27 is decremented, then the function returns. We can assume that the value arg7 points to is a count of the current Waiters still running, as it's decremented on exit. Oracle also runs until its arg7 is zero, which increases the possibility of this.

Luckily, some debug info has been left in the binary. If we use GDB to break at main.Oracle, and enter the flag HTB{1234567_1_1_1} we can see some type info

[#0] 0x5555555d0cc0 → main.Waiter(left=0xc00001a138, ch=0xc000102060, me={

pos = 0x0,

b = 0x31

}, ok=0xc00001a130)

───────────────────────────────────────────────────────────────────────────────────────────────────────

gef➤ dwQuit

gef➤ p me

$1 = {

pos = 0x0,

b = 0x31

}

gef➤ ptype me

type = struct main.aTuple {

int pos;

uint8 b;

}

b being '1', i.e. the first char of our flag attempt. With further debugging, we can see that each thread is given a main.aTuple containing a character of the flag, and the position it appears in. It is sent tuples from Oracle (which uses main.g, an array of main.aTuple). If the pos field matches, a shared variable is ANDed with me.b == tup.b. In essence, each thread waits til it sees the 'correct' tuple for its me.pos, then checks if the corresponding character is correct. We can determine this statically by looking at main.g.

gef➤ p main.g

$1 = {

array = 0x1f2660 <main..stmp_0>,

len = 0x7,

cap = 0x7

}

gef➤ p main.g.array

$2 = (main.aTuple *) 0x1f2660 <main..stmp_0>

gef➤ p *main.g.array

$3 = {

pos = 0x4,

b = 0x31

}

gef➤ set $arr = main.g.array

gef➤ p *[email protected]

$5 = {{

pos = 0x4,

b = 0x31

}, {

pos = 0x0,

b = 0x67

}, {

pos = 0x2,

b = 0x74

}, {

pos = 0x1,

b = 0x33

}, {

pos = 0x5,

b = 0x6e

}, {

pos = 0x3,

b = 0x74

}, {

pos = 0x6,

b = 0x67

}}

This results in the first segment, g3tt1ng.

Rust

Rust is a statically/strongly typed compiled language with a heavy focus on stability, security and performance.

000b05d0 uint64_t rust_check(char* flag, int64_t len)

000b05dc void* rbx_4

000b05dc if (len != 6)

000b07ee label_b07ee:

000b07ee rbx_4 = nullptr

000b05e5 else

000b05e5 uint32_t rax_1 = zx.d(*flag)

000b05fa if ((not.b(rax_1.b) & (rax_1.b - 'A' u< 26) << 5) != 0)

000b05fa goto label_b07ee

000b0600 uint32_t rbp_1 = zx.d(flag[1])

000b0616 if ((not.b(rbp_1.b) & (rbp_1.b - 'A' u< 26) << 5) != 0)

000b0616 goto label_b07ee

000b061c uint32_t rdi = zx.d(flag[2])

000b0632 if ((not.b(rdi.b) & (rdi.b - 'A' u< 26) << 5) != 0)

000b0632 goto label_b07ee

000b0638 uint32_t rsi = zx.d(flag[3])

000b064e if ((not.b(rsi.b) & (rsi.b - 'A' u< 26) << 5) != 0)

000b064e goto label_b07ee

000b0654 uint32_t r9_1 = zx.d(flag[4])

000b066d if ((not.b(r9_1.b) & (r9_1.b - 'A' u< 26) << 5) != 0)

000b066d goto label_b07ee

000b0673 uint32_t r8_1 = zx.d(flag[5])

000b068c if ((not.b(r8_1.b) & (r8_1.b - 'A' u< 26) << 5) != 0)

000b068c goto label_b07ee

000b06a5 if (r8_1 + r9_1 + rsi + rdi + rax_1 + rbp_1 != 0x223)

000b06a5 goto label_b07ee

000b06ab void* r14_1 = &flag[6]

The code begins with first checking that the segment is of length 6, and that each character is a lowercase ASCII value (or number or special character).

It then checks that each of the 6 characters have a sum of 0x223.

000b0736 uint64_t rax_5 = zx.q(*flag)

000b0741 uint64_t rax_7 = rax_5 * 3 + zx.q(flag[1])

000b074c uint64_t rax_9 = rax_7 * 3 + zx.q(flag[2])

000b0757 uint64_t rax_11 = rax_9 * 3 + zx.q(flag[3])

000b0762 uint64_t rax_13 = rax_11 * 3 + zx.q(flag[4])

000b0776 if (rax_13 * 3 + zx.q(flag[5]) != 0x8dd3)

000b0776 goto fail

This can be translated as:

val = flag[0]

for i in range(1, 6):

val = val * 3

val = val + flag[1]

assert val == 0x8dd3
000b06b4 void* var_48_1 = flag_end

000b06b9 char* var_40_1 = flag

000b06be void* var_38_1 = flag_end

000b06c6 int128_t var_30_1 = 0

000b06cb int64_t var_20_1 = 0

000b06dc struct Vec new_vec

000b06dc _$LT$alloc..vec..Vec$LT$...GT$$GT$::from_iter::h8a692f4fc093f52b(&new_vec, &var_50)

000b06e7 if (new_vec.cap != 6)

000b0707 c2 = 0

000b0701 else

000b0701 c2.b = bcmp(new_vec.t, &data_183070, 0x30) == 0

The from_iter function appears to take a vector argument, iterate over it forwards and backwards simultaneously, collecting the results into a new vector (new_vec.t).

001630ef arg1->t = rax_5

001630f2 arg1->len = r15_1

001630f6 uint64_t rcx = 0

001630fb if (rbx != r13)

00163103 while (r12 != rbp)

00163109 uint64_t rsi = zx.q(*(r12 - 1))

0016310f r12 = r12 - 1

00163116 rax_5[rcx] = rsi + zx.q(*(rbx + rcx))

0016311e void* rdx_5 = rbx + rcx + 1

00163122 rcx = rcx + 1

00163129 if (rdx_5 == r13)

00163129 break

0016312b arg1->cap = rcx

00163140 return arg1

This is then compared against a static value, data_183070

00183070 data_183070:

00183070 df 00 00 00 00 00 00 00 dd 00 00 00 00 00 00 00 ................

00183080 67 00 00 00 00 00 00 00 67 00 00 00 00 00 00 00 g.......g.......

00183090 dd 00 00 00 00 00 00 00 df 00 00 00 00 00 00 00 ................

The last check begins like this:

000b0778 something.field_0 = flag

000b077d something.field_8 = flag_end

000b0782 something.field_10 = 0

000b0793 _$LT$alloc..vec..Vec$LT$...GT$$GT$::from_iter::h055cee66a9154a18(&new_vec, &something)

It begins by allocating a new out_vec.data field by taking the existing length and multiplying it by 0x10.

00162dea if ((end == start && start != end) || (end != start && mulu.dp.q(length, 0x10) u>> 0x40 == 0 && rax_3 != 0 && start != end))

00162dfb int64_t rcx_3 = not.q(start) + end

00162dfe int64_t rdx_5 = zx.q(end.d - start.d) & 7

00162e02 if (rdx_5 != 0)

00162e27 int64_t temp2_1

00162e27 do

00162e10 rax_3->index = number

00162e13 rax_3->val = start

00162e17 start = start + 1

00162e1b rax_3 = &rax_3[1]

00162e1f number = number + 1

00162e23 temp2_1 = rdx_5

00162e23 rdx_5 = rdx_5 - 1

00162e23 while (temp2_1 != 1)

00162e2d if (rcx_3 u>= 7)

00162ec6 do

00162e40 rax_3->index = number

00162e43 rax_3->val = start

00162e4b rax_3->__offset(0x10).q = number + 1

00162e53 rax_3->__offset(0x18).q = start + 1

00162e5b rax_3->__offset(0x20).q = number + 2

00162e63 rax_3->__offset(0x28).q = start + 2

00162e6b rax_3->__offset(0x30).q = number + 3

00162e73 rax_3->__offset(0x38).q = start + 3

00162e7b rax_3->__offset(0x40).q = number + 4

00162e83 rax_3->__offset(0x48).q = start + 4

00162e8b rax_3->__offset(0x50).q = number + 5

00162e93 rax_3->__offset(0x58).q = start + 5

00162e9b rax_3->__offset(0x60).q = number + 6

00162ea3 rax_3->__offset(0x68).q = start + 6

00162eab rax_3->__offset(0x70).q = number + 7

00162eb3 rax_3->__offset(0x78).q = start + 7

00162eb7 start = start + 8

00162ebb number = number + 8

00162ebf rax_3 = rax_3 - -0x80

00162ebf while (start != end)

00162e29 goto label_162ee6

Something can be determined to be a structure with 3 fields of 64 bits each as determined by its use in the from_iter function.

00162d9a int64_t rax

00162d9a int64_t var_38 = rax

00162d9e int64_t rbx = arg2->start

00162da1 int64_t r13 = arg2->end

00162da5 int64_t rbp = arg2->number

00162dac uint64_t length = r13 - rbx

00162daf int64_t* rax_3

00162daf if (r13 == rbx)

00162ece rax_3 = &nullptr->ident.abi_version

00162ed3 out_vec->data = 8

00162ed6 out_vec->len = length

00162dbd else

00162dbd size_t rax_2

00162dbd int64_t rdx_1

00162dbd rdx_1:rax_2 = mulu.dp.q(length, 0x10)

00162dc0 if (mulu.dp.q(length, 0x10) u>> 0x40 != 0)

00162efc alloc::raw_vec::capacity_overflow::hd1e88f904b72f59f()

00162efc noreturn

00162dd1 int64_t rdx_2

00162dd1 rax_3, rdx_2 = __rust_alloc(rax_2, 8)

00162dda if (rax_3 == 0)

00162f0c alloc::alloc::handle_alloc_error::hda5f4d703a660516(rax_2, 8, rdx_2)

00162f0c noreturn

00162de0 out_vec->data = rax_3

00162de3 out_vec->len = length

This function has two paths based on the size of the input string. The easier one to read is the verbose >= 7 path - for each 8 elements in the input, we write two values into the array of 0x10 structures - the current index, and a pointer into the input string. This results in essentially a Vec<(usize, &u8)>, referencing the bytes of the original data. Back in rust_check

alloc::slice::merge_sort::h5f03ae8218fddccf(r14_1, rbx_1)

The array is sorted according to the value of each byte - rearranging the string but keeping the original indexes intact. The final from_iter function is very complex, using a lot of SIMD and advanced x86 features in the code, but we can make an educated guess - the result of it is compared against an array of 6 uint64_t values, none of which exceed 6. As we know the indexes of the string have been preserved, none of which can exceed 6, we can guess this function is simply gathering the indexes.

We can now write a solver using the Z3 theorem prover to determine the flag based on our list of constraints.

- ASCII, non-uppercase

from z3 import *

s = Solver()

flag = [BitVec("flag_%s" % i, 64) for i in range(6)]

for f in flag:

s.add(f >= 0x20)

s.add(f < 0x7e)

s.add(Not(And(f >= 65, f <= 90)))

- Sum of all byes

total = sum(f for f in flag)

s.add(total == 547)

- Multiplication

value = BitVec("value", 64)

s.add(value == 0)

for f in flag:

value = value * 3

value += f

s.add(value == 36307)

- Reversing + addition

for i, v in enumerate([223, 221, 103, 103, 221, 223]):

s.add(flag[i] + flag[len(flag)-1-i] == v)

- Ordering

ordering = [2, 3, 0, 4, 1, 5]

for i in range(len(ordering) - 1):

for j in range(0, len(ordering)):

if j > i:

s.add(flag[ordering[i]] < flag[ordering[j]])

- Extracting flag

print(s.check())

m = s.model()

for f in flag:

print(chr(m[f].as_long()), end='')

print("")

The result is: fr34ky

Python

Python is a duck-typed interpreted scripting language that is implemented in C with a C API for FFI.

00163490 bool python_check(char* arg1, int64_t arg2)

00163492 bool r15 = false

001634a5 if (arg2 == 5)

001634c0 char* rbx_1 = arg1

001634c3 char* r13_1 = &secret

001634ca r15 = true

001634d0 Py_Initialize()

001634da seed(0x7a69)

001634ec int64_t* rax_2 = PyCMethod_New(&GenDef, 0, 0, 0)

0016353e do

001634f7 int64_t rax_3 = PyObject_CallNoArgs(rax_2)

00163502 char rax_4 = PyLong_AsLong(rax_3)

0016350d Py_DecRef(rax_3)

00163515 if (r15 != 0)

00163528 r15 = ((sx.d(*rbx_1) ^ zx.d(rax_4)) == zx.d(*r13_1)).b

0016352c r13_1 = &r13_1[1]

00163537 rbx_1 = &rbx_1[1]

00163537 while (r13_1 != "Failed to create Java VM") // ignore this - bad relocation processing by my decompiler!

00163540 int64_t temp0_1 = *rax_2

00163540 *rax_2 = *rax_2 - 1

00163544 if (temp0_1 == 1)

00163553 _Py_Dealloc(rax_2)

00163546 Py_Finalize()

001634b8 return r15

We can use the Python documentation to improve the types:

00163490 bool python_check(char* arg1, int64_t arg2)

00163492 bool r15 = false

001634a5 if (arg2 == 5)

001634c0 char* flag_char = arg1

001634c3 char* secret_char = &secret

001634ca r15 = true

001634d0 Py_Initialize()

001634da seed(0x7a69)

001634ec struct PyObject* method = PyCMethod_New(ml: &GenDef, self: nullptr)

0016353e do

001634f7 struct PyObject* res = PyObject_CallNoArgs(method)

00163502 char res = PyLong_AsLong(obj: res)

0016350d Py_DecRef(res)

00163515 if (r15 != 0)

00163528 r15 = ((sx.d(*flag_char) ^ zx.d(res)) == zx.d(*secret_char)).b

0016352c secret_char = &secret_char[1]

00163537 flag_char = &flag_char[1]

00163537 while (secret_char != "Failed to create Java VM")

00163540 struct PyObject ref = method->refcount

00163540 method->refcount = method->refcount - 1

00163544 if (ref == 1)

00163553 _Py_Dealloc(method)

00163546 Py_Finalize()

001634b8 return r15
0024fa80 struct PyMethodDef GenDef =

0024fa80 {

0024fa80 char* ml_name = 0x1a8e78 {"rand_stream"}

0024fa88 void* ml_method = GetNum

0024fa90 int32_t ml_flags = 0x80

0024fa94 char* ml_doc = nullptr

0024fa9c }

We initialize a new Python function named rand_stream that has its backing C method as GetNum.

00163400 struct PyObject* seed(int32_t arg1)

0016340e struct PyObject* rand = PyImport_ImportModule("random")

0016341d randomMod = rand

00163424 struct PyObject* rax = PyObject_GetAttrString(o: rand, name: "seed")

0016342f struct PyObject* rax_1 = PyLong_FromLong(sx.q(arg1))

0016343c struct PyObject* tup = PyTuple_New(1)

0016344c PyTuple_SetItem(t: tup, index: 0, v: rax_1)

00163457 struct PyObject* rax_2 = PyObject_CallObject(callable: rax, args: tup)

0016345c struct PyObject temp0 = tup->refcount

0016345c tup->refcount = tup->refcount - 1

00163461 struct PyObject temp1_1

00163461 struct PyObject temp2_1

00163461 if (temp0 == 1)

00163473 rax_2 = _Py_Dealloc(tup)

00163478 temp1_1 = rax->refcount

00163478 rax->refcount = rax->refcount - 1

00163463 else

00163463 temp2_1 = rax->refcount

00163463 rax->refcount = rax->refcount - 1

00163468 if ((temp0 == 1 && temp1_1 != 1) || (temp0 != 1 && temp2_1 != 1))

0016346f return rax_2

00163468 if ((temp0 == 1 && temp1_1 == 1) || (temp0 != 1 && temp2_1 == 1))

00163487 return _Py_Dealloc(rax) __tailcall

The seed function imports the random module (saving the pointer to randomMod), before calling random.seed() with the argument passed (31337).

python_check seeds the random module, before repeatedly calling GetNum and XORing the result with each character of the flag, and comparing it to each character of secret.
001632f0 int64_t GetNum()

00163307 void* fsbase

00163307 int64_t rax = *(fsbase + 0x28)

00163317 struct PyObject* rax_2 = PyObject_GetAttrString(o: randomMod, name: "randrange")

00163324 struct PyObject* rax_3 = PyLong_FromLong(0x100)

00163329 struct PyObject* var_30 = rax_3

00163331 struct PyThreadState* tstate = PyThreadState_Get()

00163339 void* rax_4 = rax_2->__offset(0x8).q

00163344 int64_t rax_6

00163344 int64_t r13_1

00163344 if ((*(rax_4 + 0xa9) & 8) != 0)

0016334e rax_6 = *(rax_2 + *(rax_4 + 0x38))

00163356 if (rax_6 != 0)

00163382 r13_1 = _Py_CheckFunctionResult(tstate: tstate, callable: rax_2, res: rax_6(rax_2, &var_30, -0x7fffffffffffffff, 0), where: nullptr)

00163356 if ((*(rax_4 + 0xa9) & 8) == 0 || ((*(rax_4 + 0xa9) & 8) != 0 && rax_6 == 0))

001633f0 r13_1 = _PyObject_MakeTpCall(tstate, rax_2, &var_30, 1, 0)

00163385 struct PyObject temp0 = rax_2->refcount

00163385 rax_2->refcount = rax_2->refcount - 1

0016338a struct PyObject temp1_1

0016338a struct PyObject temp2_1

GetNum loads random.randrange and calls it with an argument of 0x100.

With this, we can solve this part:

#!/usr/bin/env python3

from pwn import *

import struct

import sys

import random

fn = sys.argv[1]

p = ELF(fn, checksec=False)

secret = p.read(p.sym['secret'], 5)

flag = []

random.seed(31337)

for b in secret:

flag.append(b ^ random.randrange(0x100))

print(bytes(flag).decode())

This results in our next flag segment - u51Ng

Java

Java is an OOP language that is compiled to bytecode and interpreted by the Java VM.

001635a7 struct JNIEnv* rdi_1 = env

001635c5 int64_t rax_5 = (*(rdi_1->field_0 + 0x28))(rdi_1, "Checker", 0, &Class, 0x752)

001635c8 void* rdi_2 = env

001635d2 (*(*rdi_2 + 0x80))(rdi_2)

001635db if (rax_5 == 0)

0016367f r13 = 1

00163685 puts(str: "Failed to find Checker class")

A function loaded at offset 0x28 from JNIEnv is called to 'find the Checker class'. We can surmise that this is env->DefineClass(env, "Checker", Null, &Class, size).

001635e1 void* rdi_3 = env

001635f9 int64_t rax_7 = (*(*rdi_3 + 0x388))(rdi_3, checker_class, "hello_java", "(Ljava/lang/String;)Z")

00163605 if (rax_7 == 0)

00163697 r13 = 1

0016369d puts(str: "Failed to find main function")

0016360b else

0016360b void* rdi_4 = env

0016361b void* rdi_5 = env

0016362a int64_t r8_1 = *rdi_5

00163639 r13.b = (*(r8_1 + 0x3a8))(rdi_5, checker_class, rax_7, (*(*rdi_4 + 0x538))(rdi_4, rax_2), r8_1) != 0

0016363d free(ptr: rax_2)

If DefineClass is at +0x28, and 'at index 5 in the pointer table' according to oracle docs, then ((0x388 - 0x28)/8) + 5 = 113 - which is

jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,

const char *name, const char *sig);

Returns the method ID for a static method of a class. The method is specified by its name and signature.

GetStaticMethodID() causes an uninitialized class to be initialized.

The signature of Ljava/lang/String;)Z specifies a function which takes a single String argument and returns a boolean.

The next function (based on the offset) is CallStaticBooleanMethod, which has a signature of:

NativeType CallStatic<type>Method (JNIEnv *env, jclass clazz,jmethodID methodID, ...);.

One of the arguments to it is NewStringUTF, which is called on the existing flag pointer. This prepares a UTF8 string object which can be passed to Java.

We can see where the class is located in the binary, so we can extract it with dd: dd if=ffi of=checker.class bs=1 skip=1503184 count=1874

Then decompile it:

package p000;

import java.util.stream.IntStream;

/* renamed from: Checker */

/* loaded from: checker.class */

public class Checker {

public static boolean hello_java(String str) {

int[] iArr = {219, 227, 209, 154, 104, 97, 158, 163};

return str.chars().filter(i -> {

return i % 3 == 0;

}).count() == 4 && IntStream.range(0, str.length() - 1).mapToObj(i -> {

return new Object[]{Integer.valueOf(i), Integer.valueOf(str.charAt(i)), Integer.valueOf(str.charAt(i + 1))};

}).filter(objArr -> {

return ((Integer) objArr[1]).intValue() + ((Integer) objArr[2]).intValue() == iArr[((Integer) objArr[0]).intValue()];

}).count() == ((long) (str.length() - 1));

}

}

We:

  • Create an IntStream counting from 0 to the length of the string-1
  • For each index, create an array of [i, string[i], string[i+1]]
  • For each array, add the two string parts, and compare them to iArr[i] - keep only the iterations satisfying the check
  • Check that all iterations satisfied the check

We can then write a solver:

#!/usr/bin/env python3

from z3 import *

s = Solver()

sums = [219, 227, 209, 154, 104, 97, 158, 163]

flag = [BitVec("flag_%s" % i, 8) for i in range(9)]

for f in flag:

s.add(f >= 0x20)

s.add(f <= 0x7f)

for i, v in enumerate(sums):

s.add(flag[i] + flag[i+1] == v)

print(s.check())

m = s.model()

for f in flag:

print(chr(m[f].as_long()), end='')

print("")

And get the segment: func710n5.

Flag

Putting it all together, we get our final flag: HTB{g3tt1ng_fr34ky_u51Ng_func710n5}

Hack The Blog

The latest news and updates, direct from Hack The Box