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.
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
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
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)
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
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!
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
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
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.
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")
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.
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)
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.
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")
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
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.
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)
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
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.
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!)
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