RiseFromTheDead
11 minutes to read
We are given a 64-bit binary called rise
and also a core file:
$ file *
core: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './rise flag', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: './rise', platform: 'x86_64'
rise: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8341bf2064b7903b6e87d69c63a1849338d3f1e6, for GNU/Linux 3.2.0, not stripped
The core file corresponds to an execution of rise
(actually, the command was ./rise flag
).
Reverse engineering
If we open rise
in Ghidra, we will see the following decompiled source code in C for main
(some variables have been renamed):
int main(int argc, char **argv) {
undefined8 *flag_chunk;
undefined8 uVar1;
ulong i;
char *pcVar3;
int ret;
undefined8 *puVar4;
byte zero;
zero = 0;
if (argc < 2) {
pcVar3 = "./program";
if (argc == 1) {
pcVar3 = *argv;
}
fprintf(stderr, "Usage: %s <secret file>\n", pcVar3);
ret = -1;
} else {
ret = open(argv[1], 0);
if (ret == -1) {
perror("Opening file");
ret = -1;
} else {
flag_chunk = (undefined8 *) mmap(NULL, 0x1000, 3, 2, ret, 0);
if (flag_chunk == NULL) {
perror("Mapping file");
ret = -1;
} else {
*(undefined8 *) ((long) flag_chunk + 0xaf) = 0;
flag_chunk[0x1ff] = 0;
puVar4 = (undefined8 *) ((long) flag_chunk + 0xb7U & 0xfffffffffffffff8);
i = (ulong) (((int) flag_chunk - (int) puVar4) + 0x1000U >> 3);
for (; i != 0; i--) {
*puVar4 = 0;
puVar4 = puVar4 + (ulong)zero * -2 + 1;
}
uVar1 = init_shuffle_list(flag_chunk);
*flag_chunk = 0;
*(undefined8 *) ((long) flag_chunk + 0xa7) = 0;
puVar4 = (undefined8 *) ((ulong) (flag_chunk + 1) & 0xfffffffffffffff8);
for (i = (ulong) (((int) flag_chunk - (int) (undefined8 *) ((ulong) (flag_chunk + 1) & 0xfffffffffffffff8)) + 0xafU >> 3); i != 0; i--) {
*puVar4 = 0;
puVar4 = puVar4 + (ulong) zero * -2 + 1;
}
shuf(uVar1, flag_chunk);
puts((char *) flag_chunk);
kill(0, 0xb);
ret = 0;
}
}
}
return ret;
}
First of all, it opens the file whose file name comes from argv[1]
(from the core file, we know it is opening a file called flag
). Next, it allocates memory with mmap
and copies the contents of the file in that memory region.
After that, it uses init_shuffle_list
:
char * init_shuffle_list(char *flag_chunk) {
char cVar1;
int fd;
char *p_flag;
byte random_byte;
char *local_30;
byte b;
fd = open("/dev/urandom", 0);
local_30 = NULL;
p_flag = flag_chunk + 0xaf;
LAB_001012c4:
read(fd, &random_byte, 1);
do {
b = random_byte;
if (random_byte < 0xaf) {
cVar1 = pos_in_list(local_30, random_byte);
if (cVar1 == 0) break;
}
read(fd, &random_byte, 1);
} while (true);
append_list(&local_30, b, *flag_chunk);
flag_chunk = flag_chunk + 1;
if (flag_chunk == p_flag) {
close(fd);
return local_30;
}
goto LAB_001012c4;
}
What we see here is that some random bytes are generated with /dev/urandom
. These random bytes are stored right next to the actual flag (offset 0xaf
). So, we already know that the length of the flag is 0xaf
(175 characters).
If the random byte (actually, random number) is less than 0xaf
, it uses pos_in_list
to determine if there is a character at that random index:
undefined8 pos_in_list(char *local_30, byte int) {
if (local_30 == NULL) {
return 0;
}
do {
if (*(byte *) ((long) local_30 + 8) == int) {
return 1;
}
local_30 = *(char **) local_30;
} while (local_30 != NULL);
return 0;
}
Once a character is found at the random index, it is stored with append_list
:
void append_list(char **param_1, undefined param_2,u ndefined param_3) {
long **pplVar1;
long **pplVar2;
long *plVar3;
undefined8 *puVar4;
pplVar1 = (long **) *param_1;
if ((long **) *param_1 == NULL) {
puVar4 = (undefined8 *) malloc(0x10);
*puVar4 = 0;
*(undefined *) (puVar4 + 1) = param_2;
*(undefined *) ((long) puVar4 + 9) = param_3;
*param_1 = (char *) puVar4;
} else {
do {
pplVar2 = pplVar1;
pplVar1 = (long **) *pplVar2;
} while ((long **) *pplVar2 != NULL);
plVar3 = (long *) malloc(0x10);
*pplVar2 = plVar3;
*plVar3 = 0;
*(undefined *) (*pplVar2 + 1) = param_2;
*(undefined *) ((long) *pplVar2 + 9) = param_3;
}
}
This function looks a bit weird, but it basically takes the byte of the flag and stores it together with the random byte from before in a 0x20
-sized chunk on the heap. It somehow creates a mapping between the flag bytes and the random bytes. We will see this later with GDB.
Finally, the program calls shuf
, which shuffles the flag using the previous mapping, and overwrites the flag buffer:
void shuf(long *param_1, long param_2) {
if (param_1 != NULL) {
do {
*(undefined *) (param_2 + (ulong) *(byte *) (param_1 + 1)) = *(undefined *) ((long) param_1 + 9);
*(undefined *) ((long) param_1 + 9) = 0;
param_1 = (long *) *param_1;
} while (param_1 != NULL);
}
}
Finally, the program crashes because of the use of kill(0, 0xb)
(SIGSEGV).
Dynamic analysis
We already know that the flag length is 0xaf
, so let’s create a fake flag for this and test it with GDB:
$ python3 -q
>>> from string import ascii_letters, digits
>>> flag = 'HTB{' + ((digits + ascii_letters) * 10)[:170] + '}'
>>> flag
'HTB{0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJ}'
>>> with open('flag', 'w') as f:
... f.write(flag)
...
175
Now, we can run the program with GDB and set breakpoints:
# gdb -q rise
Reading symbols from rise...
(No debugging symbols found in rise)
gef> break shuf
Breakpoint 1 at 0x12ee
gef> run flag
Starting program: /tmp/rise flag
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x00005555555552ee in shuf ()
At this point, we can see both arguments for shuf
:
gef> p/x $rdi
$1 = 0x5555555592a0
gef> p/x $rsi
$2 = 0x7ffff7ffa000
gef> vmmap heap
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x0000555555559000 0x000055555557a000 0x0000000000021000 0x0000000000000000 rw- [heap] <- $rdx, $rdi, $r9
gef> vmmap 0x7ffff7ffa000
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x00007ffff7ffa000 0x00007ffff7ffb000 0x0000000000001000 0x0000000000000000 rw- /tmp/flag <- $rbx, $rsi
The first one is a heap address, which contains the map between flag bytes and random bytes. And the other one is the mmap
chunk that was allocated with the flag text (although it has been erased).
On the heap, we have a lot of chunks with the random mappings. These are some examples:
gef> visual-heap -n
0x555555559000: 0x0000000000000000 0x0000000000000291 | ................ |
0x555555559010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x555555559290: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592a0: 0x00005555555592c0 0x000000000000484b | ..UUUU..KH...... |
0x5555555592b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592c0: 0x00005555555592e0 0x0000000000005498 | ..UUUU...T...... |
0x5555555592d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592e0: 0x0000555555559300 0x000000000000425f | ..UUUU.._B...... |
0x5555555592f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559300: 0x0000555555559320 0x0000000000007b30 | .UUUU..0{...... |
0x555555559310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559320: 0x0000555555559340 0x000000000000302d | @.UUUU..-0...... |
0x555555559330: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559340: 0x0000555555559360 0x000000000000311e | `.UUUU...1...... |
0x555555559350: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559360: 0x0000555555559380 0x0000000000003210 | ..UUUU...2...... |
0x555555559370: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559380: 0x00005555555593a0 0x000000000000332a | ..UUUU..*3...... |
0x555555559390: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593a0: 0x00005555555593c0 0x0000000000003463 | ..UUUU..c4...... |
0x5555555593b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593c0: 0x00005555555593e0 0x0000000000003599 | ..UUUU...5...... |
0x5555555593d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593e0: 0x0000555555559400 0x0000000000003679 | ..UUUU..y6...... |
0x5555555593f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559400: 0x0000555555559420 0x000000000000373f | .UUUU..?7...... |
0x555555559410: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559420: 0x0000555555559440 0x0000000000003812 | @.UUUU...8...... |
0x555555559430: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559440: 0x0000555555559460 0x000000000000397f | `.UUUU...9...... |
0x555555559450: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559460: 0x0000555555559480 0x0000000000006149 | ..UUUU..Ia...... |
0x555555559470: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559480: 0x00005555555594a0 0x00000000000062a8 | ..UUUU...b...... |
0x555555559490: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594a0: 0x00005555555594c0 0x0000000000006368 | ..UUUU..hc...... |
0x5555555594b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594c0: 0x00005555555594e0 0x0000000000006477 | ..UUUU..wd...... |
0x5555555594d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594e0: 0x0000555555559500 0x00000000000065aa | ..UUUU...e...... |
0x5555555594f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559500: 0x0000555555559520 0x00000000000066a6 | .UUUU...f...... |
0x555555559510: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559520: 0x0000555555559540 0x0000000000006776 | @.UUUU..vg...... |
0x555555559530: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559540: 0x0000555555559560 0x0000000000006816 | `.UUUU...h...... |
Now we can hit finish
to end the shuf
function and see the shuffled flag:
gef> finish
...
gef> x/s $rsi
0x7ffff7ffa000: "gkI1jHrG3wBv4HLj2q8dTmhFZI5vxz1lXdYPrqnyZN3u80g2{oKnfOuDseHJySc7r9klWMGaqahH6xpDxBt6b0ttzEBjp2TBlDI4sPVucKwwAvU1CioQ5mgdA6J3k709Ni7QLeFVinaCYfJ9Xsm}OUGAT5z8yMCShEc4FRfEbbeoRWp"
One interesting character is {
. In the previous mapping we see 7b30
. This means that the byte {
(0x7b
) will be stored at index 0x30
:
gef> x/c $rsi + 0x30
0x7ffff7ffa030: 0x7b
As a result, we can guess that the mapping indicates where the flag character will be placed in the output shuffled buffer. This is HTB{
, using the previous mapping:
gef> x/c $rsi + 0x4b
0x7ffff7ffa04b: 0x48
gef> x/c $rsi + 0x98
0x7ffff7ffa098: 0x54
gef> x/c $rsi + 0x5f
0x7ffff7ffa05f: 0x42
gef> x/c $rsi + 0x30
0x7ffff7ffa030: 0x7b
gef> printf "%c%c%c%c\n", ((char*) $rsi)[0x4b], ((char*) $rsi)[0x98], ((char*) $rsi)[0x5f], ((char*) $rsi)[0x30]
HTB{
Notice that the mapping is modified once an entry is used, the flag character is deleted:
gef> visual-heap -n
0x555555559000: 0x0000000000000000 0x0000000000000291 | ................ |
0x555555559010: 0x0000000000000000 0x0000000000000000 | ................ |
* 39 lines, 0x270 bytes
0x555555559290: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592a0: 0x00005555555592c0 0x000000000000004b | ..UUUU..KH...... |
0x5555555592b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592c0: 0x00005555555592e0 0x0000000000000098 | ..UUUU...T...... |
0x5555555592d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555592e0: 0x0000555555559300 0x000000000000005f | ..UUUU.._B...... |
0x5555555592f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559300: 0x0000555555559320 0x0000000000000030 | .UUUU..0{...... |
0x555555559310: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559320: 0x0000555555559340 0x000000000000002d | @.UUUU..-0...... |
0x555555559330: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559340: 0x0000555555559360 0x000000000000001e | `.UUUU...1...... |
0x555555559350: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559360: 0x0000555555559380 0x0000000000000010 | ..UUUU...2...... |
0x555555559370: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559380: 0x00005555555593a0 0x000000000000002a | ..UUUU..*3...... |
0x555555559390: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593a0: 0x00005555555593c0 0x0000000000000063 | ..UUUU..c4...... |
0x5555555593b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593c0: 0x00005555555593e0 0x0000000000000099 | ..UUUU...5...... |
0x5555555593d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555593e0: 0x0000555555559400 0x0000000000000079 | ..UUUU..y6...... |
0x5555555593f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559400: 0x0000555555559420 0x000000000000003f | .UUUU..?7...... |
0x555555559410: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559420: 0x0000555555559440 0x0000000000000012 | @.UUUU...8...... |
0x555555559430: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559440: 0x0000555555559460 0x000000000000007f | `.UUUU...9...... |
0x555555559450: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559460: 0x0000555555559480 0x0000000000000049 | ..UUUU..Ia...... |
0x555555559470: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559480: 0x00005555555594a0 0x00000000000000a8 | ..UUUU...b...... |
0x555555559490: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594a0: 0x00005555555594c0 0x0000000000000068 | ..UUUU..hc...... |
0x5555555594b0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594c0: 0x00005555555594e0 0x0000000000000077 | ..UUUU..wd...... |
0x5555555594d0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x5555555594e0: 0x0000555555559500 0x00000000000000aa | ..UUUU...e...... |
0x5555555594f0: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559500: 0x0000555555559520 0x00000000000000a6 | .UUUU...f...... |
0x555555559510: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559520: 0x0000555555559540 0x0000000000000076 | @.UUUU..vg...... |
0x555555559530: 0x0000000000000000 0x0000000000000021 | ........!....... |
0x555555559540: 0x0000555555559560 0x0000000000000016 | `.UUUU...h...... |
Core file analysis
We can easily find the shuffled flag by looking at the strings of this file:
# strings -100 core | sort -u
b5xo1ar_gmcrirttdne8ohsoshf6t/:ewm1ea8a_2h4r/ms7cetH6nig2oss}__eyer6aot{_ea_/peot./4-2tho.fb7Bac7_rerr__/su1_n!5p8e/c.7Tlcm_saos_sdm_rpa6p3tl2recty_22ffemef_i0e03u-vl5enh9deru
Now, we need to find the mapping. If we load the core file in GDB, we might want to look at the heap, but it doesn’t work:
# gdb -q -c core
warning: Can't open file /mnt/rise during file-backed mapping note processing
warning: Can't open file /lib/x86_64-linux-gnu/libc-2.31.so during file-backed mapping note processing
warning: Can't open file /lib/x86_64-linux-gnu/ld-2.31.so during file-backed mapping note processing
warning: Can't open file /mnt/flag during file-backed mapping note processing
[New LWP 995]
Python Exception <class 'FileNotFoundError'>: [Errno 2] No such file or directory: '/proc/995/maps'
Core was generated by `./rise flag'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00007f7067d8b087 in ?? ()
gef> visual-heap -n
[!] Failed to get the arena, heap commands may not work properly.
[!] No valid arena
So, we must find it using other options, such as xxd
. Since all heap chunks are 0x21
-sized chunks, we can grep
by this to find the mappings:
# xxd core | grep -A 1 '0000 0000 0000 0000 2100 0000 0000 0000'
00005290: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
000052a0: c032 ea4b 4e56 0000 3300 0000 0000 0000 .2.KNV..3.......
000052b0: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
000052c0: e032 ea4b 4e56 0000 7700 0000 0000 0000 .2.KNV..w.......
000052d0: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
000052e0: 0033 ea4b 4e56 0000 5d00 0000 0000 0000 .3.KNV..].......
000052f0: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
00005300: 2033 ea4b 4e56 0000 4700 0000 0000 0000 3.KNV..G.......
00005310: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
00005320: 4033 ea4b 4e56 0000 5b00 0000 0000 0000 @3.KNV..[.......
00005330: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
00005340: 6033 ea4b 4e56 0000 9200 0000 0000 0000 `3.KNV..........
00005350: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
00005360: 8033 ea4b 4e56 0000 9300 0000 0000 0000 .3.KNV..........
00005370: 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
00005380: a033 ea4b 4e56 0000 0f00 0000 0000 0000 .3.KNV..........
...
And here we have the indices where each flag byte will be placed in the shuffled flag. We only need to reverse it.
For instance, the first byte is the one at index 0x33
in the shuffled flag, the second byte is the one at index 0x77
, the third byte is the one at index 0x5d
and so on and so forth.
Solution
For this, I wrote the above output into a file (heap.txt
) and then used it with Python to unshuffle the flag:
#!/usr/bin/env python3
shuffled_flag = 'b5xo1ar_gmcrirttdne8ohsoshf6t/:ewm1ea8a_2h4r/ms7cetH6nig2oss}__eyer6aot{_ea_/peot./4-2tho.fb7Bac7_rerr__/su1_n!5p8e/c.7Tlcm_saos_sdm_rpa6p3tl2recty_22ffemef_i0e03u-vl5enh9deru'
positions = []
with open('heap.txt') as f:
f.readline()
while (line := f.readline()):
positions.append(int(line[30:32], 16))
f.readline()
flag = ''.join(shuffled_flag[p] for p in positions)
print(flag)
If we execute the script, we will get the flag:
$ python3 solve.py
HTB{by_the_powers_of_https://man7.org/linux/man-pages/man5/core.5.html_i_resurrect_this_process_from_the_dead-reveal_your_secrets_to_me!228da2f265e1f3c3c8f4b777600611e822649a}