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
MOV dword ptr [RBP + local_5c],EDI
0040095c MOV qword ptr [RBP + local_68],RSI
0040095f 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"
CALL puts int puts(char * __s)
0040097b 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)
MOV EDI=>s_Just_give_me_correct_inputs_then_00400d = "Just give me correct inputs t
0040098a CALL puts int puts(char * __s)
0040098f 00400994 CMP dword ptr [RBP + local_5c],0x64
00400998 JZ LAB_004009a4
MOV EAX,0x0
0040099a JMP LAB_00400c9a
0040099f MOV RAX,qword ptr [RBP + local_68]
004009a4 ADD RAX,0x208
004009a8 MOV RAX,qword ptr [RAX]
004009ae MOVZX EAX,byte ptr [RAX]
004009b1 TEST AL,AL
004009b4 JZ LAB_004009c2
004009b6 MOV EAX,0x0
004009b8 JMP LAB_00400c9a
004009bd MOV RAX,qword ptr [RBP + local_68]
004009c2 ADD RAX,0x210
004009c6 MOV RAX,qword ptr [RAX]
004009cc MOV RDX,RAX
004009cf MOV EAX,DAT_00400e2a = 20h
004009d2 MOV ECX,0x4
004009d7 MOV RSI,RDX
004009dc MOV RDI,RAX
004009df 004009e2 CMPSB.REPE RDI=>DAT_00400e2a,RSI = 20h
004009e4 SETA DL
004009e7 SETC AL
MOV ECX,EDX
004009ea SUB CL,AL
004009ec MOV EAX,ECX
004009ee MOVSX EAX,AL
004009f0 TEST EAX,EAX
004009f3 JZ LAB_00400a01
004009f5 MOV EAX,0x0
004009f7 JMP LAB_00400c9a
004009fc MOV EDI=>s_Stage_1_clear!_00400e2e,s_Stage_1_clear = "Stage 1 clear!"
00400a01 CALL puts int puts(char * __s)
00400a06 LEA RAX=>local_18,[RBP + -0x10]
00400a0b MOV EDX,0x4
00400a0f MOV RSI,RAX
00400a14 MOV EDI,0x0
00400a17 MOV EAX,0x0
00400a1c CALL read ssize_t read(int __fd, void * __
00400a21 LEA RAX=>local_18,[RBP + -0x10]
00400a26 MOV EDX,0x4
00400a2a MOV ESI=>DAT_00400e3d,DAT_00400e3d
00400a2f MOV RDI,RAX
00400a34 CALL memcmp int memcmp(void * __s1, void * _
00400a37 TEST EAX,EAX
00400a3c JZ LAB_00400a4a
00400a3e MOV EAX,0x0
00400a40 JMP LAB_00400c9a
00400a45 LEA RAX=>local_18,[RBP + -0x10]
00400a4a MOV EDX,0x4
00400a4e MOV RSI,RAX
00400a53 MOV EDI,0x2
00400a56 MOV EAX,0x0
00400a5b CALL read ssize_t read(int __fd, void * __
00400a60 LEA RAX=>local_18,[RBP + -0x10]
00400a65 MOV EDX,0x4
00400a69 MOV ESI=>DAT_00400e42,DAT_00400e42
00400a6e MOV RDI,RAX
00400a73 CALL memcmp int memcmp(void * __s1, void * _
00400a76 TEST EAX,EAX
00400a7b JZ LAB_00400a89
00400a7d MOV EAX,0x0
00400a7f JMP LAB_00400c9a
00400a84 MOV EDI=>s_Stage_2_clear!_00400e47,s_Stage_2_clear = "Stage 2 clear!"
00400a89 CALL puts int puts(char * __s)
00400a8e MOV EDI=>DAT_00400e56,DAT_00400e56 = DEh
00400a93 CALL getenv char * getenv(char * __name)
00400a98 MOV EDX,DAT_00400e5b = CAh
00400a9d MOV ECX,0x5
00400aa2 MOV RSI,RDX
00400aa7 MOV RDI,RAX
00400aaa CMPSB.REPE RDI,RSI=>DAT_00400e5b
00400aad SETA DL
00400aaf SETC AL
00400ab2 MOV ECX,EDX
00400ab5 SUB CL,AL
00400ab7 MOV EAX,ECX
00400ab9 MOVSX EAX,AL
00400abb TEST EAX,EAX
00400abe JZ LAB_00400acc
00400ac0 MOV EAX,0x0
00400ac2 JMP LAB_00400c9a
00400ac7 MOV EDI=>s_Stage_3_clear!_00400e60,s_Stage_3_clear = "Stage 3 clear!"
00400acc CALL puts int puts(char * __s)
00400ad1 MOV EDX=>DAT_00400e6f,DAT_00400e6f = 72h r
00400ad6 MOV EAX,DAT_00400e71 = 0Ah
00400adb MOV RSI=>DAT_00400e6f,RDX = 72h r
00400ae0 MOV RDI=>DAT_00400e71,RAX = 0Ah
00400ae3 CALL fopen FILE * fopen(char * __filename,
00400ae6 MOV qword ptr [RBP + local_50],RAX
00400aeb CMP qword ptr [RBP + local_50],0x0
00400aef JNZ LAB_00400b00
00400af4 MOV EAX,0x0
00400af6 JMP LAB_00400c9a
00400afb LEA RAX=>local_18,[RBP + -0x10]
00400b00 MOV RDX,qword ptr [RBP + local_50]
00400b04 MOV RCX,RDX
00400b08 MOV EDX,0x1
00400b0b MOV ESI,0x4
00400b10 MOV RDI,RAX
00400b15 CALL fread size_t fread(void * __ptr, size_
00400b18 CMP RAX,0x1
00400b1d JZ LAB_00400b2d
00400b21 MOV EAX,0x0
00400b23 JMP LAB_00400c9a
00400b28 LEA RAX=>local_18,[RBP + -0x10]
00400b2d MOV EDX,0x4
00400b31 MOV ESI=>DAT_00400e73,DAT_00400e73
00400b36 MOV RDI,RAX
00400b3b CALL memcmp int memcmp(void * __s1, void * _
00400b3e TEST EAX,EAX
00400b43 JZ LAB_00400b51
00400b45 MOV EAX,0x0
00400b47 JMP LAB_00400c9a
00400b4c MOV RAX,qword ptr [RBP + local_50]
00400b51 MOV RDI,RAX
00400b55 CALL fclose int fclose(FILE * __stream)
00400b58 MOV EDI=>s_Stage_4_clear!_00400e78,s_Stage_4_clear = "Stage 4 clear!"
00400b5d CALL puts int puts(char * __s)
00400b62 MOV EDX,0x0
00400b67 MOV ESI,0x1
00400b6c MOV EDI,0x2
00400b71 CALL socket int socket(int __domain, int __t
00400b76 MOV dword ptr [RBP + local_40],EAX
00400b7b CMP dword ptr [RBP + local_40],-0x1
00400b7e JNZ LAB_00400b98
00400b82 MOV EDI=>s_socket_error,_tell_admin_00400e87,s_soc = "socket error, tell admin"
00400b84 CALL puts int puts(char * __s)
00400b89 MOV EAX,0x0
00400b8e JMP LAB_00400c9a
00400b93 MOV word ptr [RBP + local_38],0x2
00400b98 MOV dword ptr [RBP + local_34],0x0
00400b9e MOV RAX,qword ptr [RBP + local_68]
00400ba5 ADD RAX,0x218
00400ba9 MOV RAX,qword ptr [RAX]
00400baf MOV RDI,RAX
00400bb2 CALL atoi int atoi(char * __nptr)
00400bb5 MOVZX EAX,AX
00400bba MOV EDI,EAX
00400bbd CALL htons uint16_t htons(uint16_t __hostsh
00400bbf MOV word ptr [RBP + local_36],AX
00400bc4 LEA RCX=>local_38,[RBP + -0x30]
00400bc8 MOV EAX,dword ptr [RBP + local_40]
00400bcc MOV EDX,0x10
00400bcf MOV RSI,RCX
00400bd4 MOV EDI,EAX
00400bd7 CALL bind int bind(int __fd, sockaddr * __
00400bd9 TEST EAX,EAX
00400bde JNS LAB_00400bf6
00400be0 MOV EDI=>s_bind_error,_use_another_port_00400ea0,s = "bind error, use another port"
00400be2 CALL puts int puts(char * __s)
00400be7 MOV EAX,0x1
00400bec JMP LAB_00400c9a
00400bf1 MOV EAX,dword ptr [RBP + local_40]
00400bf6 MOV ESI,0x1
00400bf9 MOV EDI,EAX
00400bfe CALL listen int listen(int __fd, int __n)
00400c00 MOV dword ptr [RBP + local_44],0x10
00400c05 LEA RDX=>local_44,[RBP + -0x3c]
00400c0c LEA RCX=>local_28,[RBP + -0x20]
00400c10 MOV EAX,dword ptr [RBP + local_40]
00400c14 MOV RSI,RCX
00400c17 MOV EDI,EAX
00400c1a CALL accept int accept(int __fd, sockaddr *
00400c1c MOV dword ptr [RBP + local_3c],EAX
00400c21 CMP dword ptr [RBP + local_3c],0x0
00400c24 JNS LAB_00400c3b
00400c28 MOV EDI=>s_accept_error,_tell_admin_00400ebd,s_acc = "accept error, tell admin"
00400c2a CALL puts int puts(char * __s)
00400c2f MOV EAX,0x0
00400c34 JMP LAB_00400c9a
00400c39 LEA RSI=>local_18,[RBP + -0x10]
00400c3b MOV EAX,dword ptr [RBP + local_3c]
00400c3f MOV ECX,0x0
00400c42 MOV EDX,0x4
00400c47 MOV EDI,EAX
00400c4c CALL recv ssize_t recv(int __fd, void * __
00400c4e CMP RAX,0x4
00400c53 JZ LAB_00400c60
00400c57 MOV EAX,0x0
00400c59 JMP LAB_00400c9a
00400c5e LEA RAX=>local_18,[RBP + -0x10]
00400c60 MOV EDX,0x4
00400c64 MOV ESI=>DAT_00400e56,DAT_00400e56 = DEh
00400c69 MOV RDI,RAX
00400c6e CALL memcmp int memcmp(void * __s1, void * _
00400c71 TEST EAX,EAX
00400c76 JZ LAB_00400c81
00400c78 MOV EAX,0x0
00400c7a JMP LAB_00400c9a
00400c7f MOV EDI=>s_Stage_5_clear!_00400ed6,s_Stage_5_clear = "Stage 5 clear!"
00400c81 CALL puts int puts(char * __s)
00400c86 MOV EDI=>s_/bin/cat_flag_00400ee5,s_/bin/cat_flag_ = "/bin/cat flag"
00400c8b CALL system int system(char * __command)
00400c90 MOV EAX,0x0
00400c95 MOV RSI,qword ptr [RBP + local_10]
00400c9a XOR RSI,qword ptr FS:[0x28]
00400c9e JZ LAB_00400cae
00400ca7 CALL __stack_chk_fail undefined __stack_chk_fail()
00400ca9 LEAVE
00400cae RET 00400caf
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.
What a nice stairway you have there!
Ok let’s use the graph viewer and analyze one block at a time
00400954 PUSH RBP
00400955 MOV RBP,RSP
00400958 SUB RSP,0x70
MOV dword ptr [RBP + local_5c],EDI
0040095c MOV qword ptr [RBP + local_68],RSI
0040095f 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"
CALL puts int puts(char * __s)
0040097b 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)
MOV EDI=>s_Just_give_me_correct_inputs_then_00400d = "Just give me correct inputs t
0040098a CALL puts int puts(char * __s)
0040098f 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
MOV RAX,qword ptr [RBP + local_68]
004009a4 ADD RAX,0x208
004009a8 MOV RAX,qword ptr [RAX]
004009ae MOVZX EAX,byte ptr [RAX]
004009b1 TEST AL,AL
004009b4 JZ LAB_004009c2 004009b6
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)
MOV RAX,qword ptr [RBP + local_68]
004009c2 ADD RAX,0x210
004009c6 MOV RAX,qword ptr [RAX]
004009cc MOV RDX,RAX
004009cf MOV EAX,DAT_00400e2a = 20h
004009d2 MOV ECX,0x4
004009d7 MOV param_2,RDX
004009dc MOV param_1,RAX
004009df 004009e2 CMPSB.REPE param_1=>DAT_00400e2a,param_2 = 20h
004009e4 SETA DL
004009e7 SETC AL
MOV ECX,EDX
004009ea SUB CL,AL
004009ec MOV EAX,ECX
004009ee MOVSX EAX,AL
004009f0 TEST EAX,EAX
004009f3 JZ LAB_00400a01 004009f5
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
= os.pipe()
stdin = os.pipe()
stderror
= ['X'] * 99
args 64] = ''
args[65] = "\x20\x0A\x0D"
args[
= subprocess.Popen(["/home/input2/input"] + args,stdin=stdin,stderr=stderror) pro
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!
MOV EDI=>s_Stage_1_clear!_00400e2e,s_Stage_1_clear = "Stage 1 clear!"
00400a01 CALL puts int puts(char * __s)
00400a06 LEA RAX=>local_18,[RBP + -0x10]
00400a0b MOV EDX,0x4
00400a0f MOV RSI,RAX
00400a14 MOV EDI,0x0
00400a17 MOV EAX,0x0
00400a1c CALL read ssize_t read(int __fd, void * __
00400a21 LEA RAX=>local_18,[RBP + -0x10]
00400a26 MOV EDX,0x4
00400a2a MOV ESI=>DAT_00400e3d,DAT_00400e3d
00400a2f MOV RDI,RAX
00400a34 CALL memcmp int memcmp(void * __s1, void * _
00400a37 TEST EAX,EAX
00400a3c JZ LAB_00400a4a 00400a3e
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
LEA RAX=>local_18,[RBP + -0x10]
00400a4a MOV EDX,0x4
00400a4e MOV RSI,RAX
00400a53 MOV EDI,0x2
00400a56 MOV EAX,0x0
00400a5b CALL read
00400a60 LEA RAX=>local_18,[RBP + -0x10]
00400a65 MOV EDX,0x4
00400a69 MOV ESI=>DAT_00400e42,DAT_00400e42
00400a6e MOV RDI,RAX
00400a73 CALL memcmp
00400a76 TEST EAX,EAX
00400a7b JZ LAB_00400a89 00400a7d
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
= os.pipe()
stdin_r, stdin_w = os.pipe()
stderr_r, stderr_w
= ['X'] * 99
args 64] = ''
args[65] = "\x20\x0A\x0D"
args[
= subprocess.Popen(["/home/input2/input"] + args,stdin=stdin_r, stderr=stderr_r)
process
"\x00\x0A\x00\xFF")
os.write(stdin_w, "\x00\x0A\x02\xFF") os.write(stderr_w,
And to my surprise
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.
MOV EDI=>s_Stage_2_clear!_00400e47,s_Stage_2_clear = "Stage 2 clear!"
00400a89 CALL puts int puts(char * __s)
00400a8e MOV EDI=>DAT_00400e56,DAT_00400e56
00400a93 CALL getenv char * getenv(char * __name)
00400a98 MOV EDX,DAT_00400e5b
00400a9d MOV ECX,0x5
00400aa2 MOV RSI,RDX
00400aa7 MOV RDI,RAX
00400aaa CMPSB.REPE RDI,RSI=>DAT_00400e5b
00400aad SETA DL
00400aaf SETC AL
00400ab2 MOV ECX,EDX
00400ab5 SUB CL,AL
00400ab7 MOV EAX,ECX
00400ab9 MOVSX EAX,AL
00400abb TEST EAX,EAX
00400abe JZ LAB_00400acc 00400ac0
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
= os.pipe()
stdin_r, stdin_w = os.pipe()
stderr_r, stderr_w
= ['X'] * 99
args 64] = ''
args[65] = "\x20\x0A\x0D"
args[
= subprocess.Popen(["/home/input2/input"] + args,stdin=stdin_r, stderr=stderr_r, env={"\xDE\xAD\xBE\xEF": "\xCA\xFE\xBA\xBE"})
process
"\x00\x0A\x00\xFF")
os.write(stdin_w, "\x00\x0A\x02\xFF") os.write(stderr_w,
MOV EDI=>s_Stage_3_clear!_00400e60,s_Stage_3_clear = "Stage 3 clear!"
00400acc CALL puts int puts(char * __s)
00400ad1 MOV EDX=>DAT_00400e6f,DAT_00400e6f
00400ad6 MOV EAX,DAT_00400e71
00400adb MOV RSI=>DAT_00400e6f,RDX
00400ae0 MOV RDI=>DAT_00400e71,RAX
00400ae3 CALL fopen FILE * fopen(char * __filename,
00400ae6 MOV qword ptr [RBP + local_50],RAX
00400aeb CMP qword ptr [RBP + local_50],0x0
00400aef JNZ LAB_00400b00 00400af4
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.
LEA RAX=>local_18,[RBP + -0x10]
00400b00 MOV RDX,qword ptr [RBP + local_50]
00400b04 MOV RCX,RDX
00400b08 MOV EDX,0x1
00400b0b MOV ESI,0x4
00400b10 MOV RDI,RAX
00400b15 CALL fread
00400b18 CMP RAX,0x1
00400b1d JZ LAB_00400b2d 00400b21
Ok nothing special, just a call to fread to read our open file.
fread(buffer, 1, 4, filehandler)
LEA RAX=>local_18,[RBP + -0x10]
00400b2d MOV EDX,0x4
00400b31 MOV ESI=>DAT_00400e73,DAT_00400e73
00400b36 MOV RDI,RAX
00400b3b CALL memcmp
00400b3e TEST EAX,EAX
00400b43 JZ LAB_00400b51 00400b45
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.
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:
"\x00\x00\x00\x00")
f.write(
= os.pipe()
stdin_r, stdin_w = os.pipe()
stderr_r, stderr_w
= ['X'] * 99
args 64] = ''
args[65] = "\x20\x0A\x0D"
args[
= subprocess.Popen(["/home/input2/input"] + args,stdin=stdin_r, stderr=stderr_r, env={"\xDE\xAD\xBE\xEF": "\xCA\xFE\xBA\xBE"}, cwd="/tmp/mldb/")
process
"\x00\x0A\x00\xFF")
os.write(stdin_w, "\x00\x0A\x02\xFF") os.write(stderr_w,
MOV RAX,qword ptr [RBP + local_50]
00400b51 MOV RDI,RAX
00400b55 CALL fclose int fclose(FILE * __stream)
00400b58 MOV EDI=>s_Stage_4_clear!_00400e78,s_Stage_4_clear = "Stage 4 clear!"
00400b5d CALL puts int puts(char * __s)
00400b62 MOV EDX,0x0
00400b67 MOV ESI,0x1
00400b6c MOV EDI,0x2
00400b71 CALL socket int socket(int __domain, int __t
00400b76 MOV dword ptr [RBP + local_40],EAX
00400b7b CMP dword ptr [RBP + local_40],-0x1
00400b7e JNZ LAB_00400b98 00400b82
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
MOV word ptr [RBP + local_38],0x2
00400b98 MOV dword ptr [RBP + local_34],0x0
00400b9e MOV RAX,qword ptr [RBP + local_68]
00400ba5 ADD RAX,0x218
00400ba9 MOV RAX,qword ptr [RAX]
00400baf MOV RDI,RAX
00400bb2 CALL atoi
00400bb5 MOVZX EAX,AX
00400bba MOV EDI,EAX
00400bbd CALL htons
00400bbf MOV word ptr [RBP + local_36],AX
00400bc4 LEA RCX=>local_38,[RBP + -0x30]
00400bc8 MOV EAX,dword ptr [RBP + local_40]
00400bcc MOV EDX,0x10
00400bcf MOV RSI,RCX
00400bd4 MOV EDI,EAX
00400bd7 CALL bind int bind(int __fd, sockaddr * __
00400bd9 TEST EAX,EAX
00400bde JNS LAB_00400bf6 00400be0
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:
"\x00\x00\x00\x00")
f.write(
= os.pipe()
stdin_r, stdin_w = os.pipe()
stderr_r, stderr_w
= ['X'] * 99
args 64] = ''
args[65] = "\x20\x0A\x0D"
args[66] = "3108"
args[
= subprocess.Popen(["/home/input2/input"] + args,stdin=stdin_r, stderr=stderr_r, env={"\xDE\xAD\xBE\xEF": "\xCA\xFE\xBA\xBE"}, cwd="/tmp/mldb/")
process
"\x00\x0A\x00\xFF")
os.write(stdin_w, "\x00\x0A\x02\xFF") os.write(stderr_w,
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.
MOV EAX,dword ptr [RBP + local_40]
00400bf6 MOV ESI,0x1
00400bf9 MOV EDI,EAX
00400bfe CALL listen int listen(int __fd, int __n)
00400c00 MOV dword ptr [RBP + local_44],0x10
00400c05 LEA RDX=>local_44,[RBP + -0x3c]
00400c0c LEA RCX=>local_28,[RBP + -0x20]
00400c10 MOV EAX,dword ptr [RBP + local_40]
00400c14 MOV RSI,RCX
00400c17 MOV EDI,EAX
00400c1a CALL accept int accept(int __fd, sockaddr *
00400c1c MOV dword ptr [RBP + local_3c],EAX
00400c21 CMP dword ptr [RBP + local_3c],0x0
00400c24 JNS LAB_00400c3b 00400c28
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)
LEA RSI=>local_18,[RBP + -0x10]
00400c3b MOV EAX,dword ptr [RBP + local_3c]
00400c3f MOV ECX,0x0
00400c42 MOV EDX,0x4
00400c47 MOV EDI,EAX
00400c4c CALL recv ssize_t recv(int __fd, void * __
00400c4e CMP RAX,0x4 00400c53
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
LEA RAX=>local_18,[RBP + -0x10]
00400c60 MOV EDX,0x4
00400c64 MOV ESI=>DAT_00400e56,DAT_00400e56 = DEh
00400c69 MOV RDI,RAX
00400c6e CALL memcmp int memcmp(void * __s1, void * _
00400c71 TEST EAX,EAX
00400c76 JZ LAB_00400c81 00400c78
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:
"\x00\x00\x00\x00")
f.write(
= os.pipe()
stdin_r, stdin_w = os.pipe()
stderr_r, stderr_w
= ['X'] * 99
args 64] = ''
args[65] = "\x20\x0A\x0D"
args[66] = "3108"
args[
= subprocess.Popen(["/home/input2/input"] + args,stdin=stdin_r, stderr=stderr_r, env={"\xDE\xAD\xBE\xEF": "\xCA\xFE\xBA\xBE"}, cwd="/tmp/mldb/")
process
"\x00\x0A\x00\xFF")
os.write(stdin_w, "\x00\x0A\x02\xFF")
os.write(stderr_w,
3)
time.sleep(
= socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s connect(("localhost", 3108))
s.'\xDE\xAD\xBE\xEF')
s.sendall( 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.
MOV EDI=>s_Stage_5_clear!_00400ed6,s_Stage_5_clear = "Stage 5 clear!"
00400c81 CALL puts int puts(char * __s)
00400c86 MOV EDI=>s_/bin/cat_flag_00400ee5,s_/bin/cat_flag_ = "/bin/cat flag"
00400c8b CALL system int system(char * __command)
00400c90 MOV EAX,0x0 00400c95
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
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