Write-Ups
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.
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?
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.
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 *$arr@7
$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 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 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 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:
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.
Putting it all together, we get our final flag: HTB{g3tt1ng_fr34ky_u51Ng_func710n5}