core (misc)
Extracting the archive gives 2 files:
- chall
- core
❯ file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b94dac66618a0a01fb827ad76fbd6ba5b8811c67, for GNU/Linux 3.2.0, not stripped
❯ file core
core: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './chall DEBUG', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: './chall', platform: 'x86_64'
core
is a core dump generated from running ./chall
with an argument DEBUG
.
Static Analysis
First, it is useful to know what chall
does. Opening it up in Ghidra gives the following code.
undefined8 main(int argc,long param_2)
{
...
__stream = fopen("./pass","r");
if (__stream == (FILE *)0x0) {
fwrite("Missing password file!\n",1,0x17,stderr);
}
else {
// [1] Read the password, get the flag from stdin, and encrypt the flag with the password
fread(password,16,1,__stream);
printf("Enter flag: ");
fflush(stdout);
read(0,flag,40);
printf("Encrypted flag: ");
for (i = 0; i < 40; i++) {
flag[i] = flag[i] ^ password[i % 16];
putchar(flag[i]);
if (i != 39) {
flag[i + 1] = flag[i] ^ flag[i + 1];
}
}
// [2] Clear the password from the stack
memset(password,0,0x10);
// [3] Generate an abort signal.
// This creates a core dump if the program is executed in GDB.
if (argc == 2) {
result = strcmp(argv[1],"DEBUG");
if (result == 0) {
raise(6);
}
}
}
...
}
The program is quite short.
- First, it read the password from a file, and asks the user to provide the flag as input. Then it encrypts the flag with the password with a simple xor loop.
- Then, it clears the password from the stack using
memset
. - Finally, it raises signal 6 (
SIGABRT
). If a program aborts while running in GDB, a core dump will be generated.
The core dump is given to us by the challenge. So, we can inspect the core dump to find the flag.
Inspecting the Core Dump
Before continuing, here is a brief overview of what a core dump. When a program crashes, a core dump is generated, and it contains the whole program state, i.e. register value and memory contents. This is useful for a developer to find out what caused the program to crash.
To use a core dump, we can load it with GDB, by running the command gdb chall core
.
A useful thing to do first is to check where we are in the program.
gef➤ print/x $rip
$1 = 0x7f2d17452ce1
gef➤ info registers
...
rip 0x7f2d17452ce1 0x7f2d17452ce1 <setjmp+1>
...
It seems like the program stopped when it is in a setjmp
function. For better understanding, we can run vmmap
(or info proc mappings
) to see the memory map. (vmmap
is only available in GDB extensions like GEF
, peda
, pwndbg
)
gef➤ info proc mapping
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x5566a996c000 0x5566a996d000 0x1000 0x0 /core/chall
0x5566a996d000 0x5566a996e000 0x1000 0x1000 /core/chall
0x5566a996e000 0x5566a996f000 0x1000 0x2000 /core/chall
0x5566a996f000 0x5566a9970000 0x1000 0x2000 /core/chall
0x5566a9970000 0x5566a9971000 0x1000 0x3000 /core/chall
0x7f2d1741a000 0x7f2d1743c000 0x22000 0x0 /lib/x86_64-linux-gnu/libc-2.31.so
0x7f2d1743c000 0x7f2d17596000 0x15a000 0x22000 /lib/x86_64-linux-gnu/libc-2.31.so
0x7f2d17596000 0x7f2d175e5000 0x4f000 0x17c000 /lib/x86_64-linux-gnu/libc-2.31.so
0x7f2d175e5000 0x7f2d175e9000 0x4000 0x1ca000 /lib/x86_64-linux-gnu/libc-2.31.so
0x7f2d175e9000 0x7f2d175eb000 0x2000 0x1ce000 /lib/x86_64-linux-gnu/libc-2.31.so
0x7f2d175f3000 0x7f2d175f4000 0x1000 0x0 /lib/x86_64-linux-gnu/ld-2.31.so
0x7f2d175f4000 0x7f2d17614000 0x20000 0x1000 /lib/x86_64-linux-gnu/ld-2.31.so
0x7f2d17614000 0x7f2d1761c000 0x8000 0x21000 /lib/x86_64-linux-gnu/ld-2.31.so
0x7f2d1761d000 0x7f2d1761e000 0x1000 0x29000 /lib/x86_64-linux-gnu/ld-2.31.so
0x7f2d1761e000 0x7f2d1761f000 0x1000 0x2a000 /lib/x86_64-linux-gnu/ld-2.31.so
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
...
0x005566a996d000 0x005566a996d01b 0x00000000001000 r-x .init
0x005566a996d020 0x005566a996d0e0 0x00000000001020 r-x .plt
0x005566a996d0e0 0x005566a996d0f0 0x000000000010e0 r-x .plt.got
0x005566a996d0f0 0x005566a996d1a0 0x000000000010f0 r-x .plt.sec
0x005566a996d1a0 0x005566a996d4b5 0x000000000011a0 r-x .text
0x005566a996d4b8 0x005566a996d4c5 0x000000000014b8 r-x .fini
...
Both outputs above give the information in different formats, so we can’t just say one is better than the other. info proc mappings
tells us that contents from chall
are in the memory region with address starting with 0x5566
, and libc
has address that starts from 0x7f2d
.
vmmap
output is useful as it gets into finer details. It shows that the .text
section is in the 0x005566a996d1a0-0x005566a996d4b5
memory range.
Remember that the value of rip
is 0x7f2d17452ce1
, so it is some code inside libc. This makes sense, as raise
is a libc function. The program aborts inside libc, as expected.
Finding the stack frame
Now, the important step is to find the stack frame of the main
function, so that we can get the encrypted flag.
gef➤ print/x $rbp
$3 = 0x7ffc1d759a90
According to the disassembly, the encrypted flag is stored in the stack, at rbp-0x30
, and the password is stored at rbp-0x40
. We can use the telescope
command (only in GEF
/peda
/pwndbg
) to check the contents starting from rbp-0x40
. (I have annotated the memory contents below with their corresponding purposes.)
gef➤ tele $rbp-0x40
# password
0x007ffc1d759a50│+0x0000: 0x00000000000000
0x007ffc1d759a58│+0x0008: 0x00000000000000
# flag
0x007ffc1d759a60│+0x0010: 0x9364d7bb7e9a9eb9
0x007ffc1d759a68│+0x0018: 0x5a48f0f298bfa23a
0x007ffc1d759a70│+0x0020: 0xd829cef5738cd3a0
0x007ffc1d759a78│+0x0028: 0x4b52b0bb85f2e723
0x007ffc1d759a80│+0x0030: 0xd4419cb834cdc5bd
# canary
0x007ffc1d759a88│+0x0038: 0xf6df0028e1a91800
# return address
0x007ffc1d759a90│+0x0040: 0x005566a996d440
Everything above is as expected.
rbp-0x40
contains 16 bytes of password, which was cleared usingmemset
, so it only contains null bytes.- Then, the 40 bytes of the flag (encrypted).
- According to Ghidra, after the flag is the stack cookie. This value looks like a stack cookie too, because its least significant byte is
00
. If I’m not wrong, the stack cookie’s LSB is a null byte, to prevent it from being leaked, if a string is stored before it, and the string is not null-terminated. Think about it. - Lastly, there is a value that starts with
0x5566
. We have seen earlier that this maps to the.text
section of thechall
executable.
Recovery of the flag
Now, we can use GDB to give me the encrypted flag contents for scripting. The print-format
command in GEF
is useful to get the contents in Python list format.
gef➤ x/40bx $rbp-0x30
0x7ffc1d759a60: 0xb9 0x9e 0x9a 0x7e 0xbb 0xd7 0x64 0x93
0x7ffc1d759a68: 0x3a 0xa2 0xbf 0x98 0xf2 0xf0 0x48 0x5a
0x7ffc1d759a70: 0xa0 0xd3 0x8c 0x73 0xf5 0xce 0x29 0xd8
0x7ffc1d759a78: 0x23 0xe7 0xf2 0x85 0xbb 0xb0 0x52 0x4b
0x7ffc1d759a80: 0xbd 0xc5 0xcd 0x34 0xb8 0x9c 0x41 0xd4
gef➤ print-format --lang py --bitlen 8 -l 40 $rbp-0x30
buf = [0xb9, 0x9e, 0x9a, 0x7e, 0xbb, 0xd7, 0x64, 0x93, 0x3a, 0xa2, 0xbf, 0x98, 0xf2, 0xf0, 0x48, 0x5a, 0xa0, 0xd3, 0x8c, 0x73, 0xf5, 0xce, 0x29, 0xd8, 0x23, 0xe7, 0xf2, 0x85, 0xbb, 0xb0, 0x52, 0x4b, 0xbd, 0xc5, 0xcd, 0x34, 0xb8, 0x9c, 0x41, 0xd4]
Unfortunately, the password was already cleared from the stack, and all we get are null bytes. Let’s see what we can do from here.
By reversing the xor operations, and knowing that the flag starts with wgmy{
, it is possible to recover part of the password. It should be simple enough so you can do it yourself :p. I will skip describing how to do so.
We can deduce that the password starts with '\xce@i\x9d'
. But this is not enough. The password has a total of 16 bytes.
Here, I made a guess that maybe although the password is no longer in the stack, it may be in other sections, like the heap. I remember that functions like scanf
may store the values in the heap during the process. Maybe fread
(used by the program to read the password) does the same.
To look for the password in the heap, I can use the grep
command by GEF
. The syntax is as follows:
gef➤ help search-pattern
SearchPatternCommand: search a pattern in memory. If given an hex value (starting with 0x)
the command will also try to look for upwards cross-references to this address.
Syntax: search-pattern PATTERN [little|big] [section]
Examples:
search-pattern AAAAAAAA
search-pattern 0x555555554000 little stack
search-pattern AAAA 0x600000-0x601000
I first tried to search by just running grep '\\xce@i\\x9d'
(need the double backslash because of some parsing issues, I will make a PR to fix this soon). In a normal debugging session, this will search through all the memory regions of the process memory. However, it doesn’t work with this core dump. GEF is not commonly used with core dumps so this command doesnt work well here.
So, I have to manually provide the memory ranges. To do so, I used vmmap
to find out the memory regions, in particular, I am intersted in the heap regions.
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
...
0x005566a996c000 0x005566a996d000 0x00000000002000 r-- load1
0x005566a996d000 0x005566a996e000 0x00000000003000 r-x load2
0x005566a996e000 0x005566a996f000 0x00000000003000 r-- load3
0x005566a996f000 0x005566a9970000 0x00000000003000 r-- load4
0x005566a9970000 0x005566a9971000 0x00000000004000 --- load5
0x005566aaf07000 0x005566aaf28000 0x00000000005000 --- load6
0x007f2d1741a000 0x007f2d1741b000 0x00000000026000 r-- load7a
0x007f2d1741b000 0x007f2d1743c000 0x00000000027000 r-- load7b
...
I know that regions starting with 0x7f2d
is libc, so I’m not interested in those regions.
And regions from 0x005566a996c000
to 0x005566a9971000
belongs to chall
, according to the output of info proc mappings
.
So, I provide the memory range of load6
to grep
.
gef➤ grep '\\xce@i\\x9d' little 0x005566aaf07000-0x005566aaf28000
[+] Searching '\xce@i\x9d' in 0x005566aaf07000-0x005566aaf28000
[+] In 'load6'(0x5566aaf07000-0x5566aaf28000), permission=---
0x5566aaf07480 - 0x5566aaf0748a → "\xce@i\x9d[...]"
It found something at 0x5566aaf07480
!
gef➤ x/16bx 0x5566aaf07480
0x5566aaf07480: 0xce 0x40 0x69 0x9d 0xbe 0x59 0xd7 0x95
0x5566aaf07488: 0x98 0xa1 0x2d 0x14 0x0f 0x33 0x81 0x20
This should be the password!
Using this password, the encrypted flag can be decrypted to get the flag.