#import "@preview/polylux:0.4.0": * #import "slides/lib.typ": * #import "@preview/codly:1.3.0": * #import "@preview/codly-languages:0.1.1": * #show: codly-init.with() #let default-codly = ( display-name: false, display-icon: false, zebra-fill: none, fill: luma(240), radius: 1em, inset: (y: 0.15em), highlighted-default-color: highlight-color, highlight-fill: it => it.lighten(40%), //highlight-color, ) #codly-disable() #set text(lang: "en") #set list(marker: none) #set par(leading: 0.2em) #set list(spacing: 1em) #show: sns-polylux-template.with( txt-font: "New Computer Modern", title-font: "TeX Gyre Heros", aspect-ratio : "16-9", title : [From Large Scale Analysis to Dynamic Deobfuscation], subtitle : [The Woes of Android Reverse Engineering], event : [], //[PhD Defense], short-title : [], //[PhD Defense], //short-event : [Rennes, 2025/12/9], title-size : 32pt, section-size : 18pt, size : 22pt, //logo-1 : image("slides/imgs/logo_irisa.png"), //logo-2 : image("slides/imgs/logo_pirat.png"), // colormap : sns-polylux-template_sns-pirat, authors : ( { set text(weight: "bold") [MINEAU Jean-Marie] v(1em) }, [LALANDE Jean-François, PhD supervisor], [VIET TRIEM TONG Valérie, PhD co-supervisor] ), date : datetime(year: 2025, month: 12, day: 9), ) /* * Intro: * Dear jury, gentle people of the audience, here and online, thank you for your presence. * I am Jean-Marie Mineau, and today I will be defending my thesis about Android Application reverse engineering and the many difficulties a reverse engineer might encounter. * This thesis was suppervised by Jean-François Lalande and Valerie Viet Triem Tong, within the PIRAT research team at IRISA. */ #title-slide( logo: grid(columns: 2, image("slides/imgs/logo_pirat.png"), image(width: 500pt, "slides/imgs/logo_cs.png"), image("slides/imgs/logo_irisa.png"), image("slides/imgs/platypus.png"), ) ) #slide( new-sec: true, title: [Introduction], hide-title: true, /*foreground: { ghost-5(x: 10%, y: 30pt) ghost-4(x: 95%, y: 80%) //ghost-4(x: 45%, y: 43%) }*/ )[ #set align(center+horizon) #grid( columns: (1fr, 1fr), image("slides/imgs/google.png", width: 200pt), image("slides/imgs/phone.png", height: 350pt) ) #v(2em) ] #counter("logical-slide").update( n => n - 1 ) #slide( foreground: ghost-4(x: 60%, y: 25%, rot: 45deg) )[ #set align(center+horizon) #grid( columns: (3fr, 2fr), stack(dir: ltr, item-by-item[ - Personal Data and PII - Computing Power - Phone - Mic, Camera, \ Geolocalisation ], [ $ => $ ], item-by-item()[ - Ransomware/Spyware - Cryptojacker - Expander (phone billing) - Stalkerware ] ), { move(dx: 20pt, image("slides/imgs/phone.png", height: 350pt)) } ) ] #slide( title: [Analysing Applications: Which Tools?], foreground: eye-3(x: 3%, y: 5%) )[ #set align(center+horizon) #move(dx: -50pt, image("slides/imgs/apk-analysis.svg", width: 300pt)) ] // something is broken, so hack to keep the page number at the same #counter("logical-slide").update( n => n - 1 ) #slide( title: [Analysing Applications: Which Tools?], )[ #set list(spacing: 3em) #item-by-item[ - #cite(, form: "prose"): systematic literature review for Android static analysis, lists open-sourced tools - #cite(, form: "prose"): tests analysis tools, raises concerns about reusability and analysis of real-world applications ] ] // something is broken, so hack to keep the page number at the same #counter("logical-slide").update( n => n - 1 ) #slide( title: [Analysing Applications: Which Tools?], )[ #highlight-block(pb1-text) ] #slide( title: [Obfuscation], //foreground: eye-1(x: 95%, y: 85%, mirror: true) )[ #set list(marker: [-]) Applications might use *obfuscation* to either: - protect their IP - hide malicious behaviour #v(1em)#uncover(2)[ We will focus on two techniques: - *Dynamic Code Loading* - *Reflection* ] ] #for i in range(4) { if i != 0 { counter("logical-slide").update( n => n - 1 ) } show: yes-codly slide( title: [Obfuscation], subtitle: if i == 0 [Example] else if i == 1 [Dynamic Code Loading] else if i in (2, 3) [Reflection] else { none }, foreground: eye-1(x: 95%, y: 85%, mirror: true) )[ #if i == 0 { codly(..default-codly) } else if i == 1 { codly( highlighted-lines: (1, 5, 6, 7, 8), ..default-codly ) } else if i == 2 { codly( highlighted-lines: (2, 3), highlights: ( (line: 10, start: 42, end: 59, fill: pirat-color.blue), (line: 13, start: 3, end: 21, fill: pirat-color.blue), ), ..default-codly ) } else if i == 3 { codly( highlighted-lines: (10,), highlights: ( (line: 12, start: 14, end: 34, fill: pirat-color.blue), (line: 15, start: 2, end: 19, fill: pirat-color.blue), ), ..default-codly ) } #scale(70%, reflow: true)[ ```java String DEX = "ZGV4CjA [...] EAAABEAwAA"; String className = "W5f3 [...] 3sls="; String methodName = "n6WGYJzjDrUvR9cYljlNlw=="; ClassLoader cl = new InMemoryDexClassLoader( ByteBuffer.wrap(Base64.decode(DEX, 2)), Main.class.getClassLoader() ); Class loadedClass = this.cl.loadClass(decrypt(className)); Object obj = "FooBar"; Object ret = loadedClass.getMethod( decrypt(methodName), String.class ).invoke(null, obj); ```] ] } #counter("logical-slide").update( n => n - 1 ) #slide( title: [Obfuscation], subtitle: [Deobfuscated], )[ #show: yes-codly #codly( skips: ((3, 10), (5, 10), (6, 10)), ..default-codly ) #scale(100%)[ ```java public class Foo { public static String bar(String arg) { } } String ret = Foo.bar("FooBar"); ```] ] #slide( title: [Class Loading], )[ #set align(center) #show: yes-codly #grid( columns: (2fr, 1em, 1fr), scale(70%, reflow: true)[ #codly( highlights: ( (line: 1, start: 0, end: 11, fill: pirat-color.blue), (line: 1, start: 22, end: 43, fill: pirat-color.blue), (line: 3, start: 14, end: 27, fill: pirat-color.blue), (line: 6, start: 32, end: 40, fill: pirat-color.blue), ), ..default-codly ) ```java ClassLoader cl = new InMemoryDexClassLoader( ByteBuffer.wrap(Base64.decode(DEX, 2)), Main.class.getClassLoader() ); Class loadedClass = this.cl.loadClass(decrypt(className)); ``` ], [], uncover(2, scale(70%, reflow: true)[ #codly( ..default-codly ) ```java class A { public static void foo() { B b = new B(); b.bar(); } } ``` Where is the class loader? ]) ) ] #counter("logical-slide").update( n => n - 1 ) #slide( title: [Class Loading], )[ #item-by-item[ - Used to select classes implementation - More complexe than it looks - Doubious documentation - Not studied in the context of Android Static Analysis ] ] #counter("logical-slide").update( n => n - 1 ) #slide( title: [Class Loading], )[ #highlight-block(pb2-text) ] #slide( foreground: ghost-5(x: 10%, y: 7%) )[ #set align(center+horizon) #grid( columns: (1fr, 1fr), gutter: 2em, [ == Dynamic Analysis #item-by-item[ - Run the application - _See_ dynamically loaded bytecode - _See_ reflection calls - Limited by code coverage ] ], [ == Static Analysis #item-by-item(start: 5)[ - Do *not* run the application - *Not* limited by code coverage - Some values cannot be computed ] ], grid.cell(colspan: 2, uncover(7)[ #text(size: 30pt)[Can we combine both?] ]), ) ] #slide[ #highlight-block(pb3-text) ] #slide[ #highlight-block(pb1-text) #highlight-block(pb2-text) #highlight-block(pb3-text) ] #new-section-slide([Tool Reusability]) #slide( title: [Methodology] )[ #set align(center+horizon) #show figure.caption: none #scale(100%, get_figure()) #v(1em) #text(size: 25pt)[22 tools selected, 2 we could not package] /* #stack(dir: ltr, scale(40%, reflow: true, get_figure()), scale(55%, reflow: true, get_figure()), )*/ ] #slide( title: [Methodology], foreground: place( bottom + left, dx: 88%, dy: -63%, )[ #set align(center+horizon) #set text(size: 15pt) 62 525 APKs #v(-1.5em) from #v(-1.5em) 2010 to 2023 ] )[ #set align(center+horizon) #show figure.caption: none #scale(90%, get_figure()) #text(size: 25pt)[We check if the results *exist* after running a tool] ] #slide( title: [Results], foreground: ghost-2(x: 97%, y: 10%) )[ #set align(center+horizon) #show figure.caption: none #scale(100%, get_figure()) //#text(size: 25pt)[We check if the results *exist* after running a tool] ] #counter("logical-slide").update( n => n - 1 ) #slide( title: [Results], foreground: { ghost-2(x: 97%, y: 10%) let x_0 = 112pt let y_0 = -117pt let w = 21pt let h = 235pt let dx = 33.3 for i in range(20) { let color = if i in (2, 4, 6, 7, 8, 9, 14, 16, 18, 19) { white.transparentize(100%) } else { white.transparentize(10%) } place( bottom + left, dx: x_0 + i*dx*1pt, dy: y_0, rect( width: w, height: h, //stroke: red, fill: color, ) ) } place(bottom + left, line( start: (x_0 - 20pt, y_0 - h/2), end: (x_0 + dx * 20 * 1pt, y_0 - h/2), stroke: pirat-color.red + 3pt )) } )[ #set align(center+horizon) #show figure.caption: none #scale(100%, get_figure()) ] #counter("logical-slide").update( n => n - 1 ) #slide( title: [Results], foreground: { ghost-2(x: 97%, y: 10%) let x_0 = 112pt let y_0 = -117pt let w = 21pt let h = 235pt let dx = 33.3 for i in range(20) { let color = if i in (3, 10) { white.transparentize(100%) } else { white.transparentize(10%) } place( bottom + left, dx: x_0 + i*dx*1pt, dy: y_0, rect( width: w, height: h, //stroke: red, fill: color, ) ) } place(bottom + left, line( start: (x_0 - 20pt, y_0 - h/2), end: (x_0 + dx * 20 * 1pt, y_0 - h/2), stroke: pirat-color.red + 3pt )) } )[ #set align(center+horizon) #show figure.caption: none #scale(100%, get_figure()) ] #slide( title: [Results over Time], )[ #set align(center+horizon) #show figure.caption: none #scale(150%, get_figure()) ] #slide( title: [Bytecode Size], )[ #set align(center+horizon) #show figure.caption: none #scale(120%, get_figure()) #text(size: 22pt)[Finishing rate as a function of the bytecode size, for APKs discovered in 2022] ] #slide( title: [Conclusion] )[ #set align(center) #item-by-item[ - Over 22 tools, 10 are usable (*less than half*) - Newer applications are harder to analyse - Applications with more bytecode are harder to analyse - Applications targetting more recent versions of Android are harder to analyse ] ] #slide[ #set align(center) #text(size: 22pt)[21st International Conference on Software and Systems Reuse (ICSR 2024)] #v(2em) #show regex("\[\d+\]"): none #cite(, form: "full") ] #new-section-slide([Class Shadowing]) #slide( title: [Class Loading], )[ #set align(center) #show: yes-codly #grid( columns: (2fr, 1em, 1fr), scale(70%, reflow: true)[ #codly( highlights: (/* (line: 1, start: 0, end: 11, fill: pirat-color.blue), (line: 1, start: 22, end: 43, fill: pirat-color.blue), (line: 3, start: 14, end: 27, fill: pirat-color.blue), (line: 6, start: 32, end: 40, fill: pirat-color.blue), */), ..default-codly ) ```java ClassLoader cl = new InMemoryDexClassLoader( ByteBuffer.wrap(Base64.decode(DEX, 2)), Main.class.getClassLoader() ); Class loadedClass = this.cl.loadClass(decrypt(className)); ``` ], [], scale(70%, reflow: true)[ #codly( ..default-codly ) ```java class A { public static void foo() { B b = new B(); b.bar(); } } ``` ] ) ] #slide( title: [Android Ecosystem] )[ #set align(center+horizon) #show figure.caption: none #grid( columns: (3fr, 1fr), scale(reflow: true, get_figure()), [ #set align(left) #set text(size: 20pt) #set list(marker: [-]) === Types of classes: - APK Classes - Platform Classes - SDK Classes - Hidden APIs ] ) // TODO: hightlight ] #slide( title: [Android ClassLoaders] )[ #set align(center+horizon) #show figure.caption: none #scale(60%, reflow: true, get_figure()) ] #slide( title: [MultiDex] )[ #set align(center + horizon) #let apk-block = block.with( //fill: green.lighten(50%), stroke: black, inset: 10pt, radius: 12pt, ) #only(1)[ #apk-block[ #set align(left+top) === `app.apk` #line(length: 30%) ``` AndroidManifest.xml resources.arsc META-INF/ res/ classes.dex ``` ] ] #only(2)[ #apk-block[ #set align(left+top) === `app.apk` #line(length: 50%) #stack(dir: ltr, ``` AndroidManifest.xml resources.arsc META-INF/ res/ classes.dex ```, h(2em),[ ``` classes2.dex classes3.dex ``` ] ) ] ] #only(3)[ #apk-block[ #set align(left+top) === `app.apk` #line(length: 75%) #stack(dir:ltr, ``` AndroidManifest.xml resources.arsc META-INF/ res/ classes.dex classes2.dex classes3.dex ```, h(2em), ``` classes4.dex classes5.dex classes6.dex classes7.dex classes8.dex classes9.dex classes10.dex ```, h(2em), ``` classes11.dex classes12.dex classes13.dex classes14.dex classes15.dex classes16.dex ... ``` ) ] #ghost-4(x: 2%, y: 2%, mirror: true) ] #only(4)[ #grid( columns: (1fr, 1fr), apk-block[ #set align(left+top) === `app.apk` #line(length: 30%) ``` AndroidManifest.xml resources.arsc META-INF/ res/ classes.dex classes2.dex classes3.dex ``` ], yes-codly[ #scale(100%, reflow: true, ```python def get_dex_name(index: int): if index == 0: return "classes.dex" else: return f"classes{index+1}.dex" ```) ] ) ] #only(5)[ #grid( columns: (1fr, 1fr), apk-block[ #set align(left+top) === `app.apk` #line(length: 30%) ``` AndroidManifest.xml resources.arsc META-INF/ res/ classes.dex classes2.dex classes3.dex ``` ], yes-codly[ #set list(spacing: 0.5em) #scale(100%, reflow: true, ```python def get_dex_name(index: int): if index == 0: return "classes.dex" else: return f"classes{index+1}.dex" ```) - `classes0.dex` ? - `classes1.dex` ? - `classes02.dex` ? - `classes10.dex` ? ] ) #ghost-4(x: 2%, y: 2%, mirror: true) ] ] #for i in range(4) { if i != 0 { counter("logical-slide").update( n => n - 1 ) } slide( title: [Shadow Attacks] )[ #if i in (0, 4) { codly( ..default-codly ) } else if i == 1 { codly( highlighted-lines: (7, 8), ..default-codly ) } else if i == 2 { codly( highlighted-lines: (1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15), ..default-codly ) } #let partial-hide( i, hidden: (), partial: (), body ) = { if i in hidden { hide(body) } else if i in partial { set text(fill: luma(200)) body } else { body } } #set align(center+horizon) #grid( columns: (1fr, 1fr), column-gutter: 2em, yes-codly[ #scale(60%, reflow: true, ```python def get_dex_name(index: int): if index == 0: return "classes.dex" else: return f"classes{index+1}.dex" def load_class(class_name: str): if is_platform_class(class_name): return boot_cl.load(class_name) else: index = 0 dex_file = get_dex_name(index) while exists_in_apk(dex_file) and \ class_name not in classes_of(dex_file): index += 1 dex_file = get_dex_name(index) if file_exists_in_apk(dex_file): return load_from_file(dex_file, class_name) else: raise ClassNotFoundError() ```) ], [ #set align(left) #set text(size: 18pt) #partial-hide(i, hidden: (0,), partial: (2,))[ === SDK shadowing Trick the tool into using an APK class instead of an SDK class ] #partial-hide(i, hidden: (0,), partial: (2,))[ === Hidden API shadowing Trick the tool into using an APK class instead of an hidden API class ] #partial-hide(i, hidden: (0,1))[ === Self Shadowing Trick the tool into using an APK class instead of another APK class ] ] ) ] } #slide( title: [Impact on Tools], )[ #set align(center+horizon) #show figure.caption: none #show link.where(dest: ): it => it.body #show figure: it => { set align(left) show table: set align(center+horizon) it } #scale(100%, reflow: true, get_figure()) ] #slide( title: [Example: Androguard], foreground: eye-4(x: 97%, y: 85%, mirror: true) )[ #set align(center+horizon) #show image: scale.with(250%) #set figure(gap: 4em) #v(3em) #grid( columns: (1fr, 1fr), get_figure(), get_figure() ) ] #for i in range(3) { if i != 0 { counter("logical-slide").update( n => n - 1 ) } slide( title: [In the Wild], foreground: eye-2(x: 8%, y: 67%, height: 70pt) )[ #set align(center+horizon) #show figure.caption: none #show link.where(dest: ): it => it.body #set table( fill: (x, y) => { if ( i == 1 and (x, y) in ((1, 13), (2, 13), (4, 13), (8, 13)) ) or ( i == 2 and (x, y) in ((1, 14), (2, 14)) ) { highlight-color } else { none } } ) #scale(90%, reflow: true, get_figure()) ] } #slide( title: [Conclusion] )[ #item-by-item[ - We modeled the class loading algorithm - Static Analysis Tools did not - We introduced obfuscation techniques based on this model - Ambiguous cases exists in the wild - We did not find deliberate shadow attacks ] ] #slide[ #set align(center) #text(size: 22pt)[Digital Threats: Research and Practice] #v(2em) #show regex("\[\d+\]"): none #cite(, form: "full") ] #new-section-slide([The Application of Theseus]) #slide( title: [Overview], )[ #set align(center+horizon) #show figure.caption: none #scale(100%, reflow: true, get_figure()) ] #slide( title: [Dynamic Analysis], )[ - Frida: intercepts method calls #v(2em) #uncover("2-")[ - Android Emulator: runs on computer/server - Grodd Runner: clicks buttons ] #v(2em) #uncover(3)[ - Phone with adb enable: actuall hardware - Human: intelligent button clicker ] ] #slide( title: [Dynamic Code Loading], foreground: ghost-6(x: 80%, y: 15%, mirror: true) )[ #set align(center+horizon) #show figure.caption: none #show image: box.with(width: 58%) #get_figure() ] #for i in range(4) { if i != 0 { counter("logical-slide").update( n => n - 1 ) } slide( title: [Reflection], //foreground: ghost-6(x: 80%, y: 15%, mirror: true) )[ #show: yes-codly #set align(center+horizon) #if i == 1 { codly( highlighted-lines: (6,), ..default-codly ) } else if i == 3 { codly( offset: 5, ..default-codly ) } else { codly(..default-codly) } #if i in (0, 1) { ```java ClassLoader cl = MainActivity.class.getClassLoader(); Class clz = cl.loadClass("Reflectee"); Object obj = clz.newInstance(); Method mth = clz.getMethod("myMethod", String.class); Object[] args = {(Object)"an argument"}; String retData = (String) mth.invoke(obj, args); ``` } else if i == 2{ ```java ClassLoader cl = MainActivity.class.getClassLoader(); Class clz = cl.loadClass(getFromInternet()); Object obj = clz.newInstance(); Method mth = clz.getMethod(getFromInternet(), String.class); Object[] args = {(Object)getFromInternet()}; String retData = (String) mth.invoke(obj, args); ``` } else { ```java String retData = (String) mth.invoke(obj, args); ``` } ] } #for i in range(5) { if i != 0 { counter("logical-slide").update( n => n - 1 ) } slide( title: [Reflection Transformation] )[ #show: yes-codly #set align(center+horizon) #if i == 1 { codly( offset: 5, highlighted-lines: (10,), ..default-codly ) } else if i == 2 { codly( offset: 5, highlights: ( (line: 6, start: 0, end: 13, fill: pirat-color.blue), (line: 8, start: 3, end: 8, fill: pirat-color.blue), (line: 10, start: 3, end: 8, fill: pirat-color.blue), (line: 12, start: 18, end: 32, fill: pirat-color.blue), ), ..default-codly ) } else if i == 3 { codly( offset: 5, highlights: ( (line: 8, start: 12, end: 19, fill: pirat-color.blue), ), ..default-codly ) } else if i == 4 { codly( offset: 5, highlights: ( (line: 7, start: 5, end: 43, fill: pirat-color.blue), (line: 8, start: 21, end: 31, fill: pirat-color.blue), (line: 8, start: 38, end: 45, fill: pirat-color.blue), (line: 8, start: 47, end: 54, fill: pirat-color.blue), ), ..default-codly ) } else { codly( offset: 5, ..default-codly ) } ```java Object objRet; if (T.check_is_reflectee_mymethod_XXXX(mth)) { objRet = (Object)((Reflectee) obj).myMethod((String)args[0]); } else { objRet = mth.invoke(obj, args); } String retData = (String) objRet; ``` ] } #slide( title: [Dynamic Analysis], foreground: ghost-1(x: 97%, y: 10%, height: 70pt) )[ #set align(center+horizon) #show figure.caption: none #set table( fill: (x, y) => { if ( x == 4 and y > 2 ) { highlight-color } else { none } } ) #scale(100%, reflow: true, get_figure()) ] #slide( title: [Collected Bytecode], foreground: eye-1(x: 5%, y: 70%, height: 70pt) )[ #set align(center+horizon) #show link.where(dest: ): it => it.body #show link.where(dest: ): it => it.body #show figure.caption: none #set table( fill: (x, y) => { if ( x == 4 and y > 2 ) { highlight-color } else { none } } ) #scale(90%, reflow: true, get_figure()) ] #for i in range(3) { if i != 0 { counter("logical-slide").update( n => n - 1 ) } slide( title: [Added Calls], )[ #set align(center+horizon) #show link.where(dest: ): it => it.body #show link.where(dest: ): it => it.body #show figure.caption: none #set table( fill: (x, y) => { if ( i == 1 and (x == 3 and y > 1) ) or ( i == 2 and (x == 4 and y > 1) ){ highlight-color } else { none } } ) #scale(90%, reflow: true, get_figure()) ] } #slide( title: [Added Calls], foreground: ghost-3(x: 93%, y: 10%) )[ #import "@preview/diagraph:0.3.5": render #set align(center+horizon) #scale(47%, box(render( read("5_theseus/figs/patched_main_main.dot"), //width: 100%, labels: (name) => { move(dy: -7pt, scale(140%, text(size: 10pt, weight: "bold", name))) } ))) ] #slide( title: [Impact on Finishing Rate], )[ #set align(center+horizon) #show figure.caption: none #scale(90%, reflow: true, get_figure()) ] #slide( title: [Conclusion], )[ #item-by-item[ - We can statically analyse APKs with reflection and dynamic code loading with our method - Our dynamic analysis is questionable - The dynamically loaded bytecode we intercepted is mainly telemetry and advertisement related - We released a tool to instrument APKs ] ] #new-section-slide([Conclusion]) #slide[ We showed that: #item-by-item[ - After five years, more than half the static analysis tools are no longer usable. The size of the application seems to be the most significant factor. - Android behaviour is complex and well known. In the specific case of class loading, we showed that state-of-the-art tools do not match Android, leading to invalid analyses. - APKs can be augmented with instrumentation to improve further analyses with any other tools. - Also, dynamic analysis is still very much not trivial. ] ] #slide( title: [Futur Work], foreground: { ghost-3(x: 80%, y: 70%) ghost-6(x: 20%, y: 30%) ghost-7(x: 70%, y: 20%) } )[ #set align(center+horizon) A lot of engineering, preferably spearheaded by Google. ] #counter("logical-slide").update( n => n - 1 ) #slide( title: [Futur Work] )[ #item-by-item[ - Benchmark to evaluate finishing rate - Make tools reusing sources from Android Open Source Project - Require developpers to provide high coverage tests inputs with the APKs ] ] #empty-slide[ Questions ] /* #slide()[ #get_figure()) ] #slide()[ #pl.toolbox.slide-number #context({ pl.toolbox.all-sections((sections, current) => { for i in sections { repr(i.at("body").at("text")) } }) }) ] */ #pagebreak() #set page(height: auto, margin: 25mm) #bibliography("bibliography.bib")