Welcome to Hell
2023-03-30
Welcome To Hell
- Author: Battelle
- Rev
- 400 pts
Description
Welcome to hell, where all it seems that you can do is try to exit, maybe there is a flag hidden somewhere in this mess
Files
welcome_to_hell
- ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
Reversing
Opening welcome_to_hell
in Ghidra shows a few functions. The only one that appears to do anything useful is entry
, the first function in the binary. There are many other functions at higher addresses than entry, but they each just call syscall to exit the program. The return value of the nth exit function is n.
void entry(void)
{
int offset;
char *bufptr;
char buf [8];
bool neg;
syscall(); //read 3 bytes from stdin to buf
offset = 0;
neg = false;
bufptr = buf + 1;
if (buf[0] == '-') {
neg = true;
bufptr = buf + 2;
buf[0] = buf[1];
}
do {
offset = offset * 10 + (int)(char)(buf[0] + -0x30);
buf[0] = *bufptr;
if (buf[0] < '0') break;
bufptr = bufptr + 1;
} while (buf[0] < ':');
if (neg) {
offset = -offset;
}
(*(base + (int)((long)offset * 0x11)))(0,offset,(ulong)((long)offset * 0x11) >> 0x20);
return;
}
entry
provides some hints at what we should do. First, we see that we can provide some input that will jump to somewhere in the program. The address that we jump to is determined by an offset from base
, which is the first exit function that follows entry
. We could reverse the address calculations, or just test different offsets and see what exit code is returned.
$ ./welcome_to_hell
1
$ echo $?
1
$ ./welcome_to_hell
2
$ echo $?
2
$ ./welcome_to_hell
50
$ echo $?
50
Clearly the offset is just the number of exit functions to jump over. However, everything past base
exits the program and is not useful. Luckily we can see from entry
that we can jump to negative offsets too. But where should we jump to? After looking around we realize that most of the data before base
is just ELF headers. The most obvious choice is the suspicious data found in the string table. Decompiling this data does appear to give us intructions but also some invalid instrucitons. There seems to be syscalls in this code, so instead of reversing the invalid instructions let’s use strace
.
$ echo '-30' | strace ./welcome_to_hell
execve("./welcome_to_hell", ["./welcome_to_hell"], 0x7ffe934ba3c0 /* 10 vars */) = 0
read(0, "-30", 3) = 3
mmap(0x41414141000, 16384, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, 0, 0) = 0x41414141000
open("./welcome_to_hell", O_RDONLY) = 3
lseek(3, 176, SEEK_SET) = 176
read(3, "!\340\214!\350\205ilii\1Ibhh\350]Mhhhh!\321\1\10\5\5\f\7\16\f"..., 12112) = 12112
write(1, "Welcome to the challenge!\n", 26Welcome to the challenge!
) = 26
write(1, "Enter the flag: ", 16Enter the flag: ) = 16
read(0, "\n", 48) = 1
exit(25) = ?
+++ exited with 25 +++
The programs first reads in the 3 byte offset, same as before. We pass in ‘-30’ to jump to the beginning of the string table and run the instuctions there. The program then calls mmap
, open
, lseek
, and read
to read in data from the binary and map it into memory. Then we are welcomed to the challenge, and finally get the flag prompt. The program then quits, presumably because we didn’t give the flag. Let’s look with gdb.
► 0x4141414108f syscall <SYS_read>
fd: 0x0 (/dev/pts/0)
buf: 0x41414142fd0 ◂— xor esi, dword ptr [rbx] /* 0x3333333333333333 */
nbytes: 0x30
0x41414141091 mov rdx, rax
0x41414141094 mov rdi, 0x19
0x4141414109e je 0x414141410a9 <0x414141410a9>
0x414141410a0 mov rax, 0x3c
0x414141410a7 syscall
0x414141410a9 nop
Eventually, reach the read
syscall where the flag is read. We can see that if read
returns a length other than 0x19 then the program exits. We’ll give it a 25 character sting and continue stepping.
0x414141410a9 nop
0x414141410aa lea rsp, [rip + 9]
0x414141410b1 mov rax, 0xf
► 0x414141410b8 syscall <SYS_rt_sigreturn>
0x414141410ba add byte ptr [rax], al
0x414141410bc add byte ptr [rax], al
0x414141410be add byte ptr [rax], al
0x414141410c0 add byte ptr [rax], al
0x414141410c2 add byte ptr [rax], al
Ok now that the flag length is correct we are at the instuctions above, which points the stack pointer to the data after the syscall and calls rt_sigreturn
. This is a function used to restore the process state after the program returns from handling a signal. Typically the state is stored on the stack before control is transfered to the kernel, and restored by sigreturn when the kernel is done. In this case there is no signal, and the program is using its own fabricated signal frame (the junk instructions after the syscall) to modify the process state. This is a pwn technique know as SROP.
► 0x414141411b2 add r9, rcx
0x414141411b5 mov r10, qword ptr [r9 + 1]
0x414141411b9 xor rax, r8
0x414141411bc add cl, 0xb
0x414141411bf shr rax, cl
0x414141411c2 cmp al, r10b
0x414141411c5 je 0x414141411c9 <0x414141411c9>
0x414141411c7 jmp rdx
0x414141411c9 nop
0x414141411ca lea rsp, [rip + 9]
0x414141411d1 mov rax, 0xf
0x414141411d8 syscall
Each signal frame that is “restored” has the format above. r10
is pointed to the next character in our flag. Some operations are perfomed on rax
, but by the compare instruction it will be the correct flag character. At this point we can just contine stepping through, dumping rax
, and setting r10
to get the full flag.
#!/usr/bin/python3
from pwn import *
p = process('./welcome_to_hell')
p.send(b'-30')
p.recvline()
p.recvuntil(b'flag: ')
p.send(b'UMASS{sr0p_n_r3v_is_h3ll}')
print(p.recvall())
Pwntools is usefull to test the flag we found by passing it to to program before it quits and without newlines.
$ ./heaven.py
[+] Starting local process './welcome_to_hell': pid 1178
[+] Receiving all data: Done (25B)
[*] Stopped process './welcome_to_hell' (pid 1178)
b'UMASS{sr0p_n_r3v_is_h3ll}'
Nice. Overall this was a cool challenge. I certainly learned a lot about the ELF file format and especially UNIX signals. SROP is new to me so this was a neat chance to learn about how it works and how it can be used.