< Home

Ok, you know the deal, we login onto the pwnable server and do

input2@pwnable:~$ ls -ls
total 24
 4 -r--r----- 1 input2_pwn root      55 Jun 30  2014 flag
16 -r-sr-x--- 1 input2_pwn input2 13250 Jun 30  2014 input
 4 -rw-r--r-- 1 root       root    1754 Jun 30  2014 input.c

Again skipping the input.c file and try-executing the input file

Seems like it is expecting some inputs and special types? Let us open the binary in Ghidra. When opening it it seems everything was written in the main function or the compiler just put it their for optimization purposes (yes, compilers will eliminate functions and just put them in-line as calls are not optimal. You see this a lot with for example get and set functions).

00400954    PUSH       RBP
00400955    MOV        RBP,RSP
00400958    SUB        RSP,0x70
0040095c    MOV        dword ptr [RBP + local_5c],EDI
0040095f    MOV        qword ptr [RBP + local_68],RSI
00400963    MOV        qword ptr [RBP + local_70],RDX
00400967    MOV        RAX,qword ptr FS:[0x28]
00400970    MOV        qword ptr [RBP + local_10],RAX
00400974    XOR        EAX,EAX
00400976    MOV        EDI=>s_Welcome_to_pwnable.kr_00400da0,s_Welcom   = "Welcome to pwnable.kr"
0040097b    CALL       puts                   int puts(char * __s)
00400980    MOV        EDI=>s_Let's_see_if_you_know_how_to_giv_00400d   = "Let's see if you know how to 
00400985    CALL       puts                   int puts(char * __s)
0040098a    MOV        EDI=>s_Just_give_me_correct_inputs_then_00400d   = "Just give me correct inputs t
0040098f    CALL       puts                   int puts(char * __s)
00400994    CMP        dword ptr [RBP + local_5c],0x64
00400998    JZ         LAB_004009a4
0040099a    MOV        EAX,0x0
0040099f    JMP        LAB_00400c9a
004009a4    MOV        RAX,qword ptr [RBP + local_68]
004009a8    ADD        RAX,0x208
004009ae    MOV        RAX,qword ptr [RAX]
004009b1    MOVZX      EAX,byte ptr [RAX]
004009b4    TEST       AL,AL
004009b6    JZ         LAB_004009c2
004009b8    MOV        EAX,0x0
004009bd    JMP        LAB_00400c9a
004009c2    MOV        RAX,qword ptr [RBP + local_68]
004009c6    ADD        RAX,0x210
004009cc    MOV        RAX,qword ptr [RAX]
004009cf    MOV        RDX,RAX
004009d2    MOV        EAX,DAT_00400e2a                    = 20h
004009d7    MOV        ECX,0x4
004009dc    MOV        RSI,RDX
004009df    MOV        RDI,RAX
004009e2    CMPSB.REPE RDI=>DAT_00400e2a,RSI               = 20h
004009e4    SETA       DL
004009e7    SETC       AL
004009ea    MOV        ECX,EDX
004009ec    SUB        CL,AL
004009ee    MOV        EAX,ECX
004009f0    MOVSX      EAX,AL
004009f3    TEST       EAX,EAX
004009f5    JZ         LAB_00400a01
004009f7    MOV        EAX,0x0
004009fc    JMP        LAB_00400c9a
00400a01    MOV        EDI=>s_Stage_1_clear!_00400e2e,s_Stage_1_clear   = "Stage 1 clear!"
00400a06    CALL       puts                   int puts(char * __s)
00400a0b    LEA        RAX=>local_18,[RBP + -0x10]
00400a0f    MOV        EDX,0x4
00400a14    MOV        RSI,RAX
00400a17    MOV        EDI,0x0
00400a1c    MOV        EAX,0x0
00400a21    CALL       read                   ssize_t read(int __fd, void * __
00400a26    LEA        RAX=>local_18,[RBP + -0x10]
00400a2a    MOV        EDX,0x4
00400a2f    MOV        ESI=>DAT_00400e3d,DAT_00400e3d
00400a34    MOV        RDI,RAX
00400a37    CALL       memcmp                 int memcmp(void * __s1, void * _
00400a3c    TEST       EAX,EAX
00400a3e    JZ         LAB_00400a4a
00400a40    MOV        EAX,0x0
00400a45    JMP        LAB_00400c9a
00400a4a    LEA        RAX=>local_18,[RBP + -0x10]
00400a4e    MOV        EDX,0x4
00400a53    MOV        RSI,RAX
00400a56    MOV        EDI,0x2
00400a5b    MOV        EAX,0x0
00400a60    CALL       read                   ssize_t read(int __fd, void * __
00400a65    LEA        RAX=>local_18,[RBP + -0x10]
00400a69    MOV        EDX,0x4
00400a6e    MOV        ESI=>DAT_00400e42,DAT_00400e42
00400a73    MOV        RDI,RAX
00400a76    CALL       memcmp                 int memcmp(void * __s1, void * _
00400a7b    TEST       EAX,EAX
00400a7d    JZ         LAB_00400a89
00400a7f    MOV        EAX,0x0
00400a84    JMP        LAB_00400c9a
00400a89    MOV        EDI=>s_Stage_2_clear!_00400e47,s_Stage_2_clear   = "Stage 2 clear!"
00400a8e    CALL       puts                   int puts(char * __s)
00400a93    MOV        EDI=>DAT_00400e56,DAT_00400e56      = DEh
00400a98    CALL       getenv                 char * getenv(char * __name)
00400a9d    MOV        EDX,DAT_00400e5b                    = CAh
00400aa2    MOV        ECX,0x5
00400aa7    MOV        RSI,RDX
00400aaa    MOV        RDI,RAX
00400aad    CMPSB.REPE RDI,RSI=>DAT_00400e5b
00400aaf    SETA       DL
00400ab2    SETC       AL
00400ab5    MOV        ECX,EDX
00400ab7    SUB        CL,AL
00400ab9    MOV        EAX,ECX
00400abb    MOVSX      EAX,AL
00400abe    TEST       EAX,EAX
00400ac0    JZ         LAB_00400acc
00400ac2    MOV        EAX,0x0
00400ac7    JMP        LAB_00400c9a
00400acc    MOV        EDI=>s_Stage_3_clear!_00400e60,s_Stage_3_clear   = "Stage 3 clear!"
00400ad1    CALL       puts                   int puts(char * __s)
00400ad6    MOV        EDX=>DAT_00400e6f,DAT_00400e6f      = 72h    r
00400adb    MOV        EAX,DAT_00400e71                    = 0Ah
00400ae0    MOV        RSI=>DAT_00400e6f,RDX               = 72h    r
00400ae3    MOV        RDI=>DAT_00400e71,RAX               = 0Ah
00400ae6    CALL       fopen                  FILE * fopen(char * __filename, 
00400aeb    MOV        qword ptr [RBP + local_50],RAX
00400aef    CMP        qword ptr [RBP + local_50],0x0
00400af4    JNZ        LAB_00400b00
00400af6    MOV        EAX,0x0
00400afb    JMP        LAB_00400c9a
00400b00    LEA        RAX=>local_18,[RBP + -0x10]
00400b04    MOV        RDX,qword ptr [RBP + local_50]
00400b08    MOV        RCX,RDX
00400b0b    MOV        EDX,0x1
00400b10    MOV        ESI,0x4
00400b15    MOV        RDI,RAX
00400b18    CALL       fread                  size_t fread(void * __ptr, size_
00400b1d    CMP        RAX,0x1
00400b21    JZ         LAB_00400b2d
00400b23    MOV        EAX,0x0
00400b28    JMP        LAB_00400c9a
00400b2d    LEA        RAX=>local_18,[RBP + -0x10]
00400b31    MOV        EDX,0x4
00400b36    MOV        ESI=>DAT_00400e73,DAT_00400e73
00400b3b    MOV        RDI,RAX
00400b3e    CALL       memcmp                 int memcmp(void * __s1, void * _
00400b43    TEST       EAX,EAX
00400b45    JZ         LAB_00400b51
00400b47    MOV        EAX,0x0
00400b4c    JMP        LAB_00400c9a
00400b51    MOV        RAX,qword ptr [RBP + local_50]
00400b55    MOV        RDI,RAX
00400b58    CALL       fclose                 int fclose(FILE * __stream)
00400b5d    MOV        EDI=>s_Stage_4_clear!_00400e78,s_Stage_4_clear   = "Stage 4 clear!"
00400b62    CALL       puts                   int puts(char * __s)
00400b67    MOV        EDX,0x0
00400b6c    MOV        ESI,0x1
00400b71    MOV        EDI,0x2
00400b76    CALL       socket                 int socket(int __domain, int __t
00400b7b    MOV        dword ptr [RBP + local_40],EAX
00400b7e    CMP        dword ptr [RBP + local_40],-0x1
00400b82    JNZ        LAB_00400b98
00400b84    MOV        EDI=>s_socket_error,_tell_admin_00400e87,s_soc   = "socket error, tell admin"
00400b89    CALL       puts                   int puts(char * __s)
00400b8e    MOV        EAX,0x0
00400b93    JMP        LAB_00400c9a
00400b98    MOV        word ptr [RBP + local_38],0x2
00400b9e    MOV        dword ptr [RBP + local_34],0x0
00400ba5    MOV        RAX,qword ptr [RBP + local_68]
00400ba9    ADD        RAX,0x218
00400baf    MOV        RAX,qword ptr [RAX]
00400bb2    MOV        RDI,RAX
00400bb5    CALL       atoi                   int atoi(char * __nptr)
00400bba    MOVZX      EAX,AX
00400bbd    MOV        EDI,EAX
00400bbf    CALL       htons                  uint16_t htons(uint16_t __hostsh
00400bc4    MOV        word ptr [RBP + local_36],AX
00400bc8    LEA        RCX=>local_38,[RBP + -0x30]
00400bcc    MOV        EAX,dword ptr [RBP + local_40]
00400bcf    MOV        EDX,0x10
00400bd4    MOV        RSI,RCX
00400bd7    MOV        EDI,EAX
00400bd9    CALL       bind                   int bind(int __fd, sockaddr * __
00400bde    TEST       EAX,EAX
00400be0    JNS        LAB_00400bf6
00400be2    MOV        EDI=>s_bind_error,_use_another_port_00400ea0,s   = "bind error, use another port"
00400be7    CALL       puts                   int puts(char * __s)
00400bec    MOV        EAX,0x1
00400bf1    JMP        LAB_00400c9a
00400bf6    MOV        EAX,dword ptr [RBP + local_40]
00400bf9    MOV        ESI,0x1
00400bfe    MOV        EDI,EAX
00400c00    CALL       listen                 int listen(int __fd, int __n)
00400c05    MOV        dword ptr [RBP + local_44],0x10
00400c0c    LEA        RDX=>local_44,[RBP + -0x3c]
00400c10    LEA        RCX=>local_28,[RBP + -0x20]
00400c14    MOV        EAX,dword ptr [RBP + local_40]
00400c17    MOV        RSI,RCX
00400c1a    MOV        EDI,EAX
00400c1c    CALL       accept                 int accept(int __fd, sockaddr * 
00400c21    MOV        dword ptr [RBP + local_3c],EAX
00400c24    CMP        dword ptr [RBP + local_3c],0x0
00400c28    JNS        LAB_00400c3b
00400c2a    MOV        EDI=>s_accept_error,_tell_admin_00400ebd,s_acc   = "accept error, tell admin"
00400c2f    CALL       puts                   int puts(char * __s)
00400c34    MOV        EAX,0x0
00400c39    JMP        LAB_00400c9a
00400c3b    LEA        RSI=>local_18,[RBP + -0x10]
00400c3f    MOV        EAX,dword ptr [RBP + local_3c]
00400c42    MOV        ECX,0x0
00400c47    MOV        EDX,0x4
00400c4c    MOV        EDI,EAX
00400c4e    CALL       recv                   ssize_t recv(int __fd, void * __
00400c53    CMP        RAX,0x4
00400c57    JZ         LAB_00400c60
00400c59    MOV        EAX,0x0
00400c5e    JMP        LAB_00400c9a
00400c60    LEA        RAX=>local_18,[RBP + -0x10]
00400c64    MOV        EDX,0x4
00400c69    MOV        ESI=>DAT_00400e56,DAT_00400e56      = DEh
00400c6e    MOV        RDI,RAX
00400c71    CALL       memcmp                 int memcmp(void * __s1, void * _
00400c76    TEST       EAX,EAX
00400c78    JZ         LAB_00400c81
00400c7a    MOV        EAX,0x0
00400c7f    JMP        LAB_00400c9a
00400c81    MOV        EDI=>s_Stage_5_clear!_00400ed6,s_Stage_5_clear   = "Stage 5 clear!"
00400c86    CALL       puts                   int puts(char * __s)
00400c8b    MOV        EDI=>s_/bin/cat_flag_00400ee5,s_/bin/cat_flag_   = "/bin/cat flag"
00400c90    CALL       system                 int system(char * __command)
00400c95    MOV        EAX,0x0
00400c9a    MOV        RSI,qword ptr [RBP + local_10]
00400c9e    XOR        RSI,qword ptr FS:[0x28]
00400ca7    JZ         LAB_00400cae
00400ca9    CALL       __stack_chk_fail                    undefined __stack_chk_fail()
00400cae    LEAVE
00400caf    RET

A diagonal look through it shows it has multiple stages, it also uses a lot of library functions. The best way to see how this is structured without the decompiler is by using the graph view.

Graph Diagram

What a nice stairway you have there!

Ok let’s use the graph viewer and analyze one block at a time

Block 1: Initialization

00400954    PUSH       RBP
00400955    MOV        RBP,RSP
00400958    SUB        RSP,0x70
0040095c    MOV        dword ptr [RBP + local_5c],EDI
0040095f    MOV        qword ptr [RBP + local_68],RSI
00400963    MOV        qword ptr [RBP + local_70],RDX
00400967    MOV        RAX,qword ptr FS:[0x28]
00400970    MOV        qword ptr [RBP + local_10],RAX
00400974    XOR        EAX,EAX
00400976    MOV        EDI=>s_Welcome_to_pwnable.kr_00400da0,s_Welcom   = "Welcome to pwnable.kr"
0040097b    CALL       puts                   int puts(char * __s)
00400980    MOV        EDI=>s_Let's_see_if_you_know_how_to_giv_00400d   = "Let's see if you know how to 
00400985    CALL       puts                   int puts(char * __s)
0040098a    MOV        EDI=>s_Just_give_me_correct_inputs_then_00400d   = "Just give me correct inputs t
0040098f    CALL       puts                   int puts(char * __s)
00400994    CMP        dword ptr [RBP + local_5c],0x64
00400998    JZ         LAB_004009a4

This is the introduction section, it outputs a bit of text, sets the stackoverflow check to local_10 and checks if local_5c is equal to 0x64/100d which comes from… EDI? I am going to take a guess and suspect that EDI is our param_1 as param_1 is an integer and we check at the start of a function against an integer. This function wants 99 parameters from us.

input2@pwnable:~$ ./input 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Welcome to pwnable.kr
Let's see if you know how to give input to program

Ok, still haven’t solved the first stage but let us check out block 2

Block 2: Element 65 is 0x0

004009a4    MOV        RAX,qword ptr [RBP + local_68]
004009a8    ADD        RAX,0x208
004009ae    MOV        RAX,qword ptr [RAX]
004009b1    MOVZX      EAX,byte ptr [RAX]
004009b4    TEST       AL,AL
004009b6    JZ         LAB_004009c2

Here we do another check, we move local_68 into RAX. When I see the next instruction I got the feeling that local_68 is param_2 (and that makes the most sense as nothing else really has set it except in the function prologue). So that is a char * *. When adding 0x208 to it in a context of 64 bit it means we want element 0x41 (0x208/0x8) (or decimal 65). Then we move that qword value to RAX (so where argv was pointing to) and MOVZX that byte to EAX. We TEST the lower end of (R/E)AX with itself which lets me think we are testing for 0x0 as the next part of our quest is a JumpZero. So we need to send an empty string on place 65 (actually 64 as argv first element is the program path)

Block 3: Stage 1 passed

004009c2    MOV        RAX,qword ptr [RBP + local_68]
004009c6    ADD        RAX,0x210
004009cc    MOV        RAX,qword ptr [RAX]
004009cf    MOV        RDX,RAX
004009d2    MOV        EAX,DAT_00400e2a                    = 20h
004009d7    MOV        ECX,0x4
004009dc    MOV        param_2,RDX
004009df    MOV        param_1,RAX
004009e2    CMPSB.REPE param_1=>DAT_00400e2a,param_2       = 20h
004009e4    SETA       DL
004009e7    SETC       AL
004009ea    MOV        ECX,EDX
004009ec    SUB        CL,AL
004009ee    MOV        EAX,ECX
004009f0    MOVSX      EAX,AL
004009f3    TEST       EAX,EAX
004009f5    JZ         LAB_00400a01

Alright back at accessing our param_2 and it seems we just want the next element (0x210 == 0x42 or 66). When looking over it I’ve got the feeling this is a strcmp. The weird part is that it checks for 4 elements in the string (or 3 we write + 0x0). Now here is where Ghidra naming makes it confusing. param_1 and param_2 are just memory locations so they can be re-used. That the code shows that param_1 is re-used for other stuff means that the value of param_1 will not be used anymore in the rest of the program.

When following the DAT_00400e2a we come at

00400e2a                 undefined1 20h
00400e2b                 ??         0Ah
00400e2c                 ??         0Dh

So it suspects a string \x20\x0A\x0D

Ok, so 99 arguments, HEX strings,… yeah I’m gonna need some Python script to do this for me. Sometimes you have a brainfart and you search something like how to pass a lot of arguments to a binary and instead of piping people just tell you “why not spawn the process?”. So after wasting a couple of minutes (ok not a couple), I did a redo with subprocess!

import subprocess
import os

stdin = os.pipe()
stderror = os.pipe()

args = ['X'] * 99
args[64] = ''
args[65] = "\x20\x0A\x0D"

pro = subprocess.Popen(["/home/input2/input"] + args,stdin=stdin,stderr=stderror)

I was a bit stuck at the 64 part, I was so focused on setting it to 0x0 that I forgot how a string works in C. A string is a memory part that is read until 0x0, so an empty string is immediately 0x0. When you enter 0x0 subprocess will block you and throw a ValueError as a 0x0 is not allowed in subprocess args.

When running this python code we get

Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :-)
Stage 1 clear!

Block 4: Write to STDIN

00400a01    MOV        EDI=>s_Stage_1_clear!_00400e2e,s_Stage_1_clear   = "Stage 1 clear!"
00400a06    CALL       puts                   int puts(char * __s)
00400a0b    LEA        RAX=>local_18,[RBP + -0x10]
00400a0f    MOV        EDX,0x4
00400a14    MOV        RSI,RAX
00400a17    MOV        EDI,0x0
00400a1c    MOV        EAX,0x0
00400a21    CALL       read                   ssize_t read(int __fd, void * __
00400a26    LEA        RAX=>local_18,[RBP + -0x10]
00400a2a    MOV        EDX,0x4
00400a2f    MOV        ESI=>DAT_00400e3d,DAT_00400e3d
00400a34    MOV        RDI,RAX
00400a37    CALL       memcmp                 int memcmp(void * __s1, void * _
00400a3c    TEST       EAX,EAX
00400a3e    JZ         LAB_00400a4a

Here we see the print out of that we cleared stage 1. After that we see the setup for the READ call. If we follow top to down we can see that we will call read(0, &local_18, 4). We maybe remember from the fd challenge that 0 is STDIN so it is expecting input from stdin. After that it seems we are going to test it again against a static value. The interesting part here is memcmp, which as it says itself “unlike strcmp, the function does not stop comparing after finding a null character.“. This is very interesting as DAT_00400e3d holds \x00\x0A\x00\xFF.

So we need to write \x00\x0A\x00\xFF to STDIN

Block 5: Write to STDERR

00400a4a                 LEA        RAX=>local_18,[RBP + -0x10]
00400a4e                 MOV        EDX,0x4
00400a53                 MOV        RSI,RAX
00400a56                 MOV        EDI,0x2
00400a5b                 MOV        EAX,0x0
00400a60                 CALL       read                                             
00400a65                 LEA        RAX=>local_18,[RBP + -0x10]
00400a69                 MOV        EDX,0x4
00400a6e                 MOV        ESI=>DAT_00400e42,DAT_00400e42
00400a73                 MOV        RDI,RAX
00400a76                 CALL       memcmp                                           
00400a7b                 TEST       EAX,EAX
00400a7d                 JZ         LAB_00400a89

Wait, haven’t we just done this? Oh, see EDI? It isn’t zero this time, it is 2 which means we must write to STDERR? Eh, is that possible? Let’s try it but eh. But first we must know what we must write. So let’s check what is behind DAT_00400e42

DAT_00400e42 = \x00\x0A\x02\xFF

Now I’ve editted my code to the following.

import subprocess
import os

stdin_r, stdin_w = os.pipe()
stderr_r, stderr_w = os.pipe()

args = ['X'] * 99
args[64] = ''
args[65] = "\x20\x0A\x0D"

process = subprocess.Popen(["/home/input2/input"] + args,stdin=stdin_r, stderr=stderr_r)

os.write(stdin_w, "\x00\x0A\x00\xFF")
os.write(stderr_w, "\x00\x0A\x02\xFF")

And to my surprise

Stage 2 clear

But if you think about it, why wouldn’t you be able to write to stderr from another program? They are just pipes. The computer doesn’t care how you use them, we as humans just said “STDIN is for input, STDOUT and STDERR is for output.”, doesn’t mean that the computer agrees.

Block 6: Think about the environment!

00400a89    MOV        EDI=>s_Stage_2_clear!_00400e47,s_Stage_2_clear   = "Stage 2 clear!"
00400a8e    CALL       puts                   int puts(char * __s)
00400a93    MOV        EDI=>DAT_00400e56,DAT_00400e56
00400a98    CALL       getenv                 char * getenv(char * __name)
00400a9d    MOV        EDX,DAT_00400e5b
00400aa2    MOV        ECX,0x5
00400aa7    MOV        RSI,RDX
00400aaa    MOV        RDI,RAX
00400aad    CMPSB.REPE RDI,RSI=>DAT_00400e5b
00400aaf    SETA       DL
00400ab2    SETC       AL
00400ab5    MOV        ECX,EDX
00400ab7    SUB        CL,AL
00400ab9    MOV        EAX,ECX
00400abb    MOVSX      EAX,AL
00400abe    TEST       EAX,EAX
00400ac0    JZ         LAB_00400acc

By looking at it diagonally it seems that now the code will do a string compare against an enviroment variable.

DAT_00400e56 = \xDE\xAD\xBE\xEF which is the enviroment variable, the value it must contain is DAT_00400e5b = \xCA\xFE\xBA\xBE

Editted our Python script to include these environment variable.

import subprocess
import os

stdin_r, stdin_w = os.pipe()
stderr_r, stderr_w = os.pipe()

args = ['X'] * 99
args[64] = ''
args[65] = "\x20\x0A\x0D"

process = subprocess.Popen(["/home/input2/input"] + args,stdin=stdin_r, stderr=stderr_r, env={"\xDE\xAD\xBE\xEF": "\xCA\xFE\xBA\xBE"})

os.write(stdin_w, "\x00\x0A\x00\xFF")
os.write(stderr_w, "\x00\x0A\x02\xFF")

Stage 3 clear

Block 7: The newline fopen

00400acc    MOV    EDI=>s_Stage_3_clear!_00400e60,s_Stage_3_clear   = "Stage 3 clear!"
00400ad1    CALL       puts           int puts(char * __s)
00400ad6    MOV    EDX=>DAT_00400e6f,DAT_00400e6f
00400adb    MOV    EAX,DAT_00400e71
00400ae0    MOV    RSI=>DAT_00400e6f,RDX
00400ae3    MOV    RDI=>DAT_00400e71,RAX
00400ae6    CALL       fopen          FILE * fopen(char * __filename, 
00400aeb    MOV    qword ptr [RBP + local_50],RAX
00400aef    CMP    qword ptr [RBP + local_50],0x0
00400af4    JNZ    LAB_00400b00

Ok, first we see the message we completed the previous part and then we see that the program is trying to open a file with fopen. Here is what is weird. The value in

DAT_00400e6f = r

DAT_00400e71 = “\x0A” or… newline

if I follow the instructions it seems we are trying to do

fopen("\0A", "r"). Which means read newline. (I checked this against the source code file because I thought I was doing something wrong)

Now the rest of the code looks like if it was able to open “” we can jump to the next block. So let us do that while we also wonder how we are actually going to make a newline file on a server weren’t we are are allowed to create files.

Block 8: Reading is cool kids

00400b00         LEA    RAX=>local_18,[RBP + -0x10]
00400b04         MOV    RDX,qword ptr [RBP + local_50]
00400b08         MOV    RCX,RDX
00400b0b         MOV    EDX,0x1
00400b10         MOV    ESI,0x4
00400b15         MOV    RDI,RAX
00400b18         CALL       fread
00400b1d         CMP    RAX,0x1
00400b21         JZ     LAB_00400b2d

Ok nothing special, just a call to fread to read our open file.

fread(buffer, 1, 4, filehandler)

Block 9: Checking what was in the file

00400b2d                 LEA        RAX=>local_18,[RBP + -0x10]
00400b31                 MOV        EDX,0x4
00400b36                 MOV        ESI=>DAT_00400e73,DAT_00400e73
00400b3b                 MOV        RDI,RAX
00400b3e                 CALL       memcmp                                           
00400b43                 TEST       EAX,EAX
00400b45                 JZ         LAB_00400b51

Ok, we can see another memcmp and it seems it is expecting 4 values from fread. Checking what is inside

DAT_00400e73 = \x00\x00\x00\x00. Now first I thought I was looking at the wrong place or that somewhere else this value still needed to be set. But when looking at the XREFs which showed no writes to these addresses and checking were in the memory map this address resides, I figured out, they really do expect a file with four zero bytes.

Read only data section

Now as I was completely clueless on how to create a file where you aren’t allowed to and by searching on Google I actually saw a part of the solution as not many people request to do fopen(“0a”). This is something I overread but we have access in /tmp to write stuff. So writing it to /tmp should work. Now I just changed the working directory of our subprocess (cwd) to our /tmp folder and tadaaaa

input2@pwnable:~$ mkdir /tmp/mldb
input2@pwnable:~$ ls -lsa /tmp/mldb
total 514636
     4 drwxrwxr-x     2 input2 input2      4096 Jan 29 05:28 .
514616 drwxrwx-wt 19618 root   root   526954496 Jan 29 05:28 ..
import subprocess
import os
with open("/tmp/mldb/\x0a", "w") as f:
    f.write("\x00\x00\x00\x00")

stdin_r, stdin_w = os.pipe()
stderr_r, stderr_w = os.pipe()

args = ['X'] * 99
args[64] = ''
args[65] = "\x20\x0A\x0D"

process = subprocess.Popen(["/home/input2/input"] + args,stdin=stdin_r, stderr=stderr_r, env={"\xDE\xAD\xBE\xEF": "\xCA\xFE\xBA\xBE"}, cwd="/tmp/mldb/")

os.write(stdin_w, "\x00\x0A\x00\xFF")
os.write(stderr_w, "\x00\x0A\x02\xFF")

Stage 4 clear

Block 10: Closing the file and… sockets

00400b51         MOV    RAX,qword ptr [RBP + local_50]
00400b55         MOV    RDI,RAX
00400b58         CALL       fclose                       int fclose(FILE * __stream)
00400b5d         MOV    EDI=>s_Stage_4_clear!_00400e78,s_Stage_4_clear   = "Stage 4 clear!"
00400b62         CALL       puts                         int puts(char * __s)
00400b67         MOV    EDX,0x0
00400b6c         MOV    ESI,0x1
00400b71         MOV    EDI,0x2
00400b76         CALL       socket                       int socket(int __domain, int __t
00400b7b         MOV    dword ptr [RBP + local_40],EAX
00400b7e         CMP    dword ptr [RBP + local_40],-0x1
00400b82         JNZ    LAB_00400b98

Ok, so here we see the code that told us we finished stage 4 and then at 0x00400b67 we see the initialisation of… socket. Let me just say, I have a love hate relationship with sockets. I find them quite complicated but still very fascinating! But if we fill in what we see this socket call is

socket(2, 1, 0); or as the documentation shows us

socket(AF_INET, SOCK_STREAM, 0)

After the socket call it just checks if the socket creation was succesful and we move to the next block

Block 11: Binding the socket

00400b98         MOV    word ptr [RBP + local_38],0x2
00400b9e         MOV    dword ptr [RBP + local_34],0x0
00400ba5         MOV    RAX,qword ptr [RBP + local_68]
00400ba9         ADD    RAX,0x218
00400baf         MOV    RAX,qword ptr [RAX]
00400bb2         MOV    RDI,RAX
00400bb5         CALL       atoi                         
00400bba         MOVZX      EAX,AX
00400bbd         MOV    EDI,EAX
00400bbf         CALL       htons                        
00400bc4         MOV    word ptr [RBP + local_36],AX
00400bc8         LEA    RCX=>local_38,[RBP + -0x30]
00400bcc         MOV    EAX,dword ptr [RBP + local_40]
00400bcf         MOV    EDX,0x10
00400bd4         MOV    RSI,RCX
00400bd7         MOV    EDI,EAX
00400bd9         CALL       bind                         int bind(int __fd, sockaddr * __
00400bde         TEST       EAX,EAX
00400be0         JNS    LAB_00400bf6

Ok here is some funny stuff happening but I see we are back at accessing parameters from argv (local_68), we convert string to number (atoi), we do htons which I’ve never heard of before but seems to do something for network stuff. and then we see bind. An educated guess tells me that it expects a port number to be given in the arguments were bind will listen onto.

0x218 / 0x8 = 0x43 (67 decimal) or in our parameter list 66

and bind(socket_fd, [struct address](https://beej.us/guide/bgnet/html/#structs), 0x10)

Here is where low level languages get confusing again. At high-level languages a lot of these socket creation stuff just happens behind the hood but here you don’t just say “#yolo listen on 0.0.0.0”, no it expects a struct where you define the family, the flags,…

But by looking at it, it seems that as the link of struct_address shows, this will just listen to all network interfaces (or 0.0.0.0 if you please) and with the help of htons it seems that we generate a 0:ourport. Let’s check if our idea is correct

import subprocess
import os

with open("/tmp/mldb/\x0a", "w") as f:
    f.write("\x00\x00\x00\x00")

stdin_r, stdin_w = os.pipe()
stderr_r, stderr_w = os.pipe()

args = ['X'] * 99
args[64] = ''
args[65] = "\x20\x0A\x0D"
args[66] = "3108"

process = subprocess.Popen(["/home/input2/input"] + args,stdin=stdin_r, stderr=stderr_r, env={"\xDE\xAD\xBE\xEF": "\xCA\xFE\xBA\xBE"}, cwd="/tmp/mldb/")

os.write(stdin_w, "\x00\x0A\x00\xFF")
os.write(stderr_w, "\x00\x0A\x02\xFF")

Now to test it (open another ssh session)

input2@pwnable:~$ nc -v 0.0.0.0 3108

nc: connect to 0.0.0.0 port 3108 (tcp) failed: Connection refused

input2@pwnable:~$ nc -v 0.0.0.0 3108

Connection to 0.0.0.0 3108 port [tcp/*] succeeded!

Fuck yeah. So now it probably expects us to write stuff into the socket. I must say, I still am not 100% sure what htons did but I’ll test it out later by coding a bit in C with sockets (-kill me?-). Sometimes reverse engineering is just making assumptions and hoping they are correct, if they are not, just don’t write them down in your blogpost so you look much smarter.

Block 12: Socket created and binded now listening!

00400bf6         MOV    EAX,dword ptr [RBP + local_40]
00400bf9         MOV    ESI,0x1
00400bfe         MOV    EDI,EAX
00400c00         CALL       listen                       int listen(int __fd, int __n)
00400c05         MOV    dword ptr [RBP + local_44],0x10
00400c0c         LEA    RDX=>local_44,[RBP + -0x3c]
00400c10         LEA    RCX=>local_28,[RBP + -0x20]
00400c14         MOV    EAX,dword ptr [RBP + local_40]
00400c17         MOV    RSI,RCX
00400c1a         MOV    EDI,EAX
00400c1c         CALL       accept                       int accept(int __fd, sockaddr * 
00400c21         MOV    dword ptr [RBP + local_3c],EAX
00400c24         CMP    dword ptr [RBP + local_3c],0x0
00400c28         JNS    LAB_00400c3b

Ok, so a quick glare, this is just a socket listening for new incomming connections and accepting the connections. Not gonna waste too much time here but do note, as this is not spawning new threads or doing anything fancy, it just allows one connection as we see no indication that accept will run again. (Graph viewer also doesn’t show any returning calls to accept)

Block 13: recv data from connection

00400c3b         LEA    RSI=>local_18,[RBP + -0x10]
00400c3f         MOV    EAX,dword ptr [RBP + local_3c]
00400c42         MOV    ECX,0x0
00400c47         MOV    EDX,0x4
00400c4c         MOV    EDI,EAX
00400c4e         CALL       recv                         ssize_t recv(int __fd, void * __
00400c53         CMP    RAX,0x4

Here is the good stuff, recv. This will wait for input of the user and if I make an educated guess it looks like

recv(local_3c <– connection_fd, local_18 <– buffer, 4, 0)

So we are expecting a 4 byte input

Block 14: Comparing the socket data

00400c60         LEA    RAX=>local_18,[RBP + -0x10]
00400c64         MOV    EDX,0x4
00400c69         MOV    ESI=>DAT_00400e56,DAT_00400e56           = DEh
00400c6e         MOV    RDI,RAX
00400c71         CALL       memcmp                       int memcmp(void * __s1, void * _
00400c76         TEST       EAX,EAX
00400c78         JZ     LAB_00400c81

We’ve seen this in many previous blocks. Let us just check what is in

DAT_00400e56 = \xDE\xAD\xBE\xEF

Oh boi another deadbeef!

Let’s edit my Python script

import subprocess
import os
import socket
import time

with open("/tmp/mldb/\x0a", "w") as f:
    f.write("\x00\x00\x00\x00")

stdin_r, stdin_w = os.pipe()
stderr_r, stderr_w = os.pipe()

args = ['X'] * 99
args[64] = ''
args[65] = "\x20\x0A\x0D"
args[66] = "3108"

process = subprocess.Popen(["/home/input2/input"] + args,stdin=stdin_r, stderr=stderr_r, env={"\xDE\xAD\xBE\xEF": "\xCA\xFE\xBA\xBE"}, cwd="/tmp/mldb/")

os.write(stdin_w, "\x00\x0A\x00\xFF")
os.write(stderr_w, "\x00\x0A\x02\xFF")

time.sleep(3)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("localhost", 3108))
s.sendall('\xDE\xAD\xBE\xEF')
s.close()

Take note, this is Python 2 code, sockets and os PIPES changed a bit in Python 3. I’ve also added a sleep as sockets are not really super instant so a race condition could create a problem.

Stage 5 cleared

Final block 15!

00400c81         MOV    EDI=>s_Stage_5_clear!_00400ed6,s_Stage_5_clear   = "Stage 5 clear!"
00400c86         CALL       puts                         int puts(char * __s)
00400c8b         MOV    EDI=>s_/bin/cat_flag_00400ee5,s_/bin/cat_flag_   = "/bin/cat flag"
00400c90         CALL       system                       int system(char * __command)
00400c95         MOV    EAX,0x0

In this block we see our stage 5 clear message and after that a call to show us the flag. Now, in my output I didn’t see any flag output, what I got was… nothing?

After scratching my head and looking over my code I saw my flaw. I had set cwd to /tmp/mldb. Which meant that /bin/cat flag would translate to /bin/cat /tmp/mldb/flag.

Good thing something as symbolic links exists! (ln doesn’t suddenly give you more permissions, it is purely to be seen as a redirect!)

ln -s /home/input2/flag /tmp/mldb/flag

And let’s execute my Python code again

Challenge cleared!

Woohoo got the passphrase! When reading this post you’ll notice my explanation got shorter and shorter at the end, this was mostly as it was always the same. From somewhere an input would be taken and compared to a static given in the program. Also, it just starts to feel more natural to see what the code will do. Looks like doing these challenges with pure static analysis is paying off.

Also, don’t be discouraged. I also look up a lot on the internet during these challenges. It is not like my first search term is the perfect hit. There is no shame in looking at other people’s information or when you are stuck a quick glance at the source code file!

< Home