Hunting License
11 minutes to read
We have a binary called license
:
$ file license
license: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5be88c3ed329c1570ab807b55c1875d429a581a7, for GNU/Linux 3.2.0, not stripped
$ ldd license
linux-vdso.so.1 (0x00007ffe18bb1000)
libreadline.so.8 => /lib/x86_64-linux-gnu/libreadline.so.8 (0x00007f430a30a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f430a118000)
libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f430a0e8000)
/lib64/ld-linux-x86-64.so.2 (0x00007f430a366000)
Decompilation
If we open it in Ghidra, we will see this main
function:
int main() {
char cVar1;
int iVar2;
puts("So, you want to be a relic hunter?");
puts("First, you\'re going to need your license, and for that you need to pass the exam.");
puts("It\'s short, but it\'s not for the faint of heart. Are you up to the challenge?! (y/n)");
iVar2 = getchar();
cVar1 = (char) iVar2;
if (((cVar1 != 'y') && (cVar1 != 'Y')) && (cVar1 != '\n')) {
puts("Not many are...");
/* WARNING: Subroutine does not return */
exit(-1);
}
exam();
puts("Well done hunter - consider yourself certified!");
return 0;
}
As can be seen, we need to enter y
and then the program calls exam
:
void exam() {
int iVar1;
undefined8 local_38;
undefined8 local_30;
undefined local_28;
undefined8 local_1c;
undefined4 local_14;
char *local_10;
local_10 = (char *) readline("Okay, first, a warmup - what\'s the first password? This one\'s not ev en hidden: ");
iVar1 = strcmp(local_10, "PasswordNumeroUno");
if (iVar1 != 0) {
puts("Not even close!");
/* WARNING: Subroutine does not return */
exit(-1);
}
free(local_10);
local_1c = 0;
local_14 = 0;
reverse(&local_1c, t, 0xb);
local_10 = (char *) readline("Getting harder - what\'s the second password? ");
iVar1 = strcmp(local_10, (char *) &local_1c);
if (iVar1 != 0) {
puts("You\'ve got it all backwards...");
/* WARNING: Subroutine does not return */
exit(-1);
}
free(local_10);
local_38 = 0;
local_30 = 0;
local_28 = 0;
xor(&local_38, t2, 0x11, 0x13);
local_10 = (char *) readline("Your final test - give me the third, and most protected, password: ");
iVar1 = strcmp(local_10, (char *) &local_38);
if (iVar1 != 0) {
puts("Failed at the final hurdle!");
/* WARNING: Subroutine does not return */
exit(-1);
}
free(local_10);
}
There are three passwords, all of them checked with strcmp
. The first one appears in plaintext (PasswordNumeroUno
), the rest are encoded or encrypted.
Static analysis
One way to solve the challenge is through static analysis. That is, we can take a look at the strings of the binary and see how the passwords are stored to decode/decrypt them. For instance, the second password is transformed by reverse
. This function, as its name suggests, just reverses a string. If we search for the stored password, we will see this (global variable named t
):
t XREF[3]: Entry Point(*), exam:004012ed(*),
exam:004012ed(*)
00404060 30 77 54 undefined1
64 72 30
77 73 73
00404060 30 undefined130h [0] XREF[3]: Entry Point(*), exam:004012ed(*),
exam:004012ed(*)
00404061 77 undefined177h [1]
00404062 54 undefined154h [2]
00404063 64 undefined164h [3]
00404064 72 undefined172h [4]
00404065 30 undefined130h [5]
00404066 77 undefined177h [6]
00404067 73 undefined173h [7]
00404068 73 undefined173h [8]
00404069 34 undefined134h [9]
0040406a 50 undefined150h [10]
0040406b 00 undefined100h [11]
This is just:
$ python3 -q
>>> bytes((0x30, 0x77, 0x54, 0x64, 0x72, 0x30, 0x70, 0x73, 0x73, 0x34, 0x50))
b'0wTdr0pss4P'
And reversed:
>>> bytes((0x30, 0x77, 0x54, 0x64, 0x72, 0x30, 0x70, 0x73, 0x73, 0x34, 0x50))[::-1]
b'P4ssp0rdTw0'
Then the third password is stored at t2
, and the program calls xor
:
void xor(long param_1, long param_2, ulong param_3, byte param_4) {
int local_c;
for (local_c = 0; (ulong) (long) local_c < param_3; local_c++) {
*(byte * )(param_1 + local_c) = *(byte *) (param_2 + local_c) ^ param_4;
}
}
This function implements a XOR cipher, and the XOR key is at param_4
. This argument is 0x13
, so this is the XOR key. And t2
contains these bytes:
t2 XREF[3]: Entry Point(*), exam:00401361(*),
exam:00401361(*)
00404070 47 7b 7a undefined1
61 77 52
7d 77 55
00404070 47 undefined147h [0] XREF[3]: Entry Point(*), exam:00401361(*),
exam:00401361(*)
00404071 7b undefined17Bh [1]
00404072 7a undefined17Ah [2]
00404073 61 undefined161h [3]
00404074 77 undefined177h [4]
00404075 52 undefined152h [5]
00404076 7d undefined17Dh [6]
00404077 77 undefined177h [7]
00404078 55 undefined155h [8]
00404079 7a undefined17Ah [9]
0040407a 7d undefined17Dh [10]
0040407b 72 undefined172h [11]
0040407c 7f undefined17Fh [12]
0040407d 32 undefined132h [13]
0040407e 32 undefined132h [14]
0040407f 32 undefined132h [15]
00404080 13 undefined113h [16]
So, let’s decrypt the above bytes:
>>> from pwn import xor
>>> xor(b'\x13', bytes((0x47, 0x7b, 0x7a, 0x61, 0x77, 0x52, 0x7d, 0x77, 0x55, 0x7a, 0x7d, 0x72, 0x7f, 0x32, 0x32, 0x32, 0x13)))
b'ThirdAndFinal!!!\x00'
And there we have all the passwords.
Dynamic analysis
Another approach is use GDB to perform a dynamic analysis on the program. We are going to set breakpoints at strcmp
in function exam
:
$ gdb -q license
Reading symbols from license...
(No debugging symbols found in license)
gef➤ p main
$1 = {<text variable, no debug info>} 0x401172 <main>
gef➤ disassemble exam
Dump of assembler code for function exam:
0x000000000040128a <+0>: push rbp
0x000000000040128b <+1>: mov rbp,rsp
0x000000000040128e <+4>: sub rsp,0x30
0x0000000000401292 <+8>: mov edi,0x402120
0x0000000000401297 <+13>: call 0x401050 <readline@plt>
0x000000000040129c <+18>: mov QWORD PTR [rbp-0x8],rax
0x00000000004012a0 <+22>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004012a4 <+26>: mov esi,0x402170
0x00000000004012a9 <+31>: mov rdi,rax
0x00000000004012ac <+34>: call 0x401060 <strcmp@plt>
0x00000000004012b1 <+39>: test eax,eax
0x00000000004012b3 <+41>: je 0x4012c9 <exam+63>
0x00000000004012b5 <+43>: mov edi,0x402182
0x00000000004012ba <+48>: call 0x401040 <puts@plt>
0x00000000004012bf <+53>: mov edi,0xffffffff
0x00000000004012c4 <+58>: call 0x401080 <exit@plt>
0x00000000004012c9 <+63>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004012cd <+67>: mov rdi,rax
0x00000000004012d0 <+70>: call 0x401030 <free@plt>
0x00000000004012d5 <+75>: mov QWORD PTR [rbp-0x14],0x0
0x00000000004012dd <+83>: mov DWORD PTR [rbp-0xc],0x0
0x00000000004012e4 <+90>: lea rax,[rbp-0x14]
0x00000000004012e8 <+94>: mov edx,0xb
0x00000000004012ed <+99>: mov esi,0x404060
0x00000000004012f2 <+104>: mov rdi,rax
0x00000000004012f5 <+107>: call 0x4011e1 <reverse>
0x00000000004012fa <+112>: mov edi,0x402198
0x00000000004012ff <+117>: call 0x401050 <readline@plt>
0x0000000000401304 <+122>: mov QWORD PTR [rbp-0x8],rax
0x0000000000401308 <+126>: lea rdx,[rbp-0x14]
0x000000000040130c <+130>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000401310 <+134>: mov rsi,rdx
0x0000000000401313 <+137>: mov rdi,rax
0x0000000000401316 <+140>: call 0x401060 <strcmp@plt>
0x000000000040131b <+145>: test eax,eax
0x000000000040131d <+147>: je 0x401333 <exam+169>
0x000000000040131f <+149>: mov edi,0x4021c8
0x0000000000401324 <+154>: call 0x401040 <puts@plt>
0x0000000000401329 <+159>: mov edi,0xffffffff
0x000000000040132e <+164>: call 0x401080 <exit@plt>
0x0000000000401333 <+169>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000401337 <+173>: mov rdi,rax
0x000000000040133a <+176>: call 0x401030 <free@plt>
0x000000000040133f <+181>: mov QWORD PTR [rbp-0x30],0x0
0x0000000000401347 <+189>: mov QWORD PTR [rbp-0x28],0x0
0x000000000040134f <+197>: mov BYTE PTR [rbp-0x20],0x0
0x0000000000401353 <+201>: lea rax,[rbp-0x30]
0x0000000000401357 <+205>: mov ecx,0x13
0x000000000040135c <+210>: mov edx,0x11
0x0000000000401361 <+215>: mov esi,0x404070
0x0000000000401366 <+220>: mov rdi,rax
0x0000000000401369 <+223>: call 0x401237 <xor>
0x000000000040136e <+228>: mov edi,0x4021e8
0x0000000000401373 <+233>: call 0x401050 <readline@plt>
0x0000000000401378 <+238>: mov QWORD PTR [rbp-0x8],rax
0x000000000040137c <+242>: lea rdx,[rbp-0x30]
0x0000000000401380 <+246>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000401384 <+250>: mov rsi,rdx
0x0000000000401387 <+253>: mov rdi,rax
0x000000000040138a <+256>: call 0x401060 <strcmp@plt>
0x000000000040138f <+261>: test eax,eax
0x0000000000401391 <+263>: je 0x4013a7 <exam+285>
0x0000000000401393 <+265>: mov edi,0x40222c
0x0000000000401398 <+270>: call 0x401040 <puts@plt>
0x000000000040139d <+275>: mov edi,0xffffffff
0x00000000004013a2 <+280>: call 0x401080 <exit@plt>
0x00000000004013a7 <+285>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004013ab <+289>: mov rdi,rax
0x00000000004013ae <+292>: call 0x401030 <free@plt>
0x00000000004013b3 <+297>: nop
0x00000000004013b4 <+298>: leave
0x00000000004013b5 <+299>: ret
End of assembler dump.
gef➤ break *exam+34
Breakpoint 1 at 0x4012ac
gef➤ break *exam+140
Breakpoint 2 at 0x401316
gef➤ break *exam+256
Breakpoint 3 at 0x40138a
gef➤ run
Starting program: ./license
So, you want to be a relic hunter?
First, you're going to need your license, and for that you need to pass the exam.
It's short, but it's not for the faint of heart. Are you up to the challenge?! (y/n)
y
Okay, first, a warmup - what's the first password? This one's not even hidden: asdf
Breakpoint 1, 0x00000000004012ac in exam ()
Now we are at the first breakpoint, so we can check the arguments to strcmp
($rdi
contains the pointer to the first string and $rsi
contains the pointer to the second string):
gef➤ x/s $rdi
0x41e020: "asdf"
gef➤ x/s $rsi
0x402170: "PasswordNumeroUno"
As can be seen, the second string is the expected password. We take note, set $rdi
to equal $rsi
and continue to the next breakpoint:
gef➤ set $rdi = $rsi
gef➤ continue
Continuing.
Getting harder - what's the second password? qwer
Breakpoint 2, 0x0000000000401316 in exam ()
Same here:
gef➤ x/s $rdi
0x41e020: "qwer"
gef➤ x/s $rsi
0x7fffffffe7dc: "P4ssw0rdTw0"
gef➤ set $rdi = $rsi
gef➤ continue
Continuing.
Your final test - give me the third, and most protected, password: zxcv
Breakpoint 3, 0x000000000040138a in exam ()
And same here:
gef➤ x/s $rdi
0x41e020: "zxcv"
gef➤ x/s $rsi
0x7fffffffe7c0: "ThirdAndFinal!!!"
gef➤ set $rdi = $rsi
gef➤ continue
Continuing.
Well done hunter - consider yourself certified!
[Inferior 1 (process 1457) exited normally]
gef➤
Alternative
The above approach can be done also with ltrace
, which is a program that traces all external function calls (namely, strcmp
, which comes from Glibc). Again, we can find the expected passwords by reading how strcmp
is being called:
$ ltrace ./license
puts("So, you want to be a relic hunte"...So, you want to be a relic hunter?
) = 35
puts("First, you're going to need your"...First, you're going to need your license, and for that you need to pass the exam.
) = 82
puts("It's short, but it's not for the"...It's short, but it's not for the faint of heart. Are you up to the challenge?! (y/n)
) = 85
getchar(0x7f2367fb47e0, 0x1f8c2a0, 0, 0x7f2367ed4077y
) = 121
readline("Okay, first, a warmup - what's t"...Okay, first, a warmup - what's the first password? This one's not even hidden: asdf
) = "asdf"
strcmp("asdf", "PasswordNumeroUno") = 17
puts("Not even close!"Not even close!
) = 16
exit(-1 <no return ...>
+++ exited (status 255) +++
$ ltrace ./license
puts("So, you want to be a relic hunte"...So, you want to be a relic hunter?
) = 35
puts("First, you're going to need your"...First, you're going to need your license, and for that you need to pass the exam.
) = 82
puts("It's short, but it's not for the"...It's short, but it's not for the faint of heart. Are you up to the challenge?! (y/n)
) = 85
getchar(0x7f0dd0de27e0, 0x5342a0, 0, 0x7f0dd0d02077y
) = 121
readline("Okay, first, a warmup - what's t"...Okay, first, a warmup - what's the first password? This one's not even hidden: PasswordNumeroUno
) = "PasswordNumeroUno"
strcmp("PasswordNumeroUno", "PasswordNumeroUno") = 0
free(0x54d230) = <void>
readline("Getting harder - what's the seco"...Getting harder - what's the second password? asdf
) = "asdf"
strcmp("asdf", "P4ssw0rdTw0") = 17
puts("You've got it all backwards..."You've got it all backwards...
) = 31
exit(-1 <no return ...>
+++ exited (status 255) +++
$ ltrace ./license
puts("So, you want to be a relic hunte"...So, you want to be a relic hunter?
) = 35
puts("First, you're going to need your"...First, you're going to need your license, and for that you need to pass the exam.
) = 82
puts("It's short, but it's not for the"...It's short, but it's not for the faint of heart. Are you up to the challenge?! (y/n)
) = 85
getchar(0x7fe58dc397e0, 0x10472a0, 0, 0x7fe58db59077y
) = 121
readline("Okay, first, a warmup - what's t"...Okay, first, a warmup - what's the first password? This one's not even hidden: PasswordNumeroUno
) = "PasswordNumeroUno"
strcmp("PasswordNumeroUno", "PasswordNumeroUno") = 0
free(0x1060230) = <void>
readline("Getting harder - what's the seco"...Getting harder - what's the second password? P4ssw0rdTw0
) = "P4ssw0rdTw0"
strcmp("P4ssw0rdTw0", "P4ssw0rdTw0") = 0
free(0x1060230) = <void>
readline("Your final test - give me the th"...Your final test - give me the third, and most protected, password: asdf
) = "asdf"
strcmp("asdf", "ThirdAndFinal!!!") = 13
puts("Failed at the final hurdle!"Failed at the final hurdle!
) = 28
exit(-1 <no return ...>
+++ exited (status 255) +++
Questionnaire
Obviously, the program succeeds when we enter the expected passwords:
$ ./license
So, you want to be a relic hunter?
First, you're going to need your license, and for that you need to pass the exam.
It's short, but it's not for the faint of heart. Are you up to the challenge?! (y/n)
y
Okay, first, a warmup - what's the first password? This one's not even hidden: PasswordNumeroUno
Getting harder - what's the second password? P4ssw0rdTw0
Your final test - give me the third, and most protected, password: ThirdAndFinal!!!
Well done hunter - consider yourself certified!
Now, let’s connect to the remote instance to answer some questions (all the needed information is above):
$ nc 167.71.129.184 31559
What is the file format of the executable?
> ELF
[+] Correct!
What is the CPU architecture of the executable?
> x86_64
[+] Correct!
What library is used to read lines for user answers? (`ldd` may help)
> libreadline.so.8
[+] Correct!
What is the address of the `main` function?
> 0x401172
[+] Correct!
How many calls to `puts` are there in `main`? (using a decompiler may help)
> 5
[+] Correct!
What is the first password?
> PasswordNumeroUno
[+] Correct!
What is the reversed form of the second password?
> 0wTdr0wss4P
[+] Correct!
What is the real second password?
> P4ssw0rdTw0
[+] Correct!
What is the XOR key used to encode the third password?
> 0x13
[+] Correct!
What is the third password?
> ThirdAndFinal!!!
[+] Correct!
Flag
Once all answers are correct, we will get the flag:
[+] Here is the flag: `HTB{l1c3ns3_4cquir3d-hunt1ng_t1m3!}`